Browse Source

UI for schema category.

pull/287/head
Sebastian 8 years ago
parent
commit
a874246bb3
  1. 5
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs
  2. 11
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  3. 22
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts
  4. 31
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  5. 35
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss
  6. 39
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  7. 40
      src/Squidex/app/shared/components/schema-category.component.html
  8. 85
      src/Squidex/app/shared/components/schema-category.component.scss
  9. 99
      src/Squidex/app/shared/components/schema-category.component.ts
  10. 1
      src/Squidex/app/shared/declarations.ts
  11. 3
      src/Squidex/app/shared/module.ts
  12. 2
      src/Squidex/app/shared/services/schemas.service.ts
  13. 4
      src/Squidex/app/shared/state/schemas.state.spec.ts
  14. 71
      src/Squidex/app/shared/state/schemas.state.ts

5
src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs

@ -30,6 +30,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; } public string Name { get; set; }
/// <summary>
/// The name of the category.
/// </summary>
public string Category { get; set; }
/// <summary> /// <summary>
/// Indicates if the schema is published. /// Indicates if the schema is published.
/// </summary> /// </summary>

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

@ -16,11 +16,12 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column"> <sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
<li class="nav-item" *ngFor="let schema of schemasFiltered | async; trackBy: trackBySchema"> [name]="category"
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a> [schemas]="schemasState.schemas | async"
</li> [schemasFilter]="schemasFilter.valueChanges | async"
</ul> [isReadonly]="true">
</sqx-schema-category>
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>

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

@ -8,11 +8,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { import { AppsState, SchemasState } from '@app/shared';
AppsState,
SchemaDto,
SchemasState
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-schemas-page', selector: 'sqx-schemas-page',
@ -21,20 +17,10 @@ import {
}) })
export class SchemasPageComponent implements OnInit { export class SchemasPageComponent implements OnInit {
public schemasFilter = new FormControl(); public schemasFilter = new FormControl();
public schemasFiltered =
this.schemasState.publishedSchemas
.combineLatest(this.schemasFilter.valueChanges.startWith(''),
(schemas, query) => {
if (query && query.length > 0) {
return schemas.filter(t => t.name.indexOf(query) >= 0);
} else {
return schemas;
}
});
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
private readonly schemasState: SchemasState public readonly schemasState: SchemasState
) { ) {
} }
@ -42,8 +28,8 @@ export class SchemasPageComponent implements OnInit {
this.schemasState.load().onErrorResumeNext().subscribe(); this.schemasState.load().onErrorResumeNext().subscribe();
} }
public trackBySchema(index: number, schema: SchemaDto) { public trackByCategory(index: number, category: string) {
return schema.id; return category;
} }
} }

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

@ -21,27 +21,16 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column"> <sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
<li class="nav-item" *ngFor="let schema of schemasFiltered | async; trackBy: trackBySchema"> [name]="category"
<a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active"> [schemas]="schemasState.schemas | async"
<div class="row"> [schemasFilter]="schemasFilter.valueChanges | async"
<div class="col col-4"> (removing)="removeCategory(category)">
<span class="schema-name">{{schema.displayName}}</span> </sqx-schema-category>
</div>
<div class="col col-4"> <form [formGroup]="addCategoryForm.form" (submit)="addCategory()">
<span class="schema-user"> <input class="form-control form-control-dark new-category-input" formControlName="name" placeholder="Create new category..." />
<i class="icon-user"></i> {{schema.lastModifiedBy | sqxUserNameRef}} </form>
</span>
</div>
<div class="col col-4 schema-modified">
<small class="item-modified">{{schema.lastModified | sqxFromNow}}</small>
<span class="item-published" [class.unpublished]="!schema.isPublished"></span>
</div>
</div>
</a>
</li>
</ul>
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>

35
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss

@ -15,35 +15,16 @@ $button-size: calc(2.5rem - 2px);
line-height: 2.5rem; line-height: 2.5rem;
} }
.nav-link { .new-category-input {
padding-top: .75rem; & {
padding-bottom: .75rem; margin-top: 1rem;
} background: 0;
border-width: 0;
.schema { border-bottom-width: 1px;
&-name {
@include truncate;
color: $color-dark-foreground;
}
&-modified {
text-align: right;
width: auto;
white-space: nowrap;
padding-left: 0; padding-left: 0;
} }
&-user { &:focus {
@include border-radius(1px); @include box-shadow-none;
@include truncate;
display: inline-block;
background: $color-dark2-control;
padding: .1rem .25rem;
font-size: .8rem;
font-weight: normal;
margin-left: 10px;
margin-bottom: 2px;
max-width: 100%;
vertical-align: middle;
} }
} }

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

@ -6,12 +6,13 @@
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { import {
AppsState, AppsState,
CreateCategoryForm,
MessageBus, MessageBus,
ModalView, ModalView,
SchemaDto, SchemaDto,
@ -29,27 +30,19 @@ export class SchemasPageComponent implements OnDestroy, OnInit {
private schemaCloningSubscription: Subscription; private schemaCloningSubscription: Subscription;
public addSchemaDialog = new ModalView(); public addSchemaDialog = new ModalView();
public addCategoryForm = new CreateCategoryForm(this.formBuilder);
public schemasFilter = new FormControl(); public schemasFilter = new FormControl();
public schemasFiltered =
this.schemasState.schemas
.combineLatest(this.schemasFilter.valueChanges.startWith(''),
(schemas, query) => {
if (query && query.length > 0) {
return schemas.filter(t => t.name.indexOf(query) >= 0);
} else {
return schemas;
}
});
public import: any; public import: any;
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router
private readonly schemasState: SchemasState
) { ) {
} }
@ -76,6 +69,22 @@ export class SchemasPageComponent implements OnDestroy, OnInit {
this.schemasState.load().onErrorResumeNext().subscribe(); this.schemasState.load().onErrorResumeNext().subscribe();
} }
public removeCategory(name: string) {
this.schemasState.removeCategory(name);
}
public addCategory() {
const value = this.addCategoryForm.submit();
if (value) {
try {
this.schemasState.addCategory(value.name);
} finally {
this.addCategoryForm.submitCompleted({});
}
}
}
public onSchemaCreated(schema: SchemaDto) { public onSchemaCreated(schema: SchemaDto) {
this.router.navigate([schema.name], { relativeTo: this.route }); this.router.navigate([schema.name], { relativeTo: this.route });
@ -88,8 +97,8 @@ export class SchemasPageComponent implements OnDestroy, OnInit {
this.addSchemaDialog.show(); this.addSchemaDialog.show();
} }
public trackBySchema(index: number, schema: SchemaDto) { public trackByCategory(index: number, category: string) {
return schema.id; return category;
} }
} }

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

@ -0,0 +1,40 @@
<div dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)">
<div class="drop-indicator"></div>
<div class="header clearfix">
<a class="btn btn-sm btn-link" (click)="toggle()">
<i [class.icon-caret-right]="isOpen" [class.icon-caret-down]="!isOpen"></i>
</a>
<h3>{{displayName}} ({{schemasForCategory.length}})</h3>
<a class="btn btn-sm btn-link float-right" *ngIf="schemasForCategory.length === 0 && !isReadonly" (click)="removing.emit()">
<i class="icon-bin"></i>
</a>
</div>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="isOpen" @fade>
<li class="nav-item" *ngFor="let schema of schemasFiltered; trackBy: trackBySchema" dnd-draggable [dragEnabled]="!isReadonly" [dragData]="schema">
<a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active">
<div class="row" *ngIf="!isReadonly">
<div class="col col-4">
<span class="schema-name schema-name-accent">{{schema.displayName}}</span>
</div>
<div class="col col-4">
<span class="schema-user">
<i class="icon-user"></i> {{schema.lastModifiedBy | sqxUserNameRef}}
</span>
</div>
<div class="col col-4 schema-modified">
<small class="item-modified">{{schema.lastModified | sqxFromNow}}</small>
<span class="item-published" [class.unpublished]="!schema.isPublished"></span>
</div>
</div>
<span class="schema-name" *ngIf="isReadonly">{{schema.displayName}}</span>
</a>
</li>
</ul>
</div>

85
src/Squidex/app/shared/components/schema-category.component.scss

@ -0,0 +1,85 @@
@import '_mixins';
@import '_vars';
$drag-margin: -8px;
h3 {
display: inline-block;
}
.btn {
width: 2rem;
}
.category {
margin-bottom: 1rem;
}
.dnd-drag-start {
border: 0;
}
.droppable {
& {
position: relative;
}
&.dnd-drag-over,
&.dnd-drag-enter {
& {
border: 0;
}
.drop-indicator {
display: block;
}
}
.drop-indicator {
@include absolute($drag-margin, $drag-margin, $drag-margin, $drag-margin);
border: 2px dashed $color-dark-black;
background: none;
display: none;
pointer-events: none;
}
}
.header {
margin-left: -1rem;
}
.nav-link {
padding-top: .75rem;
padding-bottom: .75rem;
}
.schema {
&-name {
@include truncate;
}
&-name-accent {
color: $color-dark-foreground;
}
&-modified {
text-align: right;
width: auto;
white-space: nowrap;
padding-left: 0;
}
&-user {
@include border-radius(1px);
@include truncate;
display: inline-block;
background: $color-dark2-control;
padding: .1rem .25rem;
font-size: .8rem;
font-weight: normal;
margin-left: 10px;
margin-bottom: 2px;
max-width: 100%;
vertical-align: middle;
}
}

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

@ -0,0 +1,99 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import {
fadeAnimation,
ImmutableArray,
LocalStoreService,
SchemaDetailsDto,
SchemaDto,
SchemasState,
Types
} from '@app/shared/internal';
@Component({
selector: 'sqx-schema-category',
styleUrls: ['./schema-category.component.scss'],
templateUrl: './schema-category.component.html',
animations: [
fadeAnimation
]
})
export class SchemaCategoryComponent implements OnInit, OnChanges {
@Output()
public removing = new EventEmitter();
@Input()
public name: string;
@Input()
public isReadonly: boolean;
@Input()
public schemasFilter: string;
@Input()
public schemas: ImmutableArray<SchemaDto>;
public displayName: string;
public schemasFiltered: ImmutableArray<SchemaDto>;
public schemasForCategory: ImmutableArray<SchemaDto>;
public isOpen = true;
public allowDrop = (schema: any) => {
return (Types.is(schema, SchemaDto) || Types.is(schema, SchemaDetailsDto)) && !this.isSameCategory(schema);
}
constructor(
private readonly localStore: LocalStoreService,
private readonly schemasState: SchemasState
) {
}
public ngOnInit() {
this.isOpen = this.localStore.get(`schema-category.${name}`) !== 'false';
}
public toggle() {
this.isOpen = !this.isOpen;
this.localStore.set(`schema-category.${name}`, this.isOpen + '');
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes['schemas'] || changes['schemasFilter']) {
const query = this.schemasFilter;
this.schemasForCategory = this.schemas.filter(x => this.isSameCategory(x));
this.schemasFiltered = this.schemasForCategory.filter(x => !query || x.name.indexOf(query) >= 0);
}
if (changes['name']) {
if (!this.name || this.name.length === 0) {
this.displayName = 'All Schemas';
} else {
this.displayName = this.name;
}
}
}
private isSameCategory(schema: SchemaDto): boolean {
return (!this.name && !schema.category) || schema.category === this.name;
}
public changeCategory(schema: SchemaDto) {
this.schemasState.changeCategory(schema, this.name).onErrorResumeNext().subscribe();
}
public trackBySchema(index: number, schema: SchemaDto) {
return schema.id;
}
}

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

@ -17,5 +17,6 @@ export * from './components/language-selector.component';
export * from './components/markdown-editor.component'; export * from './components/markdown-editor.component';
export * from './components/pipes'; export * from './components/pipes';
export * from './components/rich-editor.component'; export * from './components/rich-editor.component';
export * from './components/schema-category.component';
export * from './internal'; export * from './internal';

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

@ -61,6 +61,7 @@ import {
RuleEventsState, RuleEventsState,
RulesService, RulesService,
RulesState, RulesState,
SchemaCategoryComponent,
SchemaMustExistGuard, SchemaMustExistGuard,
SchemaMustExistPublishedGuard, SchemaMustExistPublishedGuard,
SchemasService, SchemasService,
@ -99,6 +100,7 @@ import {
HistoryListComponent, HistoryListComponent,
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
SchemaCategoryComponent,
UserDtoPicture, UserDtoPicture,
UserIdPicturePipe, UserIdPicturePipe,
UserNamePipe, UserNamePipe,
@ -122,6 +124,7 @@ import {
LanguageSelectorComponent, LanguageSelectorComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
RouterModule, RouterModule,
SchemaCategoryComponent,
UserDtoPicture, UserDtoPicture,
UserIdPicturePipe, UserIdPicturePipe,
UserNamePipe, UserNamePipe,

2
src/Squidex/app/shared/services/schemas.service.ts

@ -603,7 +603,7 @@ export class UpdateSchemaDto {
export class UpdateSchemaCategoryDto { export class UpdateSchemaCategoryDto {
constructor( constructor(
public readonly category?: string public readonly name?: string
) { ) {
} }
} }

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

@ -174,7 +174,7 @@ describe('SchemasState', () => {
it('should change category and update user info when category changed', () => { it('should change category and update user info when category changed', () => {
const category = 'my-new-category'; const category = 'my-new-category';
schemasService.setup(x => x.putCategory(app, oldSchemas[0].name, It.is<UpdateSchemaCategoryDto>(i => i.category === category), version)) schemasService.setup(x => x.putCategory(app, oldSchemas[0].name, It.is<UpdateSchemaCategoryDto>(i => i.name === category), version))
.returns(() => Observable.of(new Versioned<any>(newVersion, {}))); .returns(() => Observable.of(new Versioned<any>(newVersion, {})));
schemasState.changeCategory(oldSchemas[0], category, modified).subscribe(); schemasState.changeCategory(oldSchemas[0], category, modified).subscribe();
@ -206,7 +206,7 @@ describe('SchemasState', () => {
it('should change category and update user info when category of selected schema changed', () => { it('should change category and update user info when category of selected schema changed', () => {
const category = 'my-new-category'; const category = 'my-new-category';
schemasService.setup(x => x.putCategory(app, oldSchemas[0].name, It.is<UpdateSchemaCategoryDto>(i => i.category === category), version)) schemasService.setup(x => x.putCategory(app, oldSchemas[0].name, It.is<UpdateSchemaCategoryDto>(i => i.name === category), version))
.returns(() => Observable.of(new Versioned<any>(newVersion, {}))); .returns(() => Observable.of(new Versioned<any>(newVersion, {})));
schemasState.changeCategory(oldSchemas[0], category, modified).subscribe(); schemasState.changeCategory(oldSchemas[0], category, modified).subscribe();

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

@ -43,6 +43,14 @@ import {
const FALLBACK_NAME = 'my-schema'; const FALLBACK_NAME = 'my-schema';
export class CreateCategoryForm extends Form<FormGroup> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['']
}));
}
}
export class CreateSchemaForm extends Form<FormGroup> { export class CreateSchemaForm extends Form<FormGroup> {
public schemaName = public schemaName =
this.form.controls['name'].valueChanges.map(n => n || FALLBACK_NAME) this.form.controls['name'].valueChanges.map(n => n || FALLBACK_NAME)
@ -150,6 +158,8 @@ export class AddFieldForm extends Form<FormGroup> {
} }
interface Snapshot { interface Snapshot {
categories: { [name: string]: boolean };
schemasApp?: string; schemasApp?: string;
schemas: ImmutableArray<SchemaDto>; schemas: ImmutableArray<SchemaDto>;
@ -164,6 +174,10 @@ export class SchemasState extends State<Snapshot> {
this.changes.map(x => x.selectedSchema) this.changes.map(x => x.selectedSchema)
.distinctUntilChanged(); .distinctUntilChanged();
public categories =
this.changes.map(x => ImmutableArray.of(Object.keys(x.categories)).sortByStringAsc(s => s))
.distinctUntilChanged();
public schemas = public schemas =
this.changes.map(x => x.schemas) this.changes.map(x => x.schemas)
.distinctUntilChanged(); .distinctUntilChanged();
@ -186,7 +200,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.of() }); super({ schemas: ImmutableArray.empty(), categories: {} });
} }
public select(idOrName: string | null): Observable<SchemaDetailsDto | null> { public select(idOrName: string | null): Observable<SchemaDetailsDto | null> {
@ -220,7 +234,9 @@ export class SchemasState extends State<Snapshot> {
return this.next(s => { return this.next(s => {
const schemas = ImmutableArray.of(dtos).sortByStringAsc(x => x.displayName); const schemas = ImmutableArray.of(dtos).sortByStringAsc(x => x.displayName);
return { ...s, schemas, schemasApp: this.appName, isLoaded: true }; const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, schemasApp: this.appName, isLoaded: true, categories };
}); });
}) })
.notify(this.dialogs); .notify(this.dialogs);
@ -250,6 +266,22 @@ export class SchemasState extends State<Snapshot> {
.notify(this.dialogs); .notify(this.dialogs);
} }
public addCategory(name: string) {
this.next(s => {
const categories = addCategory(s.categories, name);
return { ...s, categories: categories };
});
}
public removeCategory(name: string) {
this.next(s => {
const categories = removeCategory(s.categories, name);
return { ...s, categories: categories };
});
}
public addField(schema: SchemaDetailsDto, request: AddFieldDto, now?: DateTime): Observable<FieldDto> { public addField(schema: SchemaDetailsDto, request: AddFieldDto, now?: DateTime): Observable<FieldDto> {
return this.schemasService.postField(this.appName, schema.name, request, schema.version) return this.schemasService.postField(this.appName, schema.name, request, schema.version)
.do(dto => { .do(dto => {
@ -366,7 +398,9 @@ export class SchemasState extends State<Snapshot> {
const schemas = s.schemas.replaceBy('id', schema).sortByStringAsc(x => x.displayName); const schemas = s.schemas.replaceBy('id', schema).sortByStringAsc(x => x.displayName);
const selectedSchema = s.selectedSchema && s.selectedSchema.id === schema.id ? schema : s.selectedSchema; const selectedSchema = s.selectedSchema && s.selectedSchema.id === schema.id ? schema : s.selectedSchema;
return { ...s, schemas, selectedSchema }; const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, selectedSchema, categories };
}); });
} }
@ -379,6 +413,37 @@ export class SchemasState extends State<Snapshot> {
} }
} }
function buildCategories(categories: { [name: string]: boolean }, schemas: ImmutableArray<SchemaDto>) {
categories = { ...categories };
for (let category in categories) {
if (!categories[category]) {
delete categories[category];
}
}
for (let schema of schemas.values) {
categories[schema.category || ''] = false;
}
return categories;
}
function addCategory(categories: { [name: string]: boolean }, category: string) {
categories = { ...categories };
categories[category] = true;
return categories;
}
function removeCategory(categories: { [name: string]: boolean }, category: string) {
categories = { ...categories };
delete categories[category];
return categories;
}
const setPublished = (schema: SchemaDto | SchemaDetailsDto, publish: boolean, user: string, version: Version, now?: DateTime) => { const setPublished = (schema: SchemaDto | SchemaDetailsDto, publish: boolean, user: string, version: Version, now?: DateTime) => {
if (Types.is(schema, SchemaDetailsDto)) { if (Types.is(schema, SchemaDetailsDto)) {
return new SchemaDetailsDto( return new SchemaDetailsDto(

Loading…
Cancel
Save