Browse Source

A lot of improvements.

pull/271/head
Sebastian Stehle 8 years ago
parent
commit
35b3697bc6
  1. 26
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  2. 102
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  3. 17
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  4. 25
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  5. 146
      src/Squidex/app/features/administration/state/users.state.ts
  6. 66
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.html
  7. 135
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  8. 56
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html
  9. 4
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.scss
  10. 55
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts
  11. 92
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  12. 57
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  13. 96
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  14. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss
  15. 6
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  16. 38
      src/Squidex/app/features/schemas/state/schemas.state.ts
  17. 2
      src/Squidex/app/framework/angular/forms/copy.directive.ts
  18. 3
      src/Squidex/app/framework/angular/forms/slider.component.ts
  19. 31
      src/Squidex/app/framework/angular/modals/modal-dialog.component.html
  20. 6
      src/Squidex/app/framework/angular/modals/modal-dialog.component.scss
  21. 51
      src/Squidex/app/framework/angular/modals/modal-dialog.component.ts
  22. 21
      src/Squidex/app/framework/angular/modals/modal-view.directive.ts
  23. 12
      src/Squidex/app/framework/angular/pager.component.html
  24. 2
      src/Squidex/app/framework/angular/pager.component.scss
  25. 26
      src/Squidex/app/framework/angular/pager.component.ts
  26. 6
      src/Squidex/app/framework/angular/panel.component.html
  27. 9
      src/Squidex/app/framework/angular/panel.component.scss
  28. 3
      src/Squidex/app/framework/angular/panel.component.ts
  29. 0
      src/Squidex/app/framework/angular/routers/parent-link.directive.ts
  30. 8
      src/Squidex/app/framework/declarations.ts
  31. 6
      src/Squidex/app/framework/module.ts
  32. 94
      src/Squidex/app/framework/state.ts

26
src/Squidex/app/features/administration/pages/users/user-page.component.html

@ -1,11 +1,17 @@
<sqx-title message="User Management"></sqx-title>
<form [formGroup]="userForm" (ngSubmit)="save()">
<form [formGroup]="userForm.form" (ngSubmit)="save()">
<input style="display:none" type="password" name="foilautofill"/>
<sqx-panel desiredWidth="26rem" isBlank="true">
<ng-container title>
{{user ? 'Edit User' : 'New User'}}
<ng-container *ngIf="selectedUser | async; else noUser">
Edit User
</ng-container>
<ng-template #noUser>
New User
</ng-template>
</ng-container>
<ng-container menu>
@ -17,30 +23,30 @@
</ng-container>
<ng-container content>
<div *ngIf="userFormError">
<div class="form-alert form-alert-error" [innerHTML]="userFormError"></div>
</div>
<ng-container *ngIf="userForm.error | async; let error">
<div class="form-alert form-alert-error" [innerHTML]="error"></div>
</ng-container>
<div class="form-group">
<label for="email">Email</label>
<sqx-control-errors for="email" [submitted]="userFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="email" [submitted]="userForm.submitted | async"></sqx-control-errors>
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" autocomplete="false" />
</div>
<div class="form-group">
<label for="displayName">Display Name</label>
<sqx-control-errors for="displayName" [submitted]="userFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="displayName" [submitted]="userForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" />
</div>
<div class="form-group form-group-password" [class.hidden]="isCurrentUser">
<div class="form-group form-group-password" [class.hidden]="isCurrentUser | async">
<div class="form-group">
<label for="password">Password</label>
<sqx-control-errors for="password" [submitted]="userFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="password" [submitted]="userForm.submitted | async"></sqx-control-errors>
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="false" />
</div>
@ -48,7 +54,7 @@
<div class="form-group">
<label for="password">Confirm Password</label>
<sqx-control-errors for="passwordConfirm" [submitted]="userFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="passwordConfirm" [submitted]="userForm.submitted | async"></sqx-control-errors>
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="false" />
</div>

102
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -6,14 +6,12 @@
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { AuthService, ValidatorsEx } from '@app/shared';
import { UserDto } from './../../services/users.service';
import { UsersState } from './../../state/users.state';
import { UserForm, UsersState, getSelectedUser } from './../../state/users.state';
@Component({
selector: 'sqx-user-page',
@ -22,47 +20,24 @@ import { UsersState } from './../../state/users.state';
})
export class UserPageComponent implements OnDestroy, OnInit {
private selectedUserSubscription: Subscription;
private user?: UserDto;
public userFormSubmitted = false;
public userFormError = '';
public userForm =
this.formBuilder.group({
email: ['',
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]
],
displayName: ['',
[
Validators.required,
Validators.maxLength(100)
]
],
password: ['',
[
Validators.nullValidator
]
],
passwordConfirm: ['',
[
ValidatorsEx.match('password', 'Passwords must be the same.')
]
]
});
public userForm: UserForm;
public user: UserDto | null;
public selectedUser =
this.usersState.changes.map(getSelectedUser)
.distinctUntilChanged();
public isCurrentUser = false;
public isCurrentUser =
this.usersState.changes.map(x => x.isCurrentUser)
.distinctUntilChanged();
constructor(
private readonly authService: AuthService,
private readonly formBuilder: FormBuilder,
constructor(formBuilder: FormBuilder,
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly usersState: UsersState
) {
this.userForm = new UserForm(formBuilder);
}
public ngOnDestroy() {
@ -71,30 +46,29 @@ export class UserPageComponent implements OnDestroy, OnInit {
public ngOnInit() {
this.selectedUserSubscription =
this.usersState.selectedUser.subscribe(user => this.setupAndPopulateForm(user!));
this.usersState.changes.map(getSelectedUser).subscribe(user => {
this.user = user;
this.userForm.load(user);
});
}
public save() {
this.userFormSubmitted = true;
if (this.userForm.valid) {
this.userForm.disable();
const request = this.userForm.submit();
const requestDto = this.userForm.value;
if (!this.user) {
this.usersState.createUser(requestDto)
if (request) {
if (this.user) {
this.usersState.updateUser(this.user, request)
.subscribe(user => {
this.back();
this.userForm.submitCompleted();
}, error => {
this.resetFormState(error.displayMessage);
this.userForm.submitFailed(error);
});
} else {
this.usersState.updateUser(this.user!, requestDto)
.subscribe(() => {
this.resetFormState();
this.usersState.createUser(request)
.subscribe(user => {
this.back();
}, error => {
this.resetFormState(error.displayMessage);
this.userForm.submitFailed(error);
});
}
}
@ -103,28 +77,4 @@ export class UserPageComponent implements OnDestroy, OnInit {
private back() {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
}
private setupAndPopulateForm(user: UserDto | null) {
this.user = user;
this.isCurrentUser = user !== null && user.id === this.authService.user!.id;
this.userForm.controls['password'].setValidators(
user ?
Validators.nullValidator :
Validators.required);
this.resetFormState();
this.userForm.reset();
this.userForm.patchValue(user || {});
}
private resetFormState(message: string = '') {
this.userFormSubmitted = false;
this.userFormError = message;
this.userForm.controls['password'].reset();
this.userForm.controls['passwordConfirm'].reset();
this.userForm.enable();
}
}

17
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -49,7 +49,7 @@
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed">
<tbody>
<ng-template ngFor let-user [ngForOf]="usersState.users | async" [ngForTrackBy]="usersState.trackByUser">
<ng-template ngFor let-user [ngForOf]="users | async">
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" />
@ -81,20 +81,7 @@
</div>
</div>
<ng-container *ngIf="usersState.usersPager | async as pager">
<div class="grid-footer clearfix" *ngIf="pager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!pager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!pager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>
</ng-container>
<sqx-pager [pager]="usersPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
</ng-container>
</sqx-panel>

25
src/Squidex/app/features/administration/pages/users/users-page.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { AuthService, DialogService } from '@app/shared';
import { AuthService } from '@app/shared';
import { UserDto } from './../../services/users.service';
import { UsersState } from './../../state/users.state';
@ -16,15 +16,21 @@ import { UsersState } from './../../state/users.state';
@Component({
selector: 'sqx-users-page',
styleUrls: ['./users-page.component.scss'],
templateUrl: './users-page.component.html'
templateUrl: './users-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UsersPageComponent implements OnInit {
public usersFilter = new FormControl();
public users =
this.usersState.changes.map(x => x.users);
public usersPager =
this.usersState.changes.map(x => x.usersPager);
constructor(
public readonly usersState: UsersState,
public readonly authState: AuthService,
private readonly dialogs: DialogService
public readonly usersState: UsersState
) {
}
@ -36,13 +42,8 @@ export class UsersPageComponent implements OnInit {
this.usersState.search(this.usersFilter.value).subscribe();
}
public load(showInfo = false) {
this.usersState.loadUsers()
.subscribe(() => {
if (showInfo) {
this.dialogs.notifyInfo('Users reloaded.');
}
});
public load(notify = false) {
this.usersState.loadUsers(notify).subscribe();
}
public lock(user: UserDto) {

146
src/Squidex/app/features/administration/state/users.state.ts

@ -6,14 +6,18 @@
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import '@app/framework/utils/rxjs-extensions';
import {
AuthService,
DialogService,
ImmutableArray,
Pager
Pager,
Form,
State,
ValidatorsEx
} from '@app/shared';
import {
@ -22,54 +26,115 @@ import {
UsersService
} from './../services/users.service';
@Injectable()
export class UsersState {
public users = new BehaviorSubject<ImmutableArray<UserDto>>(ImmutableArray.empty());
public usersPager = new BehaviorSubject(new Pager(0));
public usersQuery = new BehaviorSubject('');
export class UserForm extends Form<FormGroup> {
constructor(
formBuilder: FormBuilder
) {
super(formBuilder.group({
email: ['',
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]
],
displayName: ['',
[
Validators.required,
Validators.maxLength(100)
]
],
password: ['',
[
Validators.nullValidator
]
],
passwordConfirm: ['',
[
ValidatorsEx.match('password', 'Passwords must be the same.')
]
]
}));
}
public load(user?: UserDto) {
if (user) {
this.form.controls['password'].setValidators(null);
} else {
this.form.controls['password'].setValidators(Validators.required);
}
super.load(user);
}
}
public selectedUser = new BehaviorSubject<UserDto | null>(null);
interface Snapshot {
users: UserDto[];
usersPager: Pager;
usersQuery?: string;
selectedUserId?: string | null;
isCurrentUser?: boolean;
}
@Injectable()
export class UsersState extends State<Snapshot> {
constructor(
private readonly usersService: UsersService,
private readonly dialogs: DialogService
private readonly authState: AuthService,
private readonly dialogs: DialogService,
private readonly usersService: UsersService
) {
super({ users: [], usersPager: new Pager(10) });
}
public selectUser(id: string | null): Observable<UserDto | null> {
return this.loadUser(id)
public selectUser(selectedUserId: string | null): Observable<UserDto | null> {
return this.loadUser(selectedUserId)
.do(user => {
this.selectedUser.next(user);
const isCurrentUser = selectedUserId === this.authState.user!.id;
this.next(s => ({...s, selectedUserId, isCurrentUser }));
});
}
private loadUser(id: string | null) {
return !id ?
Observable.of(null) :
Observable.of(this.users.value.find(x => x.id === id))
Observable.of(this.snapshot.users.find(x => x.id === id))
.switchMap(user => {
if (!user) {
return this.usersService.getUser(id).catch(() => Observable.of(null));
} else {
return Observable.of(user);
}
});
}
public loadUsers(): Observable<any> {
return this.usersService.getUsers(this.usersPager.value.pageSize, this.usersPager.value.skip, this.usersQuery.value)
public loadUsers(notify = false): Observable<any> {
return this.usersService.getUsers(this.snapshot.usersPager.pageSize, this.snapshot.usersPager.skip, this.snapshot.usersQuery)
.catch(error => this.dialogs.notifyError(error))
.do(dtos => {
this.users.nextBy(v => ImmutableArray.of(dtos.items));
this.usersPager.nextBy(v => v.setCount(dtos.total));
if (notify) {
this.dialogs.notifyInfo('Users reloaded.');
}
this.next(s => {
const users = dtos.items;
const usersPager = s.usersPager.setCount(dtos.total);
return { ...s, users, usersPager, usersLoading: false };
});
});
}
public createUser(request: CreateUserDto): Observable<UserDto> {
return this.usersService.postUser(request)
.do(dto => {
this.users.nextBy(v => v.pushFront(dto));
this.usersPager.nextBy(v => v.incrementCount());
this.next(s => {
const users = [dto, ...s.users];
const usersPager = s.usersPager.incrementCount();
return { ...s, users, usersPager };
});
});
}
@ -78,7 +143,11 @@ export class UsersState {
.do(() => {
this.dialogs.notifyInfo('User saved successsfull');
this.replaceUser(user.update(request.email, request.displayName));
this.next(s => {
const users = s.users.map(u => u.id === user.id ? u.update(request.email, request.displayName) : u);
return { ...s, users };
});
});
}
@ -86,7 +155,11 @@ export class UsersState {
return this.usersService.lockUser(user.id)
.catch(error => this.dialogs.notifyError(error))
.do(() => {
this.replaceUser(user.lock());
this.next(s => {
const users = s.users.map(u => u.id === user.id ? u.lock() : u);
return { ...s, users };
});
});
}
@ -94,36 +167,31 @@ export class UsersState {
return this.usersService.unlockUser(user.id)
.catch(error => this.dialogs.notifyError(error))
.do(() => {
this.replaceUser(user.unlock());
this.next(s => {
const users = s.users.map(u => u.id === user.id ? u.unlock() : u);
return { ...s, users };
});
});
}
public search(filter: string): Observable<any> {
this.usersPager.nextBy(v => new Pager(0));
this.usersQuery.nextBy(v => filter);
public search(query: string): Observable<any> {
this.next({ usersPager: new Pager(0), usersQuery: query });
return this.loadUsers();
}
public goNext(): Observable<any> {
this.usersPager.nextBy(v => v.goNext());
this.next(s => ({ ...s, usersPager: s.usersPager.goNext() }));
return this.loadUsers();
}
public goPrev(): Observable<any> {
this.usersPager.nextBy(v => v.goPrev());
this.next(s => ({ ...s, usersPager: s.usersPager.goPrev() }));
return this.loadUsers();
}
}
public trackByUser(index: number, user: UserDto): any {
return user.id;
}
private replaceUser(user: UserDto) {
this.users.nextBy(v => v.replaceBy('id', user));
this.selectedUser.nextBy(v => v !== null && v.id === user.id ? user : v);
}
}
export const getSelectedUser = (c: Snapshot) => c.users.find(x => x.id === c.selectedUserId);

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

@ -1,44 +1,36 @@
<div class="modal-dialog">
<div class="modal-content">
<form [formGroup]="editForm" (ngSubmit)="saveSchema()">
<div class="modal-header">
<h4 class="modal-title">Edit Schema</h4>
<form [formGroup]="editForm" (ngSubmit)="saveSchema()">
<sqx-modal-dialog (close)="complete()">
<ng-container title>
Edit Schema
</ng-container>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="complete()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="schemaName">Name</label>
<ng-container content>
<div class="form-group">
<label for="schemaName">Name</label>
<input type="text" class="form-control" id="schemaName" readonly [ngModel]="schema.name" [ngModelOptions]="{standalone: true}" />
</div>
<input type="text" class="form-control" id="schemaName" readonly [ngModel]="schema.name" [ngModelOptions]="{standalone: true}" />
</div>
<div class="form-group">
<label for="schemaLabel">Label</label>
<sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors>
<div class="form-group">
<label for="schemaLabel">Label</label>
<sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="schemaLabel" formControlName="label" />
</div>
<input type="text" class="form-control" id="schemaLabel" formControlName="label" />
</div>
<div class="form-group">
<label for="schemaHints">Hints</label>
<sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors>
<div class="form-group">
<label for="schemaHints">Hints</label>
<sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors>
<textarea type="text" class="form-control" id="schemaHints" formControlName="hints" rows="4"></textarea>
</div>
</div>
<div class="modal-footer">
<div class="clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()" [disabled]="editFormSubmitted">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button>
</div>
<textarea type="text" class="form-control" id="schemaHints" formControlName="hints" rows="4"></textarea>
</div>
</form>
</div>
</div>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()" [disabled]="editFormSubmitted">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button>
</ng-container>
</sqx-modal-dialog>
</form>

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

@ -1,74 +1,66 @@
<sqx-title message="{app} | {schema}" parameter1="app" [value1]="appsState.appName" parameter2="schema" [value2]="schemasState.schemaName"></sqx-title>
<sqx-panel desiredWidth="56rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button type="button" class="btn btn-link btn-export" (click)="exportSchemaDialog.show()">
JSON Preview
</button>
<div class="btn-group" data-toggle="buttons" #buttonPublish>
<button type="button" class="btn btn-publishing btn-toggle" [class.btn-success]="schema.isPublished" [disabled]="schema.isPublished" (click)="publish()">
Published
</button>
<button type="button" class="btn btn-publishing btn-toggle" [class.btn-danger]="!schema.isPublished" [disabled]="!schema.isPublished" (click)="unpublish()">
Unpublished
</button>
</div>
<div class="dropdown dropdown-options">
<button type="button" class="btn btn-link btn-secondary" (click)="editOptionsDropdown.toggle()" [class.active]="editOptionsDropdown.isOpen | async" #buttonOptions>
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="editOptionsDropdown" [sqxModalTarget]="buttonOptions" @fade>
<a class="dropdown-item" (click)="configureScriptsDialog.show()">
Scripts
</a>
<a class="dropdown-item" (click)="cloneSchema()">
Clone
</a>
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="deleteSchema()"
confirmTitle="Delete schema"
confirmText="Do you really want to delete the schema?">
Delete
</a>
</div>
</div>
<sqx-onboarding-tooltip id="history" [for]="buttonOptions" position="bottomRight" after="60000">
Open the context menu to delete the schema or to create some scripts for content changes.
</sqx-onboarding-tooltip>
<sqx-onboarding-tooltip id="history" [for]="buttonPublish" position="bottomRight" after="240000">
Note, that you have to publish the schema before you can add content to it.
</sqx-onboarding-tooltip>
</div>
<ng-container title>
<i class="schema-edit icon-pencil" (click)="editSchemaDialog.show()"></i> {{schema.displayName}}
</ng-container>
<ng-container menu>
<button type="button" class="btn btn-link btn-export" (click)="exportSchemaDialog.show()">
JSON Preview
</button>
<div class="btn-group" data-toggle="buttons" #buttonPublish>
<button type="button" class="btn btn-publishing btn-toggle" [class.btn-success]="schema.isPublished" [disabled]="schema.isPublished" (click)="publish()">
Published
</button>
<button type="button" class="btn btn-publishing btn-toggle" [class.btn-danger]="!schema.isPublished" [disabled]="!schema.isPublished" (click)="unpublish()">
Unpublished
</button>
</div>
<h3 class="panel-title">
<i class="schema-edit icon-pencil" (click)="editSchemaDialog.show()"></i> {{schema.displayName}}
</h3>
<div class="dropdown dropdown-options">
<button type="button" class="btn btn-link btn-secondary" (click)="editOptionsDropdown.toggle()" [class.active]="editOptionsDropdown.isOpen | async" #buttonOptions>
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="editOptionsDropdown" [sqxModalTarget]="buttonOptions" @fade>
<a class="dropdown-item" (click)="configureScriptsDialog.show()">
Scripts
</a>
<a class="dropdown-item" (click)="cloneSchema()">
Clone
</a>
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="deleteSchema()"
confirmTitle="Delete schema"
confirmText="Do you really want to delete the schema?">
Delete
</a>
</div>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<sqx-onboarding-tooltip id="history" [for]="buttonOptions" position="bottomRight" after="60000">
Open the context menu to delete the schema or to create some scripts for content changes.
</sqx-onboarding-tooltip>
<sqx-onboarding-tooltip id="history" [for]="buttonPublish" position="bottomRight" after="240000">
Note, that you have to publish the schema before you can add content to it.
</sqx-onboarding-tooltip>
</ng-container>
<div class="panel-main">
<div class="panel-content panel-content-scroll" dnd-sortable-container [sortableData]="schema.fields">
<ng-container content>
<div dnd-sortable-container [sortableData]="schema.fields">
<div *ngFor="let field of schema.fields; let i = index; trackBy: schemasState.trackByField" dnd-sortable [sortableIndex]="i" (sqxSorted)="sortFields($event)">
<sqx-field [field]="field" [schema]="schema"></sqx-field>
</div>
<button class="btn btn-success field-button" (click)="addFieldDialog.show()">
<i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">Add Field</div>
</button>
</div>
</div>
</ng-container>
</sqx-panel>
<div class="modal" *sqxModalView="editSchemaDialog;onRoot:true" @fade>
@ -95,21 +87,14 @@
</sqx-schema-scripts-form>
</div>
<div class="modal" *sqxModalView="exportSchemaDialog;onRoot:true" @fade>
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Export Schema</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="exportSchemaDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<sqx-json-editor disabled [ngModel]="schemaExport"></sqx-json-editor>
</div>
</div>
</div>
</div>
<ng-container *sqxModalView="exportSchemaDialog;onRoot:true">
<sqx-modal-dialog (close)="exportSchemaDialog.hide()">
<ng-container title>
Export Schema
</ng-container>
<ng-container content>
<sqx-json-editor disabled [ngModel]="schemaExport"></sqx-json-editor>
</ng-container>
</sqx-modal-dialog>
</ng-container>

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

@ -1,38 +1,30 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form [formGroup]="editForm" (ngSubmit)="saveSchema()">
<div class="modal-header">
<h4 class="modal-title">Scripts</h4>
<form [formGroup]="editForm.form" (ngSubmit)="saveSchema()">
<sqx-modal-dialog (close)="complete()">
<ng-container title>
Scripts
</ng-container>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="complete()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-tabs clearfix">
<ul class="nav nav-tabs2">
<li class="nav-item" *ngFor="let script of scripts">
<a class="nav-link" [class.active]="selectedField === 'script' + script" (click)="selectField('script' + script)">{{script}}</a>
</li>
</ul>
</div>
<ng-container tabs>
<ul class="nav nav-tabs2">
<li class="nav-item" *ngFor="let script of editForm.form.controls | sqxKeys">
<a class="nav-link" [class.active]="selectedField === script" (click)="selectField(script)">{{script.substr(6)}}</a>
</li>
</ul>
</ng-container>
<div class="modal-body">
<div class="form-group">
<div *ngFor="let script of scripts">
<div *ngIf="selectedField === 'script' + script">
<sqx-jscript-editor name="script" [formControlName]="'script' + script"></sqx-jscript-editor>
</div>
<ng-container content>
<div class="form-group">
<div *ngFor="let script of editForm.form.controls | sqxKeys">
<div *ngIf="selectedField === script">
<sqx-jscript-editor name="script" [formControlName]="script"></sqx-jscript-editor>
</div>
</div>
</div>
</ng-container>
<div class="modal-footer">
<div class="clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()" [disabled]="editFormSubmitted">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()" [disabled]="editFormSubmitted">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button>
</ng-container>
</sqx-modal-dialog>
</form>

4
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.scss

@ -3,8 +3,4 @@
.nav-link {
cursor: default;
}
.nav-tabs {
border: 0;
}

55
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts

@ -8,9 +8,9 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { SchemaDetailsDto, UpdateSchemaScriptsDto } from '@app/shared';
import { SchemaDetailsDto } from '@app/shared';
import { SchemasState } from './../../state/schemas.state';
import { EditScriptsForm, SchemasState } from './../../state/schemas.state';
@Component({
selector: 'sqx-schema-scripts-form',
@ -26,61 +26,36 @@ export class SchemaScriptsFormComponent implements OnInit {
public selectedField = 'scriptQuery';
public scripts = [
'Query',
'Create',
'Update',
'Delete',
'Change'
];
public editForm: EditScriptsForm;
public editFormSubmitted = false;
public editForm =
this.formBuilder.group({
scriptQuery: '',
scriptCreate: '',
scriptUpdate: '',
scriptDelete: '',
scriptChange: ''
});
constructor(
private readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder
constructor(formBuilder: FormBuilder,
private readonly schemasState: SchemasState
) {
this.editForm = new EditScriptsForm(formBuilder);
}
public ngOnInit() {
this.editForm.patchValue(this.schema);
this.editForm.load(this.schema);
}
public complete() {
this.completed.emit();
}
public saveSchema() {
this.editFormSubmitted = true;
if (this.editForm.valid) {
this.editForm.disable();
public selectField(field: string) {
this.selectedField = field;
}
const requestDto = <UpdateSchemaScriptsDto>this.editForm.value;
public saveSchema() {
const value = this.editForm.submit();
this.schemasState.configureScripts(this.schema, requestDto)
if (value) {
this.schemasState.configureScripts(this.schema, value)
.subscribe(dto => {
this.complete();
}, error => {
this.enableEditForm();
this.editForm.submitFailed();
});
}
}
public selectField(field: string) {
this.selectedField = field;
}
private enableEditForm() {
this.editForm.enable();
this.editFormSubmitted = false;
}
}

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

@ -1,51 +1,49 @@
<div class="modal-dialog">
<div class="modal-content">
<form [formGroup]="createForm" (ngSubmit)="createSchema()">
<div class="modal-header">
<h4 class="modal-title" *ngIf="import">Clone Schema</h4>
<h4 class="modal-title" *ngIf="!import">Create Schema</h4>
<form [formGroup]="createForm.form" (ngSubmit)="createSchema()">
<sqx-modal-dialog (close)="complete()" [large]="false">
<ng-container title>
<ng-container *ngIf="import; else noImport">
Clone Schema
</ng-container>
<ng-template #noImport>
Create Schema
</ng-template>
</ng-container>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="complete()">
<span aria-hidden="true">&times;</span>
</button>
<ng-container content>
<ng-container *ngIf="createForm.error | async; let error">
<div class="form-alert form-alert-error" [innerHTML]="error"></div>
</ng-container>
<div class="form-group">
<label for="schemaName">Name</label>
<sqx-control-errors for="name" submitOnly="true" [submitted]="createForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="schemaName" formControlName="name" autocomplete="off" sqxLowerCaseInput sqxFocusOnInit />
<small class="form-text text-muted">
The schema name becomes part of the api url,<br /> e.g {{apiUrl.buildUrl("api/content/")}}{{appsState.appName}}/<b>{{createForm.schemaName | async}}</b>/.
</small>
<small class="form-text text-muted">
It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later.
</small>
</div>
<div class="modal-body">
<div *ngIf="createFormError">
<div class="form-alert form-alert-error" [innerHTML]="createFormError"></div>
</div>
<div class="form-group">
<label for="schemaName">Name</label>
<sqx-control-errors for="name" submitOnly="true" [submitted]="createFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="schemaName" formControlName="name" autocomplete="off" sqxLowerCaseInput sqxFocusOnInit />
<small class="form-text text-muted">
The schema name becomes part of the api url,<br /> e.g {{apiUrl.buildUrl("api/content/")}}{{appsState.appName}}/<b>{{schemaName | async}}</b>/.
</small>
<small class="form-text text-muted">
It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later.
</small>
</div>
<div class="form-group clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<button type="submit" class="float-right btn btn-success">Create</button>
</div>
<div>
<button 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">
Hide
</button>
<sqx-json-editor *ngIf="showImport" formControlName="import"></sqx-json-editor>
</div>
<div class="form-group clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<button type="submit" class="float-right btn btn-success">Create</button>
</div>
<div>
<button 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">
Hide
</button>
<sqx-json-editor *ngIf="showImport" formControlName="import"></sqx-json-editor>
</div>
</form>
</div>
</div>
</ng-container>
</sqx-modal-dialog>
</form>

57
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts

@ -6,17 +6,11 @@
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { FormBuilder } from '@angular/forms';
import {
ApiUrlConfig,
AppsState,
ValidatorsEx
} from '@app/shared';
import { ApiUrlConfig, AppsState } from '@app/shared';
import { SchemasState } from './../../state/schemas.state';
const FALLBACK_NAME = 'my-schema';
import { CreateForm, SchemasState } from './../../state/schemas.state';
@Component({
selector: 'sqx-schema-form',
@ -30,36 +24,20 @@ export class SchemaFormComponent implements OnInit {
@Input()
public import: any;
public createForm: CreateForm;
public showImport = false;
public createFormError = '';
public createFormSubmitted = false;
public createForm =
this.formBuilder.group({
name: ['',
[
Validators.required,
Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes only (not at the end).')
]
],
import: {}
});
public schemaName =
this.createForm.controls['name'].valueChanges.map(n => n || FALLBACK_NAME)
.startWith(FALLBACK_NAME);
constructor(
constructor(formBuilder: FormBuilder,
public readonly apiUrl: ApiUrlConfig,
public readonly appsState: AppsState,
private readonly schemasState: SchemasState,
private readonly formBuilder: FormBuilder
private readonly schemasState: SchemasState
) {
this.createForm = new CreateForm(formBuilder);
}
public ngOnInit() {
this.createForm.controls['import'].setValue(this.import || {});
this.createForm.load({ import: this.import });
this.showImport = !!this.import;
}
@ -75,25 +53,18 @@ export class SchemaFormComponent implements OnInit {
}
public createSchema() {
this.createFormSubmitted = true;
if (this.createForm.valid) {
this.createForm.disable();
const value = this.createForm.submit();
const schemaName = this.createForm.controls['name'].value;
const schemaDto = Object.assign(this.createForm.controls['import'].value || {}, { name: schemaName });
if (value) {
const schemaName = value.name;
const schemaDto = Object.assign(value.import || {}, { name: schemaName });
this.schemasState.create(schemaDto)
.subscribe(dto => {
this.complete();
}, error => {
this.enableCreateForm(error.displayMessage);
this.createForm.submitFailed(error);
});
}
}
private enableCreateForm(message: string) {
this.createForm.enable();
this.createFormError = message;
}
}

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

@ -1,64 +1,54 @@
<sqx-title message="{app} | Schemas" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel theme="dark" desiredWidth="30rem">
<div class="panel-header">
<div class="panel-title-row">
<h3 class="panel-title"><i class="icon-schemas"></i> Schemas</h3>
</div>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
<div class="panel-header-row">
<sqx-shortcut keys="ctrl+shift+g" (trigger)="addSchemaDialog.show()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<button class="btn btn-success subheader-button" (click)="createSchema(null)" title="New Schema (CTRL + SHIFT + G)">
<i class="icon-plus"></i>
</button>
<div class="search-form">
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas..." />
<i class="icon-search"></i>
</div>
<sqx-panel theme="dark" desiredWidth="30rem" showSecondHeader="true">
<ng-container title>
Schemas
</ng-container>
<ng-container secondHeader>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="addSchemaDialog.show()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<button class="btn btn-success subheader-button" (click)="createSchema(null)" title="New Schema (CTRL + SHIFT + G)">
<i class="icon-plus"></i>
</button>
<div class="search-form">
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas..." />
<i class="icon-search"></i>
</div>
</div>
<div class="panel-main">
<div class="panel-content">
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column">
<li class="nav-item" *ngFor="let schema of schemasFiltered | async; trackBy: schemasState.trackBySchema">
<a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active">
<div class="row">
<div class="col col-4">
<span class="schema-name">{{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>
</ng-container>
<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; trackBy: schemasState.trackBySchema">
<a class="nav-link" [routerLink]="[schema.name]" routerLinkActive="active">
<div class="row">
<div class="col col-4">
<span class="schema-name">{{schema.displayName}}</span>
</div>
</a>
</li>
</ul>
</div>
</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>
</a>
</li>
</ul>
</ng-container>
</sqx-panel>
<div class="modal" *sqxModalView="addSchemaDialog;onRoot:true" @fade>
<div class="modal-backdrop"></div>
<ng-container *sqxModalView="addSchemaDialog;onRoot:true">
<sqx-schema-form [import]="import"
(completed)="addSchemaDialog.hide()">
</sqx-schema-form>
</div>
</ng-container>
<router-outlet></router-outlet>

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

@ -3,11 +3,6 @@
$button-size: calc(2.5rem - 2px);
.panel-header {
min-height: 8rem;
max-height: 8rem;
}
.subheader-button {
height: $button-size;
margin-right: .4rem;

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

@ -12,7 +12,6 @@ import { Subscription } from 'rxjs';
import {
AppsState,
fadeAnimation,
MessageBus,
ModalView
} from '@app/shared';
@ -24,10 +23,7 @@ import { SchemasState } from './../../state/schemas.state';
@Component({
selector: 'sqx-schemas-page',
styleUrls: ['./schemas-page.component.scss'],
templateUrl: './schemas-page.component.html',
animations: [
fadeAnimation
]
templateUrl: './schemas-page.component.html'
})
export class SchemasPageComponent implements OnDestroy, OnInit {
private schemaCloningSubscription: Subscription;

38
src/Squidex/app/features/schemas/state/schemas.state.ts

@ -6,6 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { BehaviorSubject, Observable } from 'rxjs';
import '@app/framework/utils/rxjs-extensions';
@ -18,15 +19,50 @@ import {
DateTime,
DialogService,
FieldDto,
Form,
ImmutableArray,
SchemaDto,
SchemaDetailsDto,
SchemasService,
UpdateFieldDto,
UpdateSchemaScriptsDto,
UpdateSchemaDto
UpdateSchemaDto,
ValidatorsEx
} from '@app/shared';
const FALLBACK_NAME = 'my-schema';
export class CreateForm extends Form<FormGroup> {
public schemaName =
this.form.controls['name'].valueChanges.map(n => n || FALLBACK_NAME)
.startWith(FALLBACK_NAME);
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
name: ['',
[
Validators.required,
Validators.maxLength(40),
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes only (not at the end).')
]
],
import: {}
}));
}
}
export class EditScriptsForm extends Form<FormGroup> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
scriptQuery: '',
scriptCreate: '',
scriptUpdate: '',
scriptDelete: '',
scriptChange: ''
}));
}
}
@Injectable()
export class SchemasState {
public schemasItems = new BehaviorSubject<ImmutableArray<SchemaDto>>(ImmutableArray.empty());

2
src/Squidex/app/framework/angular/forms/copy.directive.ts

@ -57,7 +57,7 @@ export class CopyDirective {
}
if (element instanceof HTMLInputElement) {
element.setSelectionRange(prevSelectionStart, prevSelectionEnd);
element.setSelectionRange(prevSelectionStart!, prevSelectionEnd!);
}
}
}

3
src/Squidex/app/framework/angular/forms/slider.component.ts

@ -26,7 +26,6 @@ export class SliderComponent implements ControlValueAccessor {
private windowMouseMoveListener: Function | null = null;
private windowMouseUpListener: Function | null = null;
private centerStartOffset = 0;
private startValue: number;
private lastValue: number;
private value: number;
private isDragging = false;
@ -87,8 +86,6 @@ export class SliderComponent implements ControlValueAccessor {
public onThumbMouseDown(event: MouseEvent): boolean {
this.centerStartOffset = event.offsetX - this.thumb.nativeElement.clientWidth * 0.5;
this.startValue = this.value;
this.windowMouseMoveListener =
this.renderer.listenGlobal('window', 'mousemove', (e: MouseEvent) => {
this.onMouseMove(e);

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

@ -0,0 +1,31 @@
<div class="modal" @fade>
<div class="modal-backdrop"></div>
<div class="modal-dialog {{large ? 'modal-lg' : ''}}">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<ng-content select=[title]></ng-content>
</h4>
<button type="button" class="close" (click)="close.emit()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-tabs clearfix" #tabsElement [hidden]="!showTabs">
<ng-content select=[tabs]></ng-content>
</div>
<div class="modal-body">
<ng-content select=[content]></ng-content>
</div>
<div class="modal-footer" [hidden]="!showFooter">
<div class="clearfix" #footerElement>
<ng-content select=[footer]></ng-content>
</div>
</div>
</div>
</div>
</div>

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

@ -0,0 +1,6 @@
@import '_mixins';
@import '_vars';
.modal {
display: block;
}

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

@ -0,0 +1,51 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { fadeAnimation } from './../animations';
@Component({
selector: 'sqx-modal-dialog',
styleUrls: ['./modal-dialog.component.scss'],
templateUrl: './modal-dialog.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.Default
})
export class ModalDialogComponent implements AfterViewInit {
@Input()
public showClose = true;
@Input()
public large = true;
@Output()
public close = new EventEmitter();
@ViewChild('tabsElement')
public tabsElement: ElementRef;
@ViewChild('footerElement')
public footerElement: ElementRef;
public showTabs = false;
public showFooter = false;
constructor(
private readonly changeDetector: ChangeDetectorRef
) {
}
public ngAfterViewInit() {
this.showTabs = this.tabsElement.nativeElement.children.length > 0;
this.showFooter = this.footerElement.nativeElement.children.length > 0;
this.changeDetector.detectChanges();
}
}

21
src/Squidex/app/framework/angular/modals/modal-view.directive.ts

@ -76,7 +76,10 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
} else {
this.renderedView = this.viewContainer.createEmbeddedView(this.templateRef);
}
this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block');
if (this.renderedView.rootNodes[0].style) {
this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block');
}
setTimeout(() => {
this.startListening();
@ -112,15 +115,19 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
if (this.modalView.closeAlways) {
this.modalView.hide();
} else {
const rootNode = this.renderedView.rootNodes[0];
const rootBounds = rootNode.getBoundingClientRect();
try {
const rootNode = this.renderedView.rootNodes[0];
const rootBounds = rootNode.getBoundingClientRect();
if (rootBounds.width > 0 && rootBounds.height > 0) {
const clickedInside = rootNode.contains(event.target);
if (rootBounds.width > 0 && rootBounds.height > 0) {
const clickedInside = rootNode.contains(event.target);
if (!clickedInside && this.modalView) {
this.modalView.hide();
if (!clickedInside && this.modalView) {
this.modalView.hide();
}
}
} catch (ex) {
return;
}
}
});

12
src/Squidex/app/framework/angular/pager.component.html

@ -0,0 +1,12 @@
<div class="grid-footer clearfix" *ngIf="pager && pager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!pager.canGoPrev" (click)="prev.emit()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!pager.canGoNext" (click)="next.emit()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>

2
src/Squidex/app/framework/angular/pager.component.scss

@ -0,0 +1,2 @@
@import '_mixins';
@import '_vars';

26
src/Squidex/app/framework/angular/pager.component.ts

@ -0,0 +1,26 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Pager } from './../internal';
@Component({
selector: 'sqx-pager',
styleUrls: ['./pager.component.scss'],
templateUrl: './pager.component.html'
})
export class PagerComponent {
@Input()
public pager: Pager;
@Output()
public next = new EventEmitter();
@Output()
public prev = new EventEmitter();
}

6
src/Squidex/app/framework/angular/panel.component.html

@ -2,7 +2,7 @@
<div class="panel panel-{{theme}}" [@slideRight]>
<ng-content select=[inner] *ngIf="isFullSize"></ng-content>
<div class="panel-header" *ngIf="!isFullSize">
<div class="panel-header" [class.large]="showSecondHeader" *ngIf="!isFullSize">
<div class="panel-title-row">
<div class="float-right">
<ng-content select=[menu]></ng-content>
@ -16,6 +16,10 @@
<a class="panel-close" sqxParentLink isLazyLoaded="true" *ngIf="showClose">
<i class="icon-close"></i>
</a>
<div class="panel-header-row" *ngIf="showSecondHeader">
<ng-content select=[secondHeader]></ng-content>
</div>
</div>
<div class="panel-main">

9
src/Squidex/app/framework/angular/panel.component.scss

@ -1,2 +1,9 @@
@import '_mixins';
@import '_vars';
@import '_vars';
.panel-header {
&.large {
min-height: 8rem;
max-height: 8rem;
}
}

3
src/Squidex/app/framework/angular/panel.component.ts

@ -37,6 +37,9 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
@Input()
public showScrollbar = false;
@Input()
public showSecondHeader = false;
@Input()
public showSidebar = false;

0
src/Squidex/app/framework/angular/parent-link.directive.ts → src/Squidex/app/framework/angular/routers/parent-link.directive.ts

8
src/Squidex/app/framework/declarations.ts

@ -28,6 +28,7 @@ export * from './angular/forms/validators';
export * from './angular/http/http-extensions-impl';
export * from './angular/modals/dialog-renderer.component';
export * from './angular/modals/modal-dialog.component';
export * from './angular/modals/modal-target.directive';
export * from './angular/modals/modal-view.directive';
export * from './angular/modals/onboarding-tooltip.component';
@ -41,12 +42,13 @@ export * from './angular/pipes/name.pipe';
export * from './angular/pipes/numbers.pipes';
export * from './angular/routers/can-deactivate.guard';
export * from './angular/routers/parent-link.directive';
export * from './angular/ignore-scrollbar.directive';
export * from './angular/image-source.directive';
export * from './angular/panel.component';
export * from './angular/panel-container.directive';
export * from './angular/parent-link.directive';
export * from './angular/pager.component';
export * from './angular/popup-link.directive';
export * from './angular/scroll-active.directive';
export * from './angular/shortcut.component';
@ -55,4 +57,6 @@ export * from './angular/template-wrapper.directive';
export * from './angular/title.component';
export * from './angular/user-report.component';
export * from './internal';
export * from './internal';
export * from './state';

6
src/Squidex/app/framework/module.ts

@ -41,12 +41,14 @@ import {
LocalStoreService,
LowerCaseInputDirective,
MessageBus,
ModalDialogComponent,
ModalTargetDirective,
ModalViewDirective,
MoneyPipe,
MonthPipe,
OnboardingService,
OnboardingTooltipComponent,
PagerComponent,
PanelContainerDirective,
PanelComponent,
ParentLinkDirective,
@ -104,11 +106,13 @@ import {
KeysPipe,
KNumberPipe,
LowerCaseInputDirective,
ModalDialogComponent,
ModalTargetDirective,
ModalViewDirective,
MoneyPipe,
MonthPipe,
OnboardingTooltipComponent,
PagerComponent,
PanelContainerDirective,
PanelComponent,
ParentLinkDirective,
@ -158,11 +162,13 @@ import {
KeysPipe,
KNumberPipe,
LowerCaseInputDirective,
ModalDialogComponent,
ModalTargetDirective,
ModalViewDirective,
MoneyPipe,
MonthPipe,
OnboardingTooltipComponent,
PagerComponent,
PanelContainerDirective,
PanelComponent,
ParentLinkDirective,

94
src/Squidex/app/framework/state.ts

@ -0,0 +1,94 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AbstractControl } from '@angular/forms';
import { BehaviorSubject, Observable } from 'rxjs';
import { ErrorDto } from './utils/error';
export interface FormState {
submitted: boolean;
error?: string;
}
export class Form<T extends AbstractControl> {
private readonly state = new State<FormState>({ submitted: false });
public submitted =
this.state.changes.map(s => s.submitted);
public error =
this.state.changes.map(s => s.error);
constructor(
public readonly form: T
) {
}
public load(value: any) {
this.state.next({ submitted: false, error: null });
this.form.reset(value);
}
public submit(): any | null {
this.state.next({ submitted: true });
if (this.form.valid) {
this.form.disable();
return this.form.value;
} else {
return null;
}
}
public submitCompleted() {
this.state.next({ submitted: false, error: null });
this.form.enable();
}
public submitFailed(error?: string | ErrorDto) {
this.state.next({ submitted: false, error: this.getError(error) });
this.form.enable();
}
private getError(error?: string | ErrorDto) {
if (error instanceof ErrorDto) {
return error.displayMessage;
} else {
return error;
}
}
}
export class State<T extends {}> {
private readonly state: BehaviorSubject<T>;
public get changes(): Observable<T> {
return this.state;
}
public get snapshot() {
return this.state.value;
}
constructor(state: T) {
this.state = new BehaviorSubject(state);
}
public next(update: ((v: T) => T) | object) {
if (update instanceof Function) {
this.state.next(update(this.state.value));
} else {
this.state.next(Object.assign({}, this.snapshot, update));
}
}
}
Loading…
Cancel
Save