Browse Source

Content selector.

pull/272/head
Sebastian Stehle 8 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/search-form.component';
export * from './pages/schemas/schemas-page.component';
export * from './shared/assets-editor.component';
export * from './shared/content-item.component';
export * from './shared/contents-selector.component';
export * from './shared/references-editor.component';

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

@ -25,6 +25,7 @@ import {
ContentPageComponent,
ContentItemComponent,
ContentsPageComponent,
ContentsSelectorComponent,
ReferencesEditorComponent,
SchemasPageComponent,
SearchFormComponent
@ -48,19 +49,7 @@ const routes: Routes = [
{
path: 'new',
component: ContentPageComponent,
canDeactivate: [CanDeactivateGuard],
children: [
{
path: 'references/:schemaName/:language',
component: ContentsPageComponent,
data: {
isReadOnly: true
},
resolve: {
schema: ResolvePublishedSchemaGuard
}
}
]
canDeactivate: [CanDeactivateGuard]
},
{
path: ':contentId',
@ -76,16 +65,6 @@ const routes: Routes = [
data: {
channel: 'contents.{contentId}'
}
},
{
path: 'references/:schemaName/:language',
component: ContentsPageComponent,
data: {
isReadOnly: true
},
resolve: {
schema: ResolvePublishedSchemaGuard
}
}
]
}
@ -108,6 +87,7 @@ const routes: Routes = [
ContentItemComponent,
ContentPageComponent,
ContentsPageComponent,
ContentsSelectorComponent,
ReferencesEditorComponent,
SchemasPageComponent,
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">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
[schemaFields]="contentFields"
[selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)"
(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">
<ng-container title>
@ -17,7 +17,7 @@
<ng-container content>
<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>
</li>
</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.
*/
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import {
AppContext,
AppsState,
SchemaDto,
SchemasService
SchemasState
} from '@app/shared';
@Component({
selector: 'sqx-schemas-page',
styleUrls: ['./schemas-page.component.scss'],
templateUrl: './schemas-page.component.html',
providers: [
AppContext
]
templateUrl: './schemas-page.component.html'
})
export class SchemasPageComponent {
export class SchemasPageComponent implements OnInit {
public schemasFilter = new FormControl();
public schemasFiltered =
this.schemasFilter.valueChanges
.startWith(null)
.distinctUntilChanged()
.debounceTime(300)
.combineLatest(this.loadSchemas(),
(query, schemas) => {
this.schemasFilter.setValue(query);
schemas = schemas.filter(t => t.isPublished);
this.schemasState.publishedSchemas
.combineLatest(this.schemasFilter.valueChanges.startWith(''),
(schemas, query) => {
if (query && query.length > 0) {
schemas = schemas.filter(t => t.name.indexOf(query) >= 0);
}
return schemas;
}).map(schemas => {
return schemas.sort((a, b) => {
if (a.name < b.name) {
return -1;
return schemas.filter(t => t.name.indexOf(query) >= 0);
} else {
return schemas;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
});
constructor(public readonly ctx: AppContext,
private readonly schemasService: SchemasService
constructor(
public readonly appsState: AppsState,
private readonly schemasState: SchemasState
) {
}
private loadSchemas(): Observable<SchemaDto[]> {
return this.schemasService.getSchemas(this.ctx.appName)
.catch(error => {
this.ctx.notifyError(error);
return [];
});
public ngOnInit() {
this.schemasState.loadSchemas().subscribe();
}
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"
[ngModel]="selected"
(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="drop-area-container" *ngIf="schema && !isDisabled">
<div class="drop-area" dnd-droppable (onDropSuccess)="onContentDropped($event.dragData.content)" [allowDrop]="canDrop()" [routerLink]="['references', schemaId, languageCode]">
Drop content here to add a reference.
<div class="drop-area" (click)="selectorModal.show()">
click here to link a content.
</div>
</div>
@ -14,8 +14,8 @@
<ng-template ngFor let-content let-i="index" [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="onContentsSorted($event)"
[language]="language"
[schemaFields]="contentFields"
[schema]="schema"
[schemaFields]="schemaFields"
(deleting)="onContentRemoving(content)"
isReadOnly="true"
isReference="true"></tr>
@ -23,4 +23,13 @@
</ng-template>
</tbody>
</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-weight: normal;
text-align: center;
padding: 2rem;
padding: 1rem;
cursor: pointer;
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 {
AppContext,
AppLanguageDto,
AppsState,
ContentDto,
ContentsService,
FieldDto,
ImmutableArray,
MathHelper,
ModalView,
SchemaDetailsDto,
SchemasService,
Types
@ -32,7 +33,6 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
styleUrls: ['./references-editor.component.scss'],
templateUrl: './references-editor.component.html',
providers: [
AppContext,
SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR
]
})
@ -46,15 +46,18 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
@Input()
public language: AppLanguageDto;
public selectorModal = new ModalView();
public schema: SchemaDetailsDto;
public schemaFields: FieldDto[];
public contentItems = ImmutableArray.empty<ContentDto>();
public contentFields: FieldDto[];
public isDisabled = false;
public isInvalidSchema = false;
constructor(public readonly ctx: AppContext,
constructor(
private readonly appsState: AppsState,
private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService
) {
@ -65,7 +68,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
return;
}
this.schemasService.getSchema(this.ctx.appName, this.schemaId)
this.schemasService.getSchema(this.appsState.appName, this.schemaId)
.subscribe(dto => {
this.schema = dto;
@ -81,7 +84,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
if (Types.isArrayOfString(value) && value.length > 0) {
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 => {
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;
}
public canDrop() {
const component = this;
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);
public onContentsSelected(contents: ContentDto[]) {
for (let content of contents) {
this.contentItems = this.contentItems.push(content);
}
if (contents.length > 0) {
this.updateValue();
}
this.selectorModal.hide();
}
public onContentRemoving(content: ContentDto) {
@ -144,14 +143,14 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
}
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) {
this.contentFields = [this.schema.fields[0]];
if (this.schemaFields.length === 0 && this.schema.fields.length > 0) {
this.schemaFields = [this.schema.fields[0]];
}
if (this.contentFields.length === 0) {
this.contentFields = [<any>{}];
if (this.schemaFields.length === 0) {
this.schemaFields = [<any>{}];
}
}
}

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

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

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

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

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

@ -31,6 +31,12 @@ export class ModalDialogComponent implements AfterViewInit {
@Input()
public fullHeight = false;
@Input()
public tabsClass = '';
@Input()
public contentClass = '';
@Output()
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)
.distinctUntilChanged();
public publishedSchemas =
this.changes.map(s => s.schemas.filter(x => x.isPublished))
.distinctUntilChanged();
public get schemaName() {
return this.snapshot.selectedSchema!.name;
}

Loading…
Cancel
Save