mirror of https://github.com/Squidex/squidex.git
29 changed files with 728 additions and 140 deletions
@ -1,16 +0,0 @@ |
|||||
<sqx-title message="{app} | Content" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
|
||||
|
|
||||
<div class="panel panel-light"> |
|
||||
<div class="panel-header"> |
|
||||
<h3 class="panel-title">Content</h3> |
|
||||
|
|
||||
<a class="panel-close" dashboardLink> |
|
||||
<i class="icon-close"></i> |
|
||||
</a> |
|
||||
</div> |
|
||||
|
|
||||
<div class="panel-main"> |
|
||||
<div class="panel-content"> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
@ -1,27 +0,0 @@ |
|||||
/* |
|
||||
* Squidex Headless CMS |
|
||||
* |
|
||||
* @license |
|
||||
* Copyright (c) Sebastian Stehle. All rights reserved |
|
||||
*/ |
|
||||
|
|
||||
import { Component } from '@angular/core'; |
|
||||
|
|
||||
import { |
|
||||
AppComponentBase, |
|
||||
AppsStoreService, |
|
||||
NotificationService, |
|
||||
UsersProviderService |
|
||||
} from 'shared'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'sqx-content-page', |
|
||||
styleUrls: ['./content-page.component.scss'], |
|
||||
templateUrl: './content-page.component.html' |
|
||||
}) |
|
||||
export class ContentPageComponent extends AppComponentBase { |
|
||||
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService) { |
|
||||
super(apps, notifications, users); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -0,0 +1,95 @@ |
|||||
|
<sqx-title message="{app} | Content" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
||||
|
|
||||
|
<form [formGroup]="contentForm" (ngSubmit)="saveSchema()"> |
||||
|
<div class="panel panel-light" > |
||||
|
<div class="panel-header"> |
||||
|
<div class="float-xs-right"> |
||||
|
<button type="submit" class="btn btn-primary"> |
||||
|
Save |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<h3 class="panel-title" *ngIf="isNewMode">New {{schema|displayName}}</h3> |
||||
|
|
||||
|
<a class="panel-close" routerLink="../"> |
||||
|
<i class="icon-close"></i> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<div class="panel-main"> |
||||
|
<div class="panel-content"> |
||||
|
<div *ngFor="let field of schema.fields"> |
||||
|
<div class="table-items-row"> |
||||
|
<label>{{field|displayName:'properties.label':'name'}}</label> |
||||
|
|
||||
|
<div> |
||||
|
<div [ngSwitch]="field.properties.fieldType"> |
||||
|
<div *ngSwitchCase="'number'"> |
||||
|
<div [ngSwitch]="field.properties.editor"> |
||||
|
<div *ngSwitchCase="'Input'"> |
||||
|
<input class="form-control" type="number" [formControlName]="field.name"> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'Dropdown'"> |
||||
|
<select class="form-control" [formControlName]="field.name"> |
||||
|
<option *ngFor="let value of field.properties.allowedValues">{{value}}</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'Radio'"> |
||||
|
<label *ngFor="let value of field.properties.allowedValues"> |
||||
|
<input type="radio" value="{{value}}" [formControlName]="field.name"> {{value}} |
||||
|
</label> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'string'"> |
||||
|
<div [ngSwitch]="field.properties.editor"> |
||||
|
<div *ngSwitchCase="'Input'"> |
||||
|
<input class="form-control" type="number" [formControlName]="field.name"> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'Dropdown'"> |
||||
|
<select class="form-control" [formControlName]="field.name"> |
||||
|
<option *ngFor="let value of field.properties.allowedValues">{{value}}</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'TextArea'"> |
||||
|
<textarea class="form-control" [formControlName]="field.name"></textarea> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'Radio'"> |
||||
|
<label *ngFor="let value of field.properties.allowedValues"> |
||||
|
<input type="radio" value="{{value}}" [formControlName]="field.name"> {{value}} |
||||
|
</label> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'boolean'"> |
||||
|
<div [ngSwitch]="field.properties.editor"> |
||||
|
<div *ngSwitchCase="'Checkbox'"> |
||||
|
<div class="form-check"> |
||||
|
<input type="checkbox" [formControlName]="field.name"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'Toggle'"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-hint" *ngIf="field.properties.hints && field.properties.hints.length > 0"> |
||||
|
{{field.properties.hints}} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="panel-sidebar" *ngIf="!isNewMode"> |
||||
|
<div class="nav nav-pills nav-stacked nav-light"> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" routerLink="history" routerLinkActive="active"> |
||||
|
<i class="icon-time"></i> |
||||
|
</a> |
||||
|
</li> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,62 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
|
||||
|
import { |
||||
|
AppComponentBase, |
||||
|
AppsStoreService, |
||||
|
NotificationService, |
||||
|
SchemaDetailsDto, |
||||
|
UsersProviderService |
||||
|
} from 'shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-content-page', |
||||
|
styleUrls: ['./content-page.component.scss'], |
||||
|
templateUrl: './content-page.component.html' |
||||
|
}) |
||||
|
export class ContentPageComponent extends AppComponentBase { |
||||
|
public schema: SchemaDetailsDto; |
||||
|
|
||||
|
public contentForm: FormGroup; |
||||
|
|
||||
|
public isNewMode = false; |
||||
|
|
||||
|
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, |
||||
|
private readonly route: ActivatedRoute |
||||
|
) { |
||||
|
super(apps, notifications, users); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.route.params.map(p => p['contentId']).subscribe(contentId => { |
||||
|
this.isNewMode = !contentId; |
||||
|
}); |
||||
|
|
||||
|
this.route.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => { |
||||
|
this.schema = schema; |
||||
|
|
||||
|
this.setupForm(schema); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private setupForm(schema: SchemaDetailsDto) { |
||||
|
const controls: { [key: string]: AbstractControl } = {}; |
||||
|
|
||||
|
for (const field of schema.fields) { |
||||
|
const formControl = new FormControl(); |
||||
|
|
||||
|
controls[field.name] = formControl; |
||||
|
} |
||||
|
|
||||
|
this.contentForm = new FormGroup(controls); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,33 @@ |
|||||
|
<sqx-title message="{app} | Contents" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
||||
|
|
||||
|
<div class="panel panel-light"> |
||||
|
<div class="panel-header"> |
||||
|
<div class="float-xs-right"> |
||||
|
<a class="btn btn-success" [routerLink]="['new']"> |
||||
|
<i class="icon-plus"></i> New |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<h3 class="panel-title">{{schema|displayName}} Contents</h3> |
||||
|
|
||||
|
<a class="panel-close" routerLink="../"> |
||||
|
<i class="icon-close"></i> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<div class="panel-main"> |
||||
|
<div class="panel-content"> |
||||
|
</div> |
||||
|
<div class="panel-sidebar"> |
||||
|
<div class="nav nav-pills nav-stacked nav-light"> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" routerLink="history" routerLinkActive="active"> |
||||
|
<i class="icon-time"></i> |
||||
|
</a> |
||||
|
</li> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<router-outlet></router-outlet> |
||||
@ -0,0 +1,7 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.panel { |
||||
|
min-width: 600px; |
||||
|
max-width: 600px; |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
|
||||
|
import { |
||||
|
AppComponentBase, |
||||
|
AppsStoreService, |
||||
|
NotificationService, |
||||
|
SchemaDetailsDto, |
||||
|
UsersProviderService |
||||
|
} from 'shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-contents-page', |
||||
|
styleUrls: ['./contents-page.component.scss'], |
||||
|
templateUrl: './contents-page.component.html' |
||||
|
}) |
||||
|
export class ContentsPageComponent extends AppComponentBase implements OnInit { |
||||
|
public schema: SchemaDetailsDto; |
||||
|
|
||||
|
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, |
||||
|
private readonly route: ActivatedRoute |
||||
|
) { |
||||
|
super(apps, notifications, users); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.route.data.map(p => p['schema']).subscribe(schema => { |
||||
|
this.schema = schema; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,35 @@ |
|||||
|
<sqx-title message="{app} | Schemas" parameter1="app" value1="{{appName() | async}}"></sqx-title> |
||||
|
|
||||
|
<div class="panel panel-dark"> |
||||
|
<div class="panel-header"> |
||||
|
<div> |
||||
|
<h3 class="panel-title">Schemas</h3> |
||||
|
|
||||
|
<a class="panel-close" dashboardLink> |
||||
|
<i class="icon-close"></i> |
||||
|
</a> |
||||
|
</div> |
||||
|
|
||||
|
<div class="subheader"> |
||||
|
<div class="search-form"> |
||||
|
<input class="form-control form-control-dark" [formControl]="schemasFilter" placeholder="Search for schemas..." /> |
||||
|
|
||||
|
<i class="icon-search"></i> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="panel-main"> |
||||
|
<div class="panel-content"> |
||||
|
<div class="schemas"> |
||||
|
<div class="schema" *ngFor="let schema of schemasFiltered | async" [routerLink]="[schema.name]" routerLinkActive="active"> |
||||
|
<div class="schema-inner"> |
||||
|
<span class="schema-name">{{schema|displayName}}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<router-outlet></router-outlet> |
||||
@ -0,0 +1,83 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.panel { |
||||
|
min-width: 280px; |
||||
|
max-width: 280px; |
||||
|
} |
||||
|
|
||||
|
.panel-header { |
||||
|
min-height: 120px; |
||||
|
max-height: 120px; |
||||
|
} |
||||
|
|
||||
|
.subheader { |
||||
|
@include flex-box; |
||||
|
@include flex-flow(row); |
||||
|
margin-top: 2rem; |
||||
|
margin-right: -40px; |
||||
|
} |
||||
|
|
||||
|
.search-form { |
||||
|
& { |
||||
|
@include flex-grow(1); |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.form-control { |
||||
|
padding-left: 50px; |
||||
|
} |
||||
|
|
||||
|
.icon-search { |
||||
|
@include absolute(10px, auto, auto, 12px); |
||||
|
color: $color-dark-foreground-selected; |
||||
|
font-size: 1.3rem; |
||||
|
font-weight: lighter; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.schemas { |
||||
|
margin-left: -$panel-padding; |
||||
|
margin-right: -$panel-padding; |
||||
|
} |
||||
|
|
||||
|
.schema { |
||||
|
& { |
||||
|
padding-left: $panel-padding; |
||||
|
padding-right: $panel-padding; |
||||
|
} |
||||
|
|
||||
|
&:hover, |
||||
|
&.active { |
||||
|
& { |
||||
|
background: $color-dark-background-selected; |
||||
|
} |
||||
|
|
||||
|
& + .schema > .schema-inner { |
||||
|
border-color: transparent; |
||||
|
} |
||||
|
|
||||
|
.schema-inner { |
||||
|
border-color: transparent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-inner { |
||||
|
padding-top: 1rem; |
||||
|
padding-bottom: 1rem; |
||||
|
border-top: 1px solid darken($color-dark-foreground, 10%); |
||||
|
} |
||||
|
|
||||
|
&-name { |
||||
|
@include truncate; |
||||
|
color: $color-dark-foreground-selected; |
||||
|
font-size: 1rem; |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
|
||||
|
&:first-child { |
||||
|
.schema-inner { |
||||
|
border: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { FormControl } from '@angular/forms'; |
||||
|
import { BehaviorSubject, Observable } from 'rxjs'; |
||||
|
|
||||
|
import { |
||||
|
AppComponentBase, |
||||
|
AppsStoreService, |
||||
|
ImmutableArray, |
||||
|
NotificationService, |
||||
|
SchemaDto, |
||||
|
SchemasService, |
||||
|
UsersProviderService |
||||
|
} from 'shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-schemas-page', |
||||
|
styleUrls: ['./schemas-page.component.scss'], |
||||
|
templateUrl: './schemas-page.component.html' |
||||
|
}) |
||||
|
export class SchemasPageComponent extends AppComponentBase implements OnInit { |
||||
|
public schemas = new BehaviorSubject(ImmutableArray.empty<SchemaDto>()); |
||||
|
public schemasFilter = new FormControl(); |
||||
|
public schemasFiltered = |
||||
|
Observable.of(null) |
||||
|
.merge(this.schemasFilter.valueChanges.debounceTime(100)) |
||||
|
.combineLatest(this.schemas, |
||||
|
(query, schemas) => { |
||||
|
|
||||
|
schemas = schemas.filter(t => t.isPublished); |
||||
|
|
||||
|
if (query && query.length > 0) { |
||||
|
schemas = schemas.filter(t => t.name.indexOf(query) >= 0); |
||||
|
} |
||||
|
|
||||
|
schemas = |
||||
|
schemas.sort((a, b) => { |
||||
|
if (a.name < b.name) { |
||||
|
return -1; |
||||
|
} |
||||
|
if (a.name > b.name) { |
||||
|
return 1; |
||||
|
} |
||||
|
return 0; |
||||
|
}); |
||||
|
|
||||
|
return schemas; |
||||
|
}); |
||||
|
|
||||
|
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, |
||||
|
private readonly schemasService: SchemasService |
||||
|
) { |
||||
|
super(apps, notifications, users); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.load(); |
||||
|
} |
||||
|
|
||||
|
public load() { |
||||
|
this.appName() |
||||
|
.switchMap(app => this.schemasService.getSchemas(app).retry(2)) |
||||
|
.subscribe(dtos => { |
||||
|
this.schemas.next(ImmutableArray.of(dtos)); |
||||
|
}, error => { |
||||
|
this.notifyError(error); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,55 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; |
||||
|
|
||||
|
import { SchemaDetailsDto, SchemasService } from './../services/schemas.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class ResolvePublishedSchemaGuard implements Resolve<SchemaDetailsDto> { |
||||
|
constructor( |
||||
|
private readonly schemasService: SchemasService, |
||||
|
private readonly router: Router |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<SchemaDetailsDto> { |
||||
|
const appName = this.findParameter(route, 'appName'); |
||||
|
const schemaName = this.findParameter(route, 'schemaName'); |
||||
|
|
||||
|
const result = |
||||
|
this.schemasService.getSchema(appName, schemaName).toPromise() |
||||
|
.then(dto => { |
||||
|
if (!dto || !dto.isPublished) { |
||||
|
this.router.navigate(['/404']); |
||||
|
} |
||||
|
|
||||
|
return dto; |
||||
|
}).catch(() => { |
||||
|
this.router.navigate(['/404']); |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private findParameter(route: ActivatedRouteSnapshot, name: string) { |
||||
|
let result: string; |
||||
|
|
||||
|
while (route) { |
||||
|
result = route.params[name]; |
||||
|
|
||||
|
if (result) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
route = route.parent; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; |
||||
|
|
||||
|
import { SchemaDetailsDto, SchemasService } from './../services/schemas.service'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class ResolveSchemaGuard implements Resolve<SchemaDetailsDto> { |
||||
|
constructor( |
||||
|
private readonly schemasService: SchemasService, |
||||
|
private readonly router: Router |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<SchemaDetailsDto> { |
||||
|
const appName = this.findParameter(route, 'appName'); |
||||
|
const schemaName = this.findParameter(route, 'schemaName'); |
||||
|
|
||||
|
const result = |
||||
|
this.schemasService.getSchema(appName, schemaName).toPromise() |
||||
|
.then(dto => { |
||||
|
if (!dto) { |
||||
|
this.router.navigate(['/404']); |
||||
|
} |
||||
|
|
||||
|
return dto; |
||||
|
}).catch(() => { |
||||
|
this.router.navigate(['/404']); |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private findParameter(route: ActivatedRouteSnapshot, name: string) { |
||||
|
let result: string; |
||||
|
|
||||
|
while (route) { |
||||
|
result = route.params[name]; |
||||
|
|
||||
|
if (result) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
route = route.parent; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue