Browse Source

Content selector.

pull/272/head
Sebastian Stehle 9 years ago
parent
commit
c132808de2
  1. 2
      src/Squidex/app/features/content/declarations.ts
  2. 26
      src/Squidex/app/features/content/module.ts
  3. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  4. 4
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  5. 60
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts
  6. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  7. 70
      src/Squidex/app/features/content/shared/contents-selector.component.html
  8. 38
      src/Squidex/app/features/content/shared/contents-selector.component.scss
  9. 147
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  10. 17
      src/Squidex/app/features/content/shared/references-editor.component.html
  11. 2
      src/Squidex/app/features/content/shared/references-editor.component.scss
  12. 43
      src/Squidex/app/features/content/shared/references-editor.component.ts
  13. 8
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  14. 4
      src/Squidex/app/framework/angular/modals/modal-dialog.component.html
  15. 6
      src/Squidex/app/framework/angular/modals/modal-dialog.component.ts
  16. 4
      src/Squidex/app/shared/state/schemas.state.ts

2
src/Squidex/app/features/content/declarations.ts

@ -11,6 +11,8 @@ export * from './pages/content/content-page.component';
export * from './pages/contents/contents-page.component'; export * from './pages/contents/contents-page.component';
export * from './pages/contents/search-form.component'; export * from './pages/contents/search-form.component';
export * from './pages/schemas/schemas-page.component'; export * from './pages/schemas/schemas-page.component';
export * from './shared/assets-editor.component'; export * from './shared/assets-editor.component';
export * from './shared/content-item.component'; export * from './shared/content-item.component';
export * from './shared/contents-selector.component';
export * from './shared/references-editor.component'; export * from './shared/references-editor.component';

26
src/Squidex/app/features/content/module.ts

@ -25,6 +25,7 @@ import {
ContentPageComponent, ContentPageComponent,
ContentItemComponent, ContentItemComponent,
ContentsPageComponent, ContentsPageComponent,
ContentsSelectorComponent,
ReferencesEditorComponent, ReferencesEditorComponent,
SchemasPageComponent, SchemasPageComponent,
SearchFormComponent SearchFormComponent
@ -48,19 +49,7 @@ const routes: Routes = [
{ {
path: 'new', path: 'new',
component: ContentPageComponent, component: ContentPageComponent,
canDeactivate: [CanDeactivateGuard], canDeactivate: [CanDeactivateGuard]
children: [
{
path: 'references/:schemaName/:language',
component: ContentsPageComponent,
data: {
isReadOnly: true
},
resolve: {
schema: ResolvePublishedSchemaGuard
}
}
]
}, },
{ {
path: ':contentId', path: ':contentId',
@ -76,16 +65,6 @@ const routes: Routes = [
data: { data: {
channel: 'contents.{contentId}' channel: 'contents.{contentId}'
} }
},
{
path: 'references/:schemaName/:language',
component: ContentsPageComponent,
data: {
isReadOnly: true
},
resolve: {
schema: ResolvePublishedSchemaGuard
}
} }
] ]
} }
@ -108,6 +87,7 @@ const routes: Routes = [
ContentItemComponent, ContentItemComponent,
ContentPageComponent, ContentPageComponent,
ContentsPageComponent, ContentsPageComponent,
ContentsSelectorComponent,
ReferencesEditorComponent, ReferencesEditorComponent,
SchemasPageComponent, SchemasPageComponent,
SearchFormComponent SearchFormComponent

2
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -116,8 +116,8 @@
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackByContent"> <ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackByContent">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active" <tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[language]="languageSelected" [language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema" [schema]="schema"
[schemaFields]="contentFields"
[selected]="isItemSelected(content)" [selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)" (selectedChange)="selectItem(content, $event)"
(unpublishing)="unpublishContent(content)" (unpublishing)="unpublishContent(content)"

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | Schemas" parameter1="app" [value1]="ctx.appName"></sqx-title> <sqx-title message="{app} | Schemas" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel theme="dark" desiredWidth="16rem" showSecondHeader="true"> <sqx-panel theme="dark" desiredWidth="16rem" showSecondHeader="true">
<ng-container title> <ng-container title>
@ -17,7 +17,7 @@
<ng-container content> <ng-container content>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column"> <ul class="nav nav-panel nav-dark nav-dark-bordered flex-column">
<li class="nav-item" *ngFor="let schema of schemasFiltered | async"> <li class="nav-item" *ngFor="let schema of schemasFiltered | async; trackBy: trackBySchema">
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a> <a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a>
</li> </li>
</ul> </ul>

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

@ -5,65 +5,45 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { import {
AppContext, AppsState,
SchemaDto, SchemaDto,
SchemasService SchemasState
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-schemas-page', selector: 'sqx-schemas-page',
styleUrls: ['./schemas-page.component.scss'], styleUrls: ['./schemas-page.component.scss'],
templateUrl: './schemas-page.component.html', templateUrl: './schemas-page.component.html'
providers: [
AppContext
]
}) })
export class SchemasPageComponent { export class SchemasPageComponent implements OnInit {
public schemasFilter = new FormControl(); public schemasFilter = new FormControl();
public schemasFiltered = public schemasFiltered =
this.schemasFilter.valueChanges this.schemasState.publishedSchemas
.startWith(null) .combineLatest(this.schemasFilter.valueChanges.startWith(''),
.distinctUntilChanged() (schemas, query) => {
.debounceTime(300)
.combineLatest(this.loadSchemas(),
(query, schemas) => {
this.schemasFilter.setValue(query);
schemas = schemas.filter(t => t.isPublished);
if (query && query.length > 0) { if (query && query.length > 0) {
schemas = schemas.filter(t => t.name.indexOf(query) >= 0); return schemas.filter(t => t.name.indexOf(query) >= 0);
} } else {
return schemas;
return schemas;
}).map(schemas => {
return schemas.sort((a, b) => {
if (a.name < b.name) {
return -1;
} }
if (a.name > b.name) {
return 1;
}
return 0;
}); });
});
constructor(public readonly ctx: AppContext, constructor(
private readonly schemasService: SchemasService public readonly appsState: AppsState,
private readonly schemasState: SchemasState
) { ) {
} }
private loadSchemas(): Observable<SchemaDto[]> { public ngOnInit() {
return this.schemasService.getSchemas(this.ctx.appName) this.schemasState.loadSchemas().subscribe();
.catch(error => { }
this.ctx.notifyError(error);
return []; public trackBySchema(index: number, schema: SchemaDto) {
}); return schema.id;
} }
} }

2
src/Squidex/app/features/content/shared/content-item.component.html

@ -1,4 +1,4 @@
<td class="cell-select" *ngIf="!isReadOnly" (click)="shouldStop($event)"> <td class="cell-select" *ngIf="!isReference" (click)="shouldStop($event)">
<input type="checkbox" class="form-control" <input type="checkbox" class="form-control"
[ngModel]="selected" [ngModel]="selected"
(ngModelChange)="selectedChange.emit($event);" (ngModelChange)="selectedChange.emit($event);"

70
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -0,0 +1,70 @@
<sqx-modal-dialog (close)="complete()" large="true" fullHeight="true" contentClass="grid">
<ng-container title>
Select contents
</ng-container>
<ng-container tabs>
<form class="form-inline search-form" (ngSubmit)="search()">
<input class="form-control form-control-expandable" [formControl]="contentsFilter" placeholder="Search for content" />
<a class="expand-search" (click)="searchModal.toggle()" #archive>
<i class="icon-caret-down"></i>
</a>
</form>
<button class="btn btn-link btn-secondary" (click)="load(true)">
<i class="icon-reset"></i> Refresh
</button>
</ng-container>
<ng-container content>
<div class="grid-header">
<table class="table table-items table-fixed" *ngIf="contentItems">
<thead>
<tr>
<th class="cell-select">
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th>
<th class="cell-auto" *ngFor="let field of schemaFields">
<span class="field">{{field.displayName}}</span>
</th>
<th class="cell-time">
Updated
</th>
<th class="cell-user">
By
</th>
</tr>
</thead>
</table>
</div>
<div class="grid-content">
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="contentItems">
<tbody>
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackByContent">
<tr [sqxContent]="content"
[selected]="isItemSelected(content)"
(selectedChange)="onContentSelected(content)"
[language]="languageSelected"
[schemaFields]="schemaFields"
[schema]="schema"
isReadOnly="true"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
</div>
<div class="grid-footer">
<sqx-pager [pager]="contentsPager"></sqx-pager>
</div>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<button type="submit" class="float-right btn btn-success" (click)="select()" [disabled]="selectionCount === 0">Link selected contents ({{selectionCount}})</button>
</ng-container>
</sqx-modal-dialog>

38
src/Squidex/app/features/content/shared/contents-selector.component.scss

@ -0,0 +1,38 @@
@import '_vars';
@import '_mixins';
.content {
cursor: pointer;
}
.icon-plus {
font-size: .8rem;
}
.search-form {
display: inline-block;
}
.form-inline {
position: relative;
}
.form-control-expandable {
padding-right: 1.5rem;
}
.expand-search {
@include absolute(8px, 8px, auto, auto);
color: $color-border-dark !important;
font-size: .9rem;
font-weight: normal;
cursor: pointer !important;
}
:host /deep/ .modal-body {
background: $color-background;
}
:host /deep/ .modal-tabs {
background: $color-dark-foreground;
}

147
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -0,0 +1,147 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl } from '@angular/forms';
import {
AppsState,
ContentDto,
ContentsService,
DialogService,
FieldDto,
ImmutableArray,
ModalView,
Pager,
SchemaDetailsDto,
LanguageDto
} from '@app/shared';
@Component({
selector: 'sqx-contents-selector',
styleUrls: ['./contents-selector.component.scss'],
templateUrl: './contents-selector.component.html'
})
export class ContentsSelectorComponent implements OnInit {
@Input()
public language: LanguageDto[];
@Input()
public schema: SchemaDetailsDto;
@Input()
public schemaFields: FieldDto[];
@Output()
public selected = new EventEmitter<ContentDto[]>();
public searchModal = new ModalView();
public contentItems: ImmutableArray<ContentDto>;
public contentsFilter = new FormControl();
public contentsQuery = '';
public contentsPager = new Pager(0);
public selectedItems: { [id: string]: ContentDto; } = {};
public selectionCount = 0;
public isAllSelected = false;
constructor(
private readonly appsState: AppsState,
private readonly contentsService: ContentsService,
private readonly dialogs: DialogService
) {
}
public ngOnInit() {
this.load();
}
public load(showInfo = false) {
this.contentsService.getContents(this.appsState.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, undefined, false)
.finally(() => {
this.selectedItems = {};
this.updateSelectionSummary();
})
.subscribe(dtos => {
this.contentItems = ImmutableArray.of(dtos.items);
this.contentsPager = this.contentsPager.setCount(dtos.total);
if (showInfo) {
this.dialogs.notifyInfo('Contents reloaded.');
}
}, error => {
this.dialogs.notifyError(error);
});
}
public search() {
this.contentsQuery = this.contentsFilter.value;
this.contentsPager = new Pager(0);
this.load();
}
public goNext() {
this.contentsPager = this.contentsPager.goNext();
this.load();
}
public goPrev() {
this.contentsPager = this.contentsPager.goPrev();
this.load();
}
public isItemSelected(content: ContentDto) {
return this.selectedItems[content.id];
}
public complete() {
this.selected.emit([]);
}
public select() {
this.selected.emit(Object.values(this.selectedItems));
}
public selectAll(isSelected: boolean) {
this.selectedItems = {};
if (isSelected) {
for (let content of this.contentItems.values) {
this.selectedItems[content.id] = content;
}
}
this.updateSelectionSummary();
}
public onContentSelected(content: ContentDto) {
if (this.selectedItems[content.id]) {
delete this.selectedItems[content.id];
} else {
this.selectedItems[content.id] = content;
}
this.updateSelectionSummary();
}
private updateSelectionSummary() {
this.selectionCount = Object.keys(this.selectedItems).length;
this.isAllSelected = this.selectionCount === this.contentItems.length;
}
public trackByContent(content: ContentDto): string {
return content.id;
}
}

17
src/Squidex/app/features/content/shared/references-editor.component.html

@ -1,7 +1,7 @@
<div class="references-container" [class.disabled]="isDisabled"> <div class="references-container" [class.disabled]="isDisabled">
<div class="drop-area-container" *ngIf="schema && !isDisabled"> <div class="drop-area-container" *ngIf="schema && !isDisabled">
<div class="drop-area" dnd-droppable (onDropSuccess)="onContentDropped($event.dragData.content)" [allowDrop]="canDrop()" [routerLink]="['references', schemaId, languageCode]"> <div class="drop-area" (click)="selectorModal.show()">
Drop content here to add a reference. click here to link a content.
</div> </div>
</div> </div>
@ -14,8 +14,8 @@
<ng-template ngFor let-content let-i="index" [ngForOf]="contentItems"> <ng-template ngFor let-content let-i="index" [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="onContentsSorted($event)" <tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="onContentsSorted($event)"
[language]="language" [language]="language"
[schemaFields]="contentFields"
[schema]="schema" [schema]="schema"
[schemaFields]="schemaFields"
(deleting)="onContentRemoving(content)" (deleting)="onContentRemoving(content)"
isReadOnly="true" isReadOnly="true"
isReference="true"></tr> isReference="true"></tr>
@ -23,4 +23,13 @@
</ng-template> </ng-template>
</tbody> </tbody>
</table> </table>
</div> </div>
<ng-container *sqxModalView="selectorModal;onRoot:true;closeAuto:false">
<sqx-contents-selector
[language]="language"
[schema]="schema"
[schemaFields]="schemaFields"
(selected)="onContentsSelected($event)">
</sqx-contents-selector>
</ng-container>

2
src/Squidex/app/features/content/shared/references-editor.component.scss

@ -32,7 +32,7 @@
font-size: 1.2rem; font-size: 1.2rem;
font-weight: normal; font-weight: normal;
text-align: center; text-align: center;
padding: 2rem; padding: 1rem;
cursor: pointer; cursor: pointer;
color: darken($color-border, 30%); color: darken($color-border, 30%);
} }

43
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -11,13 +11,14 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AppContext,
AppLanguageDto, AppLanguageDto,
AppsState,
ContentDto, ContentDto,
ContentsService, ContentsService,
FieldDto, FieldDto,
ImmutableArray, ImmutableArray,
MathHelper, MathHelper,
ModalView,
SchemaDetailsDto, SchemaDetailsDto,
SchemasService, SchemasService,
Types Types
@ -32,7 +33,6 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
styleUrls: ['./references-editor.component.scss'], styleUrls: ['./references-editor.component.scss'],
templateUrl: './references-editor.component.html', templateUrl: './references-editor.component.html',
providers: [ providers: [
AppContext,
SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR
] ]
}) })
@ -46,15 +46,18 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
@Input() @Input()
public language: AppLanguageDto; public language: AppLanguageDto;
public selectorModal = new ModalView();
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public schemaFields: FieldDto[];
public contentItems = ImmutableArray.empty<ContentDto>(); public contentItems = ImmutableArray.empty<ContentDto>();
public contentFields: FieldDto[];
public isDisabled = false; public isDisabled = false;
public isInvalidSchema = false; public isInvalidSchema = false;
constructor(public readonly ctx: AppContext, constructor(
private readonly appsState: AppsState,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService private readonly schemasService: SchemasService
) { ) {
@ -65,7 +68,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
return; return;
} }
this.schemasService.getSchema(this.ctx.appName, this.schemaId) this.schemasService.getSchema(this.appsState.appName, this.schemaId)
.subscribe(dto => { .subscribe(dto => {
this.schema = dto; this.schema = dto;
@ -81,7 +84,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
if (Types.isArrayOfString(value) && value.length > 0) { if (Types.isArrayOfString(value) && value.length > 0) {
const contentIds: string[] = value; const contentIds: string[] = value;
this.contentsService.getContents(this.ctx.appName, this.schemaId, 10000, 0, undefined, contentIds) this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds)
.subscribe(dtos => { .subscribe(dtos => {
this.contentItems = ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)).filter(r => !!r).map(r => r!)); this.contentItems = ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)).filter(r => !!r).map(r => r!));
}); });
@ -100,20 +103,16 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.callTouched = fn; this.callTouched = fn;
} }
public canDrop() { public onContentsSelected(contents: ContentDto[]) {
const component = this; for (let content of contents) {
this.contentItems = this.contentItems.push(content);
return (dragData: any) => { }
return dragData.content instanceof ContentDto && dragData.schemaId === component.schemaId && !component.contentItems.find(c => c.id === dragData.content.id);
};
}
public onContentDropped(content: ContentDto) {
if (content) {
this.contentItems = this.contentItems.pushFront(content);
if (contents.length > 0) {
this.updateValue(); this.updateValue();
} }
this.selectorModal.hide();
} }
public onContentRemoving(content: ContentDto) { public onContentRemoving(content: ContentDto) {
@ -144,14 +143,14 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
} }
private loadFields() { private loadFields() {
this.contentFields = this.schema.fields.filter(x => x.properties.isListField); this.schemaFields = this.schema.fields.filter(x => x.properties.isListField);
if (this.contentFields.length === 0 && this.schema.fields.length > 0) { if (this.schemaFields.length === 0 && this.schema.fields.length > 0) {
this.contentFields = [this.schema.fields[0]]; this.schemaFields = [this.schema.fields[0]];
} }
if (this.contentFields.length === 0) { if (this.schemaFields.length === 0) {
this.contentFields = [<any>{}]; this.schemaFields = [<any>{}];
} }
} }
} }

8
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html

@ -28,12 +28,12 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<button class="btn btn-sm btn-link" (click)="toggleImport()" [class.hidden]="showImport"> <a class="btn btn-sm btn-link" (click)="toggleImport()" [class.hidden]="showImport">
Import schema Import schema
</button> </a>
<button class="btn btn-sm btn-link" (click)="toggleImport()" [class.hidden]="!showImport"> <a class="btn btn-sm btn-link" (click)="toggleImport()" [class.hidden]="!showImport">
Hide Hide
</button> </a>
<sqx-json-editor *ngIf="showImport" formControlName="import"></sqx-json-editor> <sqx-json-editor *ngIf="showImport" formControlName="import"></sqx-json-editor>
</div> </div>

4
src/Squidex/app/framework/angular/modals/modal-dialog.component.html

@ -13,11 +13,11 @@
</button> </button>
</div> </div>
<div class="modal-tabs clearfix" #tabsElement [hidden]="!showTabs"> <div class="modal-tabs {{tabsClass}} clearfix" #tabsElement [hidden]="!showTabs">
<ng-content select=[tabs]></ng-content> <ng-content select=[tabs]></ng-content>
</div> </div>
<div class="modal-body"> <div class="modal-body {{contentClass}}">
<ng-content select=[content]></ng-content> <ng-content select=[content]></ng-content>
</div> </div>

6
src/Squidex/app/framework/angular/modals/modal-dialog.component.ts

@ -31,6 +31,12 @@ export class ModalDialogComponent implements AfterViewInit {
@Input() @Input()
public fullHeight = false; public fullHeight = false;
@Input()
public tabsClass = '';
@Input()
public contentClass = '';
@Output() @Output()
public close = new EventEmitter(); public close = new EventEmitter();

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

@ -156,6 +156,10 @@ export class SchemasState extends State<Snapshot> {
this.changes.map(s => s.schemas) this.changes.map(s => s.schemas)
.distinctUntilChanged(); .distinctUntilChanged();
public publishedSchemas =
this.changes.map(s => s.schemas.filter(x => x.isPublished))
.distinctUntilChanged();
public get schemaName() { public get schemaName() {
return this.snapshot.selectedSchema!.name; return this.snapshot.selectedSchema!.name;
} }

Loading…
Cancel
Save