mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
385 changed files with 19878 additions and 5664 deletions
@ -1,9 +1,13 @@ |
|||
<main> |
|||
<router-outlet (activate)="isLoaded = true"> |
|||
<div class="loading" *ngIf="!isLoaded"> |
|||
<img src="/images/loader.gif" /> |
|||
<sqx-root-view> |
|||
<sqx-dialog-renderer> |
|||
<router-outlet (activate)="isLoaded = true"> |
|||
<div class="loading" *ngIf="!isLoaded"> |
|||
<img src="/images/loader.gif" /> |
|||
|
|||
<div>Loading Squidex</div> |
|||
</div> |
|||
</router-outlet> |
|||
<div>Loading Squidex</div> |
|||
</div> |
|||
</router-outlet> |
|||
</sqx-dialog-renderer> |
|||
</sqx-root-view> |
|||
</main> |
|||
|
|||
@ -0,0 +1,37 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Observable } from 'rxjs'; |
|||
import { IMock, Mock, Times } from 'typemoq'; |
|||
|
|||
import { UsersState } from './../state/users.state'; |
|||
import { UnsetUserGuard } from './unset-user.guard'; |
|||
|
|||
describe('UnsetUserGuard', () => { |
|||
let usersState: IMock<UsersState>; |
|||
let userGuard: UnsetUserGuard; |
|||
|
|||
beforeEach(() => { |
|||
usersState = Mock.ofType<UsersState>(); |
|||
userGuard = new UnsetUserGuard(usersState.object); |
|||
}); |
|||
|
|||
it('should unset user', () => { |
|||
usersState.setup(x => x.selectUser(null)) |
|||
.returns(() => Observable.of(null)); |
|||
|
|||
let result: boolean; |
|||
|
|||
userGuard.canActivate().subscribe(x => { |
|||
result = x; |
|||
}).unsubscribe(); |
|||
|
|||
expect(result!).toBeTruthy(); |
|||
|
|||
usersState.verify(x => x.selectUser(null), Times.once()); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,24 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { CanActivate } from '@angular/router'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import { UsersState } from './../state/users.state'; |
|||
|
|||
@Injectable() |
|||
export class UnsetUserGuard implements CanActivate { |
|||
constructor( |
|||
private readonly usersState: UsersState |
|||
) { |
|||
} |
|||
|
|||
public canActivate(): Observable<boolean> { |
|||
return this.usersState.selectUser(null).map(u => u === null); |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Router } from '@angular/router'; |
|||
import { Observable } from 'rxjs'; |
|||
import { IMock, Mock, Times } from 'typemoq'; |
|||
|
|||
import { UserDto } from './../services/users.service'; |
|||
import { UsersState } from './../state/users.state'; |
|||
import { UserMustExistGuard } from './user-must-exist.guard'; |
|||
|
|||
describe('UserMustExistGuard', () => { |
|||
const route: any = { |
|||
params: { |
|||
userId: '123' |
|||
} |
|||
}; |
|||
|
|||
let usersState: IMock<UsersState>; |
|||
let router: IMock<Router>; |
|||
let userGuard: UserMustExistGuard; |
|||
|
|||
beforeEach(() => { |
|||
router = Mock.ofType<Router>(); |
|||
usersState = Mock.ofType<UsersState>(); |
|||
userGuard = new UserMustExistGuard(usersState.object, router.object); |
|||
}); |
|||
|
|||
it('should load user and return true when found', () => { |
|||
usersState.setup(x => x.selectUser('123')) |
|||
.returns(() => Observable.of(<UserDto>{})); |
|||
|
|||
let result: boolean; |
|||
|
|||
userGuard.canActivate(route).subscribe(x => { |
|||
result = x; |
|||
}).unsubscribe(); |
|||
|
|||
expect(result!).toBeTruthy(); |
|||
|
|||
usersState.verify(x => x.selectUser('123'), Times.once()); |
|||
}); |
|||
|
|||
it('should load user and return false when not found', () => { |
|||
usersState.setup(x => x.selectUser('123')) |
|||
.returns(() => Observable.of(null)); |
|||
|
|||
let result: boolean; |
|||
|
|||
userGuard.canActivate(route).subscribe(x => { |
|||
result = x; |
|||
}).unsubscribe(); |
|||
|
|||
expect(result!).toBeFalsy(); |
|||
|
|||
router.verify(x => x.navigate(['/404']), Times.once()); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,38 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import { allParams } from '@app/framework'; |
|||
|
|||
import { UsersState } from './../state/users.state'; |
|||
|
|||
@Injectable() |
|||
export class UserMustExistGuard implements CanActivate { |
|||
constructor( |
|||
private readonly usersState: UsersState, |
|||
private readonly router: Router |
|||
) { |
|||
} |
|||
|
|||
public canActivate(route: ActivatedRouteSnapshot): Observable<boolean> { |
|||
const userId = allParams(route)['userId']; |
|||
|
|||
const result = |
|||
this.usersState.selectUser(userId) |
|||
.do(dto => { |
|||
if (!dto) { |
|||
this.router.navigate(['/404']); |
|||
} |
|||
}) |
|||
.map(u => u !== null); |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -1,89 +1,72 @@ |
|||
<sqx-title message="Event Consumers"></sqx-title> |
|||
|
|||
<sqx-panel theme="light" desiredWidth="50rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Event Consumers (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
<ng-container title> |
|||
Consumers |
|||
</ng-container> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
</div> |
|||
<ng-container menu> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Event Consumers (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<h3 class="panel-title">Event Consumers</h3> |
|||
</div> |
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
</ng-container> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
<ng-container content> |
|||
<table class="table table-items table-fixed"> |
|||
<thead> |
|||
<tr> |
|||
<th class="cell-auto"> |
|||
Name |
|||
</th> |
|||
<th class="cell-auto-right"> |
|||
Position |
|||
</th> |
|||
<th class="cell-actions-lg"> |
|||
Actions |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-scroll"> |
|||
<table class="table table-items table-fixed"> |
|||
<thead> |
|||
<tr> |
|||
<th class="cell-auto"> |
|||
Name |
|||
</th> |
|||
<th class="cell-auto-right"> |
|||
Position |
|||
</th> |
|||
<th class="cell-actions-lg"> |
|||
Actions |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
|
|||
<tbody> |
|||
<ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers"> |
|||
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0"> |
|||
<td class="auto-auto"> |
|||
<span class="truncate"> |
|||
<i class="faulted-icon icon icon-bug" (click)="showError(eventConsumer)" [class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"></i> |
|||
<tbody> |
|||
<ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers"> |
|||
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0"> |
|||
<td class="auto-auto"> |
|||
<span class="truncate"> |
|||
<i class="faulted-icon icon icon-bug" (click)="showError(eventConsumer)" [class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"></i> |
|||
|
|||
{{eventConsumer.name}} |
|||
</span> |
|||
</td> |
|||
<td class="cell-auto-right"> |
|||
<span>{{eventConsumer.position}}</span> |
|||
</td> |
|||
<td class="cell-actions-lg"> |
|||
<button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer"> |
|||
<i class="icon icon-reset"></i> |
|||
</button> |
|||
<button class="btn btn-link" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer"> |
|||
<i class="icon icon-play"></i> |
|||
</button> |
|||
<button class="btn btn-link" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer"> |
|||
<i class="icon icon-pause"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
{{eventConsumer.name}} |
|||
</span> |
|||
</td> |
|||
<td class="cell-auto-right"> |
|||
<span>{{eventConsumer.position}}</span> |
|||
</td> |
|||
<td class="cell-actions-lg"> |
|||
<button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer"> |
|||
<i class="icon icon-reset"></i> |
|||
</button> |
|||
<button class="btn btn-link" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer"> |
|||
<i class="icon icon-play"></i> |
|||
</button> |
|||
<button class="btn btn-link" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer"> |
|||
<i class="icon icon-pause"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
<div class="modal" *sqxModalView="eventConsumerErrorDialog;onRoot:true" @fade> |
|||
<div class="modal-backdrop"></div> |
|||
<div class="modal-dialog"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h4 class="modal-title">Error</h4> |
|||
|
|||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="eventConsumerErrorDialog.hide()"> |
|||
<span aria-hidden="true">×</span> |
|||
</button> |
|||
</div> |
|||
<sqx-modal-dialog *sqxModalView="eventConsumerErrorDialog;onRoot:true" (close)="eventConsumerErrorDialog.hide()"> |
|||
<ng-container #title> |
|||
Error |
|||
</ng-container> |
|||
|
|||
<div class="modal-body"> |
|||
<textarea readonly class="form-control error-message">{{eventConsumerError}}</textarea> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<ng-container #content> |
|||
<textarea readonly class="form-control error-message">{{eventConsumerError}}</textarea> |
|||
</ng-container> |
|||
</sqx-modal-dialog> |
|||
@ -1,22 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { UserDto } from 'shared'; |
|||
|
|||
export class UserCreated { |
|||
constructor( |
|||
public readonly user: UserDto |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class UserUpdated { |
|||
constructor( |
|||
public readonly user: UserDto |
|||
) { |
|||
} |
|||
} |
|||
@ -1,73 +1,62 @@ |
|||
<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"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button type="submit" class="btn btn-primary" title="CTRL + S"> |
|||
Save |
|||
</button> |
|||
</div> |
|||
|
|||
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> |
|||
|
|||
<h3 class="panel-title" *ngIf="isNewMode"> |
|||
New User |
|||
</h3> |
|||
<h3 class="panel-title" *ngIf="!isNewMode"> |
|||
Edit User |
|||
</h3> |
|||
</div> |
|||
|
|||
<sqx-panel desiredWidth="26rem" isBlank="true"> |
|||
<ng-container title> |
|||
<ng-container *ngIf="usersState.selectedUser | async; else noUser"> |
|||
Edit User |
|||
</ng-container> |
|||
|
|||
<ng-template #noUser> |
|||
New User |
|||
</ng-template> |
|||
</ng-container> |
|||
|
|||
<ng-container menu> |
|||
<button type="submit" class="btn btn-primary" title="CTRL + S"> |
|||
Save |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> |
|||
</ng-container> |
|||
|
|||
<a class="panel-close" sqxParentLink> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
<ng-container content> |
|||
<sqx-form-error [error]="userForm.error | async"></sqx-form-error> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-blank"> |
|||
<div *ngIf="userFormError"> |
|||
<div class="form-alert form-alert-error" [innerHTML]="userFormError"></div> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label for="email">Email</label> |
|||
|
|||
<div class="form-group"> |
|||
<label for="email">Email</label> |
|||
<sqx-control-errors for="email" [submitted]="userForm.submitted | async"></sqx-control-errors> |
|||
|
|||
<sqx-control-errors for="email" [submitted]="userFormSubmitted"></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> |
|||
<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> |
|||
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" /> |
|||
</div> |
|||
|
|||
<div class="form-group form-group-password" [class.hidden]="user.id === ctx.userId"> |
|||
<div class="form-group"> |
|||
<label for="password">Password</label> |
|||
<div class="form-group form-group-password" [class.hidden]="usersState.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> |
|||
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="false" /> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label for="password">Confirm Password</label> |
|||
<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> |
|||
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="false" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
</form> |
|||
</form> |
|||
@ -1,107 +1,90 @@ |
|||
<sqx-title message="User Management"></sqx-title> |
|||
|
|||
<sqx-panel desiredWidth="50rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
<sqx-panel desiredWidth="50rem" contentClass="grid"> |
|||
<ng-container title> |
|||
Users |
|||
</ng-container> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> |
|||
<ng-container menu> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<form class="form-inline" (ngSubmit)="search()"> |
|||
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> |
|||
</form> |
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut> |
|||
|
|||
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + M)"> |
|||
<i class="icon-plus"></i> New |
|||
</button> |
|||
</div> |
|||
|
|||
<h3 class="panel-title">Users</h3> |
|||
</div> |
|||
<form class="form-inline" (ngSubmit)="search()"> |
|||
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> |
|||
</form> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)"> |
|||
<i class="icon-plus"></i> New |
|||
</button> |
|||
</ng-container> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content grid"> |
|||
<div class="grid-header"> |
|||
<ng-container content> |
|||
<div class="grid-header"> |
|||
<table class="table table-items table-fixed"> |
|||
<thead> |
|||
<tr> |
|||
<th class="cell-user"> |
|||
|
|||
</th> |
|||
<th class="cell-auto"> |
|||
Name |
|||
</th> |
|||
<th class="cell-auto"> |
|||
Email |
|||
</th> |
|||
<th class="cell-actions"> |
|||
Actions |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
</table> |
|||
</div> |
|||
|
|||
<div class="grid-content"> |
|||
<div sqxIgnoreScrollbar> |
|||
<table class="table table-items table-fixed"> |
|||
<thead> |
|||
<tr> |
|||
<th class="cell-user"> |
|||
|
|||
</th> |
|||
<th class="cell-auto"> |
|||
Name |
|||
</th> |
|||
<th class="cell-auto"> |
|||
Email |
|||
</th> |
|||
<th class="cell-actions"> |
|||
Actions |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
</table> |
|||
</div> |
|||
|
|||
<div class="grid-content"> |
|||
<div sqxIgnoreScrollbar> |
|||
<table class="table table-items table-fixed"> |
|||
<tbody> |
|||
<ng-template ngFor let-user [ngForOf]="usersItems"> |
|||
<tr [routerLink]="user.id" routerLinkActive="active"> |
|||
<td class="cell-user"> |
|||
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" /> |
|||
</td> |
|||
<td class="cell-auto"> |
|||
<span class="user-name table-cell">{{user.displayName}}</span> |
|||
</td> |
|||
<td class="cell-auto"> |
|||
<span class="user-email table-cell">{{user.email}}</span> |
|||
</td> |
|||
<td class="cell-actions"> |
|||
<span *ngIf="user.id !== ctx.userId"> |
|||
<button class="btn btn-link" (click)="lock(user); $event.stopPropagation();" *ngIf="!user.isLocked" title="Lock User"> |
|||
<i class="icon icon-unlocked"></i> |
|||
</button> |
|||
<button class="btn btn-link" (click)="unlock(user); $event.stopPropagation();" *ngIf="user.isLocked" title="Unlock User"> |
|||
<i class="icon icon-lock"></i> |
|||
</button> |
|||
</span> |
|||
<button *ngIf="user.id === ctx.userId" class="btn btn-link invisible"> |
|||
|
|||
<tbody> |
|||
<ng-template ngFor let-user [ngForOf]="usersState.users | async" [ngForTrackBy]="trackByUser"> |
|||
<tr [routerLink]="user.id" routerLinkActive="active"> |
|||
<td class="cell-user"> |
|||
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" /> |
|||
</td> |
|||
<td class="cell-auto"> |
|||
<span class="user-name table-cell">{{user.displayName}}</span> |
|||
</td> |
|||
<td class="cell-auto"> |
|||
<span class="user-email table-cell">{{user.email}}</span> |
|||
</td> |
|||
<td class="cell-actions"> |
|||
<ng-container *ngIf="user.id !== authState.user?.id"> |
|||
<button class="btn btn-link" (click)="lock(user); $event.stopPropagation();" *ngIf="!user.isLocked" title="Lock User"> |
|||
<i class="icon icon-unlocked"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
<button class="btn btn-link" (click)="unlock(user); $event.stopPropagation();" *ngIf="user.isLocked" title="Unlock User"> |
|||
<i class="icon icon-lock"></i> |
|||
</button> |
|||
</ng-container> |
|||
<button *ngIf="user.id === authState.user?.id" class="btn btn-link invisible"> |
|||
|
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="grid-footer clearfix" *ngIf="usersPager.numberOfItems > 0"> |
|||
<div class="float-right pagination"> |
|||
<span class="pagination-text">{{usersPager.itemFirst}}-{{usersPager.itemLast}} of {{usersPager.numberOfItems}}</span> |
|||
|
|||
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!usersPager.canGoPrev" (click)="goPrev()"> |
|||
<i class="icon-angle-left"></i> |
|||
</button> |
|||
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!usersPager.canGoNext" (click)="goNext()"> |
|||
<i class="icon-angle-right"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="grid-footer"> |
|||
<sqx-pager [pager]="usersState.usersPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -0,0 +1,202 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
import { inject, TestBed } from '@angular/core/testing'; |
|||
|
|||
import { ApiUrlConfig } from '@app/framework'; |
|||
|
|||
import { |
|||
CreateUserDto, |
|||
UpdateUserDto, |
|||
UserDto, |
|||
UsersService, |
|||
UsersDto |
|||
} from './users.service'; |
|||
|
|||
describe('UsersService', () => { |
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [ |
|||
HttpClientTestingModule |
|||
], |
|||
providers: [ |
|||
UsersService, |
|||
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') } |
|||
] |
|||
}); |
|||
}); |
|||
|
|||
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { |
|||
httpMock.verify(); |
|||
})); |
|||
|
|||
it('should make get request to get many users', |
|||
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { |
|||
|
|||
let users: UsersDto | null = null; |
|||
|
|||
userManagementService.getUsers(20, 30).subscribe(result => { |
|||
users = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query='); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({ |
|||
total: 100, |
|||
items: [ |
|||
{ |
|||
id: '123', |
|||
email: 'mail1@domain.com', |
|||
displayName: 'User1', |
|||
isLocked: true |
|||
}, |
|||
{ |
|||
id: '456', |
|||
email: 'mail2@domain.com', |
|||
displayName: 'User2', |
|||
isLocked: true |
|||
} |
|||
] |
|||
}); |
|||
|
|||
expect(users).toEqual( |
|||
new UsersDto(100, [ |
|||
new UserDto('123', 'mail1@domain.com', 'User1', true), |
|||
new UserDto('456', 'mail2@domain.com', 'User2', true) |
|||
])); |
|||
})); |
|||
|
|||
it('should make get request with query to get many users', |
|||
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { |
|||
|
|||
let users: UsersDto | null = null; |
|||
|
|||
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => { |
|||
users = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=my-query'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({ |
|||
total: 100, |
|||
items: [ |
|||
{ |
|||
id: '123', |
|||
email: 'mail1@domain.com', |
|||
displayName: 'User1', |
|||
isLocked: true |
|||
}, |
|||
{ |
|||
id: '456', |
|||
email: 'mail2@domain.com', |
|||
displayName: 'User2', |
|||
isLocked: true |
|||
} |
|||
] |
|||
}); |
|||
|
|||
expect(users).toEqual( |
|||
new UsersDto(100, [ |
|||
new UserDto('123', 'mail1@domain.com', 'User1', true), |
|||
new UserDto('456', 'mail2@domain.com', 'User2', true) |
|||
])); |
|||
})); |
|||
|
|||
it('should make get request to get single user', |
|||
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { |
|||
|
|||
let user: UserDto | null = null; |
|||
|
|||
userManagementService.getUser('123').subscribe(result => { |
|||
user = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/user-management/123'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({ |
|||
id: '123', |
|||
email: 'mail1@domain.com', |
|||
displayName: 'User1', |
|||
pictureUrl: 'path/to/image1', |
|||
isLocked: true |
|||
}); |
|||
|
|||
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', true)); |
|||
})); |
|||
|
|||
it('should make post request to create user', |
|||
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { |
|||
|
|||
const dto = new CreateUserDto('mail@squidex.io', 'Squidex User', 'password'); |
|||
|
|||
let user: UserDto | null = null; |
|||
|
|||
userManagementService.postUser(dto).subscribe(result => { |
|||
user = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/user-management'); |
|||
|
|||
expect(req.request.method).toEqual('POST'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({ id: '123', pictureUrl: 'path/to/image1' }); |
|||
|
|||
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, false)); |
|||
})); |
|||
|
|||
it('should make put request to update user', |
|||
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { |
|||
|
|||
const dto = new UpdateUserDto('mail@squidex.io', 'Squidex User', 'password'); |
|||
|
|||
userManagementService.putUser('123', dto).subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/user-management/123'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
|
|||
it('should make put request to lock user', |
|||
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { |
|||
|
|||
userManagementService.lockUser('123').subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/user-management/123/lock'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
|
|||
it('should make put request to unlock user', |
|||
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { |
|||
|
|||
userManagementService.unlockUser('123').subscribe(); |
|||
|
|||
const req = httpMock.expectOne('http://service/p/api/user-management/123/unlock'); |
|||
|
|||
expect(req.request.method).toEqual('PUT'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush({}); |
|||
})); |
|||
}); |
|||
@ -0,0 +1,134 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import '@app/framework/angular/http/http-extensions'; |
|||
|
|||
import { ApiUrlConfig, HTTP } from '@app/framework'; |
|||
|
|||
export class UsersDto { |
|||
constructor( |
|||
public readonly total: number, |
|||
public readonly items: UserDto[] |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class UserDto { |
|||
constructor( |
|||
public readonly id: string, |
|||
public readonly email: string, |
|||
public readonly displayName: string, |
|||
public readonly isLocked: boolean |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class CreateUserDto { |
|||
constructor( |
|||
public readonly email: string, |
|||
public readonly displayName: string, |
|||
public readonly password: string |
|||
) { |
|||
} |
|||
} |
|||
|
|||
export class UpdateUserDto { |
|||
constructor( |
|||
public readonly email: string, |
|||
public readonly displayName: string, |
|||
public readonly password?: string |
|||
) { |
|||
} |
|||
} |
|||
|
|||
@Injectable() |
|||
export class UsersService { |
|||
constructor( |
|||
private readonly http: HttpClient, |
|||
private readonly apiUrl: ApiUrlConfig |
|||
) { |
|||
} |
|||
|
|||
public getUsers(take: number, skip: number, query?: string): Observable<UsersDto> { |
|||
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`); |
|||
|
|||
return HTTP.getVersioned<any>(this.http, url) |
|||
.map(response => { |
|||
const body = response.payload.body; |
|||
|
|||
const items: any[] = body.items; |
|||
|
|||
const users = items.map(item => { |
|||
return new UserDto( |
|||
item.id, |
|||
item.email, |
|||
item.displayName, |
|||
item.isLocked); |
|||
}); |
|||
|
|||
return new UsersDto(body.total, users); |
|||
}) |
|||
.pretifyError('Failed to load users. Please reload.'); |
|||
} |
|||
|
|||
public getUser(id: string): Observable<UserDto> { |
|||
const url = this.apiUrl.buildUrl(`api/user-management/${id}`); |
|||
|
|||
return HTTP.getVersioned<any>(this.http, url) |
|||
.map(response => { |
|||
const body = response.payload.body; |
|||
|
|||
return new UserDto( |
|||
body.id, |
|||
body.email, |
|||
body.displayName, |
|||
body.isLocked); |
|||
}) |
|||
.pretifyError('Failed to load user. Please reload.'); |
|||
} |
|||
|
|||
public postUser(dto: CreateUserDto): Observable<UserDto> { |
|||
const url = this.apiUrl.buildUrl('api/user-management'); |
|||
|
|||
return HTTP.postVersioned<any>(this.http, url, dto) |
|||
.map(response => { |
|||
const body = response.payload.body; |
|||
|
|||
return new UserDto( |
|||
body.id, |
|||
dto.email, |
|||
dto.displayName, |
|||
false); |
|||
}) |
|||
.pretifyError('Failed to create user. Please reload.'); |
|||
} |
|||
|
|||
public putUser(id: string, dto: UpdateUserDto): Observable<any> { |
|||
const url = this.apiUrl.buildUrl(`api/user-management/${id}`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, dto) |
|||
.pretifyError('Failed to update user. Please reload.'); |
|||
} |
|||
|
|||
public lockUser(id: string): Observable<any> { |
|||
const url = this.apiUrl.buildUrl(`api/user-management/${id}/lock`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, {}) |
|||
.pretifyError('Failed to load users. Please retry.'); |
|||
} |
|||
|
|||
public unlockUser(id: string): Observable<any> { |
|||
const url = this.apiUrl.buildUrl(`api/user-management/${id}/unlock`); |
|||
|
|||
return HTTP.putVersioned(this.http, url, {}) |
|||
.pretifyError('Failed to load users. Please retry.'); |
|||
} |
|||
} |
|||
@ -0,0 +1,211 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Observable } from 'rxjs'; |
|||
import { IMock, It, Mock, Times } from 'typemoq'; |
|||
|
|||
import { AuthService, DialogService } from '@app/shared'; |
|||
|
|||
import { UsersState } from './users.state'; |
|||
|
|||
import { |
|||
CreateUserDto, |
|||
UserDto, |
|||
UpdateUserDto, |
|||
UsersDto, |
|||
UsersService |
|||
} from './../services/users.service'; |
|||
|
|||
describe('UsersState', () => { |
|||
const oldUsers = [ |
|||
new UserDto('id1', 'mail1@mail.de', 'name1', false), |
|||
new UserDto('id2', 'mail2@mail.de', 'name2', true) |
|||
]; |
|||
|
|||
const newUser = new UserDto('id3', 'mail3@mail.de', 'name3', false); |
|||
|
|||
let authService: IMock<AuthService>; |
|||
let dialogs: IMock<DialogService>; |
|||
let usersService: IMock<UsersService>; |
|||
let usersState: UsersState; |
|||
|
|||
beforeEach(() => { |
|||
authService = Mock.ofType<AuthService>(); |
|||
|
|||
authService.setup(x => x.user) |
|||
.returns(() => <any>{ id: 'id2' }); |
|||
|
|||
dialogs = Mock.ofType<DialogService>(); |
|||
|
|||
usersService = Mock.ofType<UsersService>(); |
|||
|
|||
usersService.setup(x => x.getUsers(10, 0, undefined)) |
|||
.returns(() => Observable.of(new UsersDto(200, oldUsers))); |
|||
|
|||
usersState = new UsersState(authService.object, dialogs.object, usersService.object); |
|||
usersState.loadUsers().subscribe(); |
|||
}); |
|||
|
|||
it('should load users', () => { |
|||
expect(usersState.snapshot.users.values).toEqual(oldUsers); |
|||
expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200); |
|||
|
|||
usersService.verifyAll(); |
|||
}); |
|||
|
|||
it('should replace selected user when reloading', () => { |
|||
usersState.selectUser('id1').subscribe(); |
|||
|
|||
const newUsers = [ |
|||
new UserDto('id1', 'mail1@mail.de_new', 'name1_new', false), |
|||
new UserDto('id2', 'mail2@mail.de_new', 'name2_new', true) |
|||
]; |
|||
|
|||
usersService.setup(x => x.getUsers(10, 0, undefined)) |
|||
.returns(() => Observable.of(new UsersDto(200, newUsers))); |
|||
|
|||
usersState.loadUsers().subscribe(); |
|||
|
|||
expect(usersState.snapshot.selectedUser).toBe(newUsers[0]); |
|||
}); |
|||
|
|||
it('should raise notification on load when notify is true', () => { |
|||
usersState.loadUsers(true).subscribe(); |
|||
|
|||
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); |
|||
}); |
|||
|
|||
it('should mark as current user when selected user equals to profile', () => { |
|||
usersState.selectUser('id2').subscribe(); |
|||
|
|||
expect(usersState.snapshot.isCurrentUser).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should not load user when already loaded', () => { |
|||
let selectedUser: UserDto; |
|||
|
|||
usersState.selectUser('id1').subscribe(x => { |
|||
selectedUser = x!; |
|||
}); |
|||
|
|||
expect(selectedUser!).toEqual(oldUsers[0]); |
|||
expect(usersState.snapshot.selectedUser).toBe(oldUsers[0]); |
|||
|
|||
usersService.verify(x => x.getUser(It.isAnyString()), Times.never()); |
|||
}); |
|||
|
|||
it('should load user when not loaded', () => { |
|||
usersService.setup(x => x.getUser('id3')) |
|||
.returns(() => Observable.of(newUser)); |
|||
|
|||
let selectedUser: UserDto; |
|||
|
|||
usersState.selectUser('id3').subscribe(x => { |
|||
selectedUser = x!; |
|||
}); |
|||
|
|||
expect(selectedUser!).toEqual(newUser); |
|||
expect(usersState.snapshot.selectedUser).toBe(newUser); |
|||
}); |
|||
|
|||
it('should return null when unselecting user', () => { |
|||
let selectedUser: UserDto; |
|||
|
|||
usersState.selectUser(null).subscribe(x => { |
|||
selectedUser = x!; |
|||
}); |
|||
|
|||
expect(selectedUser!).toBeNull(); |
|||
expect(usersState.snapshot.selectedUser).toBeNull(); |
|||
|
|||
usersService.verify(x => x.getUser(It.isAnyString()), Times.never()); |
|||
}); |
|||
|
|||
it('should return null when user to select is not found', () => { |
|||
usersService.setup(x => x.getUser('unknown')) |
|||
.returns(() => Observable.throw({})); |
|||
|
|||
let selectedUser: UserDto; |
|||
|
|||
usersState.selectUser('unknown').subscribe(x => { |
|||
selectedUser = x!; |
|||
}).unsubscribe(); |
|||
|
|||
expect(selectedUser!).toBeNull(); |
|||
expect(usersState.snapshot.selectedUser).toBeNull(); |
|||
}); |
|||
|
|||
it('should mark user as locked', () => { |
|||
usersService.setup(x => x.lockUser('id1')) |
|||
.returns(() => Observable.of({})); |
|||
|
|||
usersState.selectUser('id1').subscribe(); |
|||
usersState.lockUser(oldUsers[0]).subscribe(); |
|||
|
|||
expect(usersState.snapshot.users.at(0).isLocked).toBeTruthy(); |
|||
expect(usersState.snapshot.selectedUser).toBe(usersState.snapshot.users.at(0)); |
|||
}); |
|||
|
|||
it('should unmark user as locked', () => { |
|||
usersService.setup(x => x.unlockUser('id2')) |
|||
.returns(() => Observable.of({})); |
|||
|
|||
usersState.selectUser('id2').subscribe(); |
|||
usersState.unlockUser(oldUsers[1]).subscribe(); |
|||
|
|||
expect(usersState.snapshot.users.at(1).isLocked).toBeFalsy(); |
|||
expect(usersState.snapshot.selectedUser).toBe(usersState.snapshot.users.at(1)); |
|||
}); |
|||
|
|||
it('should update user on update', () => { |
|||
const request = new UpdateUserDto('new@mail.com', 'New'); |
|||
|
|||
usersService.setup(x => x.putUser('id1', request)) |
|||
.returns(() => Observable.of({})); |
|||
|
|||
usersState.selectUser('id1').subscribe(); |
|||
usersState.updateUser(oldUsers[0], request).subscribe(); |
|||
|
|||
expect(usersState.snapshot.users.at(0).email).toEqual('new@mail.com'); |
|||
expect(usersState.snapshot.users.at(0).displayName).toEqual('New'); |
|||
expect(usersState.snapshot.selectedUser).toBe(usersState.snapshot.users.at(0)); |
|||
}); |
|||
|
|||
it('should add user to state when created', () => { |
|||
const request = new CreateUserDto(newUser.email, newUser.displayName, 'password'); |
|||
|
|||
usersService.setup(x => x.postUser(request)) |
|||
.returns(() => Observable.of(newUser)); |
|||
|
|||
usersState.createUser(request).subscribe(); |
|||
|
|||
expect(usersState.snapshot.users.at(0)).toBe(newUser); |
|||
expect(usersState.snapshot.usersPager.numberOfItems).toBe(201); |
|||
}); |
|||
|
|||
it('should load next page and prev page when paging', () => { |
|||
usersService.setup(x => x.getUsers(10, 10, undefined)) |
|||
.returns(() => Observable.of(new UsersDto(200, []))); |
|||
|
|||
usersState.goNext().subscribe(); |
|||
usersState.goPrev().subscribe(); |
|||
|
|||
usersService.verify(x => x.getUsers(10, 10, undefined), Times.once()); |
|||
usersService.verify(x => x.getUsers(10, 0, undefined), Times.exactly(2)); |
|||
}); |
|||
|
|||
it('should load with query when searching', () => { |
|||
usersService.setup(x => x.getUsers(10, 0, 'my-query')) |
|||
.returns(() => Observable.of(new UsersDto(0, []))); |
|||
|
|||
usersState.search('my-query').subscribe(); |
|||
|
|||
expect(usersState.snapshot.usersQuery).toEqual('my-query'); |
|||
|
|||
usersService.verify(x => x.getUsers(10, 0, 'my-query'), Times.once()); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,229 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { FormBuilder, Validators, FormGroup } from '@angular/forms'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import '@app/framework/utils/rxjs-extensions'; |
|||
|
|||
import { |
|||
AuthService, |
|||
DialogService, |
|||
ImmutableArray, |
|||
Pager, |
|||
Form, |
|||
State, |
|||
ValidatorsEx |
|||
} from '@app/shared'; |
|||
|
|||
import { |
|||
CreateUserDto, |
|||
UserDto, |
|||
UsersService, |
|||
UpdateUserDto |
|||
} from './../services/users.service'; |
|||
|
|||
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); |
|||
} |
|||
} |
|||
|
|||
interface Snapshot { |
|||
isCurrentUser?: boolean; |
|||
|
|||
users: ImmutableArray<UserDto>; |
|||
usersPager: Pager; |
|||
usersQuery?: string; |
|||
|
|||
selectedUser?: UserDto; |
|||
} |
|||
|
|||
@Injectable() |
|||
export class UsersState extends State<Snapshot> { |
|||
public users = |
|||
this.changes.map(x => x.users) |
|||
.distinctUntilChanged(); |
|||
|
|||
public usersPager = |
|||
this.changes.map(x => x.usersPager) |
|||
.distinctUntilChanged(); |
|||
|
|||
public selectedUser = |
|||
this.changes.map(x => x.selectedUser) |
|||
.distinctUntilChanged(); |
|||
|
|||
public isCurrentUser = |
|||
this.changes.map(x => x.isCurrentUser) |
|||
.distinctUntilChanged(); |
|||
|
|||
constructor( |
|||
private readonly authState: AuthService, |
|||
private readonly dialogs: DialogService, |
|||
private readonly usersService: UsersService |
|||
) { |
|||
super({ users: ImmutableArray.empty(), usersPager: new Pager(0) }); |
|||
} |
|||
|
|||
public selectUser(id: string | null): Observable<UserDto | null> { |
|||
return this.loadUser(id) |
|||
.do(selectedUser => { |
|||
const isCurrentUser = id === this.authState.user!.id; |
|||
|
|||
this.next(s => ({ ...s, selectedUser, isCurrentUser })); |
|||
}); |
|||
} |
|||
|
|||
private loadUser(id: string | null) { |
|||
return !id ? |
|||
Observable.of(null) : |
|||
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(notify = false): Observable<any> { |
|||
return this.usersService.getUsers(this.snapshot.usersPager.pageSize, this.snapshot.usersPager.skip, this.snapshot.usersQuery) |
|||
.do(dtos => { |
|||
if (notify) { |
|||
this.dialogs.notifyInfo('Users reloaded.'); |
|||
} |
|||
|
|||
this.next(s => { |
|||
const users = ImmutableArray.of(dtos.items); |
|||
const usersPager = s.usersPager.setCount(dtos.total); |
|||
|
|||
let selectedUser = s.selectedUser; |
|||
|
|||
if (selectedUser) { |
|||
const selectedFromResult = dtos.items.find(x => x.id === selectedUser!.id); |
|||
|
|||
if (selectedFromResult) { |
|||
selectedUser = selectedFromResult; |
|||
} |
|||
} |
|||
|
|||
return { ...s, users, usersPager, selectedUser }; |
|||
}); |
|||
}) |
|||
.notify(this.dialogs); |
|||
} |
|||
|
|||
public createUser(request: CreateUserDto): Observable<UserDto> { |
|||
return this.usersService.postUser(request) |
|||
.do(dto => { |
|||
this.next(s => { |
|||
const users = s.users.pushFront(dto); |
|||
const usersPager = s.usersPager.incrementCount(); |
|||
|
|||
return { ...s, users, usersPager }; |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
public updateUser(user: UserDto, request: UpdateUserDto): Observable<any> { |
|||
return this.usersService.putUser(user.id, request) |
|||
.do(() => { |
|||
this.dialogs.notifyInfo('User saved successsfull'); |
|||
|
|||
this.replaceUser(update(user, request)); |
|||
}); |
|||
} |
|||
|
|||
public lockUser(user: UserDto): Observable<any> { |
|||
return this.usersService.lockUser(user.id) |
|||
.do(() => { |
|||
this.replaceUser(setLocked(user, true)); |
|||
}) |
|||
.notify(this.dialogs); |
|||
} |
|||
|
|||
public unlockUser(user: UserDto): Observable<any> { |
|||
return this.usersService.unlockUser(user.id) |
|||
.do(() => { |
|||
this.replaceUser(setLocked(user, false)); |
|||
}) |
|||
.notify(this.dialogs); |
|||
} |
|||
|
|||
public search(query: string): Observable<any> { |
|||
this.next(s => ({ ...s, usersPager: new Pager(0), usersQuery: query })); |
|||
|
|||
return this.loadUsers(); |
|||
} |
|||
|
|||
public goNext(): Observable<any> { |
|||
this.next(s => ({ ...s, usersPager: s.usersPager.goNext() })); |
|||
|
|||
return this.loadUsers(); |
|||
} |
|||
|
|||
public goPrev(): Observable<any> { |
|||
this.next(s => ({ ...s, usersPager: s.usersPager.goPrev() })); |
|||
|
|||
return this.loadUsers(); |
|||
} |
|||
|
|||
private replaceUser(user: UserDto) { |
|||
return this.next(s => { |
|||
const users = s.users.replaceBy('id', user); |
|||
const selectedUser = s.selectedUser && s.selectedUser.id === user.id ? user : s.selectedUser; |
|||
|
|||
return { ...s, users, selectedUser }; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
|
|||
const update = (user: UserDto, request: UpdateUserDto) => |
|||
new UserDto(user.id, request.email, request.displayName, user.isLocked); |
|||
|
|||
const setLocked = (user: UserDto, locked: boolean) => |
|||
new UserDto(user.id, user.email, user.displayName, locked); |
|||
@ -1,33 +1,25 @@ |
|||
<sqx-title message="{app} | Settings" parameter1="app" [value1]="ctx.appName"></sqx-title> |
|||
<sqx-title message="{app} | Settings" parameter1="app" [value1]="appsState.appName"></sqx-title> |
|||
|
|||
<sqx-panel theme="dark" desiredWidth="12rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<h3 class="panel-title">API</h3> |
|||
</div> |
|||
<ng-container title> |
|||
API |
|||
</ng-container> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content"> |
|||
<ul class="nav nav-panel nav-dark flex-column"> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> |
|||
GraphQL |
|||
<i class="icon-angle-right"></i> |
|||
</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="/api/content/{{ctx.appName}}/docs" target="_blank"> |
|||
Swagger |
|||
</a> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
<ng-container content> |
|||
<ul class="nav nav-panel nav-dark flex-column"> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> |
|||
GraphQL |
|||
<i class="icon-angle-right"></i> |
|||
</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="/api/content/{{appsState.appName}}/docs" target="_blank"> |
|||
Swagger |
|||
</a> |
|||
</li> |
|||
</ul> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -1,5 +1,5 @@ |
|||
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="ctx.appName"></sqx-title> |
|||
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="appsState.appName"></sqx-title> |
|||
|
|||
<sqx-panel desiredWidth="*"> |
|||
<div #graphiQLContainer></div> |
|||
<sqx-panel desiredWidth="*" isFullSize="true"> |
|||
<div inner #graphiQLContainer></div> |
|||
</sqx-panel> |
|||
@ -1,70 +1,24 @@ |
|||
<sqx-title message="{app} | Assets" parameter1="app" [value1]="ctx.appName"></sqx-title> |
|||
|
|||
<sqx-panel desiredWidth="60rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
|
|||
<form class="form-inline" (ngSubmit)="search()"> |
|||
<input class="form-control" #inputFind [formControl]="assetsFilter" placeholder="Search for assets" /> |
|||
</form> |
|||
</div> |
|||
|
|||
<h3 class="panel-title">Assets</h3> |
|||
</div> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-scroll"> |
|||
<div class="file-drop" (sqxFileDrop)="addFiles($event)"> |
|||
<h3 class="file-drop-header">Drop files here to upload</h3> |
|||
|
|||
<div class="file-drop-or">or</div> |
|||
|
|||
<div class="file-drop-button"> |
|||
<span class="btn btn-success" (click)="fileInput.click()"> |
|||
<span>Select File(s)</span> |
|||
|
|||
<input class="file-drop-button-input" type="file" (change)="addFiles($event.target.files)" #fileInput multiple /> |
|||
</span> |
|||
</div> |
|||
|
|||
<div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<sqx-asset class="col-3" *ngFor="let file of newFiles" [initFile]="file" |
|||
(failed)="onAssetFailed(file)" |
|||
(loaded)="onAssetLoaded(file, $event)"> |
|||
</sqx-asset> |
|||
<sqx-asset class="col-3" *ngFor="let asset of assetsItems" [asset]="asset" |
|||
(deleting)="onAssetDeleting($event)" |
|||
(updated)="onAssetUpdated($event)"> |
|||
</sqx-asset> |
|||
</div> |
|||
|
|||
<div class="clearfix" *ngIf="assetsPager.numberOfItems > 0"> |
|||
<div class="float-right pagination"> |
|||
<span class="pagination-text">{{assetsPager.itemFirst}}-{{assetsPager.itemLast}} of {{assetsPager.numberOfItems}}</span> |
|||
|
|||
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!assetsPager.canGoPrev" (click)="goPrev()"> |
|||
<i class="icon-angle-left"></i> |
|||
</button> |
|||
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!assetsPager.canGoNext" (click)="goNext()"> |
|||
<i class="icon-angle-right"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<sqx-title message="{app} | Assets" parameter1="app" [value1]="appsState.appName"></sqx-title> |
|||
|
|||
<sqx-panel desiredWidth="*"> |
|||
<ng-container title> |
|||
Assets |
|||
</ng-container> |
|||
|
|||
<ng-container menu> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
|
|||
<form class="form-inline" (ngSubmit)="search()"> |
|||
<input class="form-control" #inputFind [formControl]="assetsFilter" placeholder="Search for assets" /> |
|||
</form> |
|||
</ng-container> |
|||
|
|||
<ng-container content> |
|||
<sqx-assets-list [state]="assetsState"></sqx-assets-list> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
@ -1,50 +1,2 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
.file-drop { |
|||
& { |
|||
@include transition(border-color .4s ease); |
|||
border: 2px dashed $color-border; |
|||
background: transparent; |
|||
padding: 1rem; |
|||
text-align: center; |
|||
margin-bottom: 1rem; |
|||
margin-right: 0; |
|||
} |
|||
|
|||
&.drag { |
|||
border-color: darken($color-border, 10%); |
|||
border-style: dashed; |
|||
cursor: copy; |
|||
} |
|||
|
|||
&-button-input { |
|||
@include hidden; |
|||
} |
|||
|
|||
&-button { |
|||
margin: .5rem 0; |
|||
} |
|||
|
|||
&-or { |
|||
font-size: .8rem; |
|||
} |
|||
|
|||
&-info { |
|||
color: darken($color-border, 30%); |
|||
} |
|||
} |
|||
|
|||
.btn { |
|||
cursor: default; |
|||
} |
|||
|
|||
.row { |
|||
margin-left: -8px; |
|||
margin-right: -8px; |
|||
} |
|||
|
|||
.col-3 { |
|||
padding-left: 8px; |
|||
padding-right: 8px; |
|||
} |
|||
@import '_mixins'; |
|||
@ -1,29 +1,21 @@ |
|||
<sqx-panel desiredWidth="16rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<h3 class="panel-title">Activity</h3> |
|||
</div> |
|||
|
|||
<a class="panel-close" sqxParentLink> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
<sqx-panel desiredWidth="16rem" isBlank="true"> |
|||
<ng-container title> |
|||
Activity |
|||
</ng-container> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-blank"> |
|||
<div *ngFor="let event of events | async" class="event"> |
|||
<div class="event-left"> |
|||
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" /> |
|||
<ng-container content> |
|||
<div *ngFor="let event of events | async" class="event"> |
|||
<div class="event-left"> |
|||
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" /> |
|||
</div> |
|||
<div class="event-main"> |
|||
<div class="event-message"> |
|||
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span [innerHTML]="format(event.message) | async"></span> |
|||
</div> |
|||
<div class="event-main"> |
|||
<div class="event-message"> |
|||
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span [innerHTML]="format(event.message) | async"></span> |
|||
</div> |
|||
<div class="event-created">{{event.created | sqxFromNow}}</div> |
|||
<div class="event-created">{{event.created | sqxFromNow}}</div> |
|||
|
|||
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a> |
|||
</div> |
|||
</div> |
|||
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
@ -1,70 +1,60 @@ |
|||
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title> |
|||
|
|||
<form [formGroup]="contentForm" (ngSubmit)="saveAndPublish()"> |
|||
<sqx-panel desiredWidth="53rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right" *ngIf="!content || content.status !== 'Archived'"> |
|||
<span *ngIf="isNewMode"> |
|||
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S"> |
|||
Save as Draft |
|||
</button> |
|||
<sqx-panel desiredWidth="53rem" showSidebar="true"> |
|||
<ng-container title> |
|||
<ng-container *ngIf="isNewMode"> |
|||
New Content |
|||
</ng-container> |
|||
<ng-container *ngIf="!isNewMode && content.status !== 'Archived'"> |
|||
Edit Content |
|||
</ng-container> |
|||
<ng-container *ngIf="!isNewMode && content.status === 'Archived'"> |
|||
Show Content |
|||
</ng-container> |
|||
</ng-container> |
|||
|
|||
<button type="submit" class="btn btn-primary"> |
|||
Save and Publish |
|||
</button> |
|||
</span> |
|||
<span *ngIf="!isNewMode"> |
|||
<button type="submit" class="btn btn-primary" title="CTRL + S"> |
|||
Save |
|||
</button> |
|||
</span> |
|||
|
|||
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut> |
|||
</div> |
|||
|
|||
<h3 class="panel-title" *ngIf="isNewMode"> |
|||
New Content |
|||
</h3> |
|||
<h3 class="panel-title" *ngIf="!isNewMode && content.status !== 'Archived'"> |
|||
Edit Content |
|||
</h3> |
|||
<h3 class="panel-title" *ngIf="!isNewMode && content.status === 'Archived'"> |
|||
Show Content |
|||
</h3> |
|||
</div> |
|||
<ng-container menu> |
|||
<ng-container *ngIf="isNewMode; else notNew"> |
|||
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S"> |
|||
Save as Draft |
|||
</button> |
|||
|
|||
<a class="panel-close" sqxParentLink> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
<button type="submit" class="btn btn-primary"> |
|||
Save and Publish |
|||
</button> |
|||
</ng-container> |
|||
<ng-template #notNew> |
|||
<button type="submit" class="btn btn-primary" title="CTRL + S"> |
|||
Save |
|||
</button> |
|||
</ng-template> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-scroll"> |
|||
<div class="panel-alert panel-alert-danger" *ngIf="contentOld"> |
|||
<div class="float-right"> |
|||
<a class="force" (click)="showLatest()">View latest</a> |
|||
</div> |
|||
Viewing <strong>{{content.lastModifiedBy | sqxUserNameRef:null}}'s</strong> changes of {{content.lastModified | sqxShortDate}}. |
|||
</div> |
|||
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut> |
|||
</ng-container> |
|||
|
|||
<div *ngFor="let field of schema.fields"> |
|||
<sqx-content-field [field]="field" [fieldForm]="contentForm.controls[field.name]" [languages]="languages" [contentFormSubmitted]="contentFormSubmitted"></sqx-content-field> |
|||
<ng-container content> |
|||
<div class="panel-alert panel-alert-danger" *ngIf="contentOld"> |
|||
<div class="float-right"> |
|||
<a class="force" (click)="showLatest()">View latest</a> |
|||
</div> |
|||
Viewing <strong>{{content.lastModifiedBy | sqxUserNameRef:null}}'s</strong> changes of {{content.lastModified | sqxShortDate}}. |
|||
</div> |
|||
<div class="panel-sidebar"> |
|||
<a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory *ngIf="!isNewMode"> |
|||
<i class="icon-time"></i> |
|||
</a> |
|||
<a class="panel-link" routerLink="assets" routerLinkActive="active"> |
|||
<i class="icon-media"></i> |
|||
</a> |
|||
|
|||
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000" *ngIf="!isNewMode"> |
|||
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. |
|||
</sqx-onboarding-tooltip> |
|||
<div *ngFor="let field of schema.fields"> |
|||
<sqx-content-field [field]="field" [fieldForm]="contentForm.controls[field.name]" [languages]="languages" [contentFormSubmitted]="contentFormSubmitted"></sqx-content-field> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
|
|||
<ng-container sidebar> |
|||
<a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory *ngIf="!isNewMode"> |
|||
<i class="icon-time"></i> |
|||
</a> |
|||
|
|||
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000" *ngIf="!isNewMode"> |
|||
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. |
|||
</sqx-onboarding-tooltip> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
</form> |
|||
|
|||
|
|||
@ -1,208 +1,184 @@ |
|||
<sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title> |
|||
|
|||
<sqx-panel [desiredWidth]="isReadOnly ? '40rem' : '60rem'"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()" *ngIf="!isReadOnly"></sqx-shortcut> |
|||
|
|||
<form class="form-inline" (ngSubmit)="search()"> |
|||
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" placeholder="Search for content" /> |
|||
|
|||
<a class="expand-search" (click)="searchModal.toggle()" #archive> |
|||
<i class="icon-caret-down"></i> |
|||
</a> |
|||
</form> |
|||
|
|||
<sqx-onboarding-tooltip id="contentArchive" [for]="archive" position="bottomRight" after="60000"> |
|||
Click this icon to show the advanced search menu and to show the archive! |
|||
</sqx-onboarding-tooltip> |
|||
|
|||
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000"> |
|||
Search for content using full text search over all fields and languages! |
|||
</sqx-onboarding-tooltip> |
|||
|
|||
<div class="dropdown-menu" *sqxModalView="searchModal" [sqxModalTarget]="inputFind"> |
|||
<sqx-search-form |
|||
[canArchive]="!isReadOnly" |
|||
(queryChanged)="contentsFilter.setValue($event, { emitEvent: false })" |
|||
[query]="contentsFilter.value" |
|||
(archivedChanged)="updateArchive($event)" |
|||
[archived]="isArchive"> |
|||
</sqx-search-form> |
|||
</div> |
|||
|
|||
<span *ngIf="!isReadOnly && languages.length > 1"> |
|||
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector> |
|||
</span> |
|||
|
|||
<button *ngIf="!isReadOnly" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)"> |
|||
<i class="icon-plus"></i> New |
|||
</button> |
|||
</div> |
|||
<sqx-panel [desiredWidth]="isReadOnly ? '40rem' : '60rem'" contentClass="grid"> |
|||
<ng-container title> |
|||
<ng-container *ngIf="!isReadOnly && !isArchive"> |
|||
Contents |
|||
</ng-container> |
|||
<ng-container *ngIf="isArchive"> |
|||
Archive |
|||
</ng-container> |
|||
<ng-container *ngIf="isReadOnly"> |
|||
References |
|||
</ng-container> |
|||
</ng-container> |
|||
|
|||
<ng-container menu> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()" *ngIf="!isReadOnly"></sqx-shortcut> |
|||
|
|||
<form class="form-inline" (ngSubmit)="search()"> |
|||
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" placeholder="Search for content" /> |
|||
|
|||
<a class="expand-search" (click)="searchModal.toggle()" #archive> |
|||
<i class="icon-caret-down"></i> |
|||
</a> |
|||
</form> |
|||
|
|||
<sqx-onboarding-tooltip id="contentArchive" [for]="archive" position="bottomRight" after="60000"> |
|||
Click this icon to show the advanced search menu and to show the archive! |
|||
</sqx-onboarding-tooltip> |
|||
|
|||
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000"> |
|||
Search for content using full text search over all fields and languages! |
|||
</sqx-onboarding-tooltip> |
|||
|
|||
<div class="dropdown-menu" *sqxModalView="searchModal" [sqxModalTarget]="inputFind"> |
|||
<sqx-search-form |
|||
[canArchive]="!isReadOnly" |
|||
(queryChanged)="contentsFilter.setValue($event, { emitEvent: false })" |
|||
[query]="contentsFilter.value" |
|||
(archivedChanged)="updateArchive($event)" |
|||
[archived]="isArchive"> |
|||
</sqx-search-form> |
|||
</div> |
|||
|
|||
<ng-container *ngIf="!isReadOnly && languages.length > 1"> |
|||
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector> |
|||
</ng-container> |
|||
|
|||
<button *ngIf="!isReadOnly" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)"> |
|||
<i class="icon-plus"></i> New |
|||
</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" *ngIf="!isReadOnly"> |
|||
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" /> |
|||
</th> |
|||
<th class="cell-auto" *ngFor="let field of contentFields"> |
|||
<span class="field">{{field.displayName}}</span> |
|||
</th> |
|||
<th class="cell-time"> |
|||
Updated |
|||
</th> |
|||
<th class="cell-user"> |
|||
By |
|||
</th> |
|||
<th class="cell-actions" *ngIf="!isReadOnly"> |
|||
Actions |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
</table> |
|||
</div> |
|||
|
|||
<div class="selection" *ngIf="selectionCount > 0"> |
|||
{{selectionCount}} items selected: |
|||
|
|||
<h3 class="panel-title" *ngIf="!isReadOnly && !isArchive"> |
|||
Contents |
|||
</h3> |
|||
<button class="btn btn-secondary" (click)="publishSelected()" *ngIf="canPublish"> |
|||
Publish |
|||
</button> |
|||
|
|||
<h3 class="panel-title" *ngIf="isArchive"> |
|||
<button class="btn btn-secondary" (click)="unpublishSelected()" *ngIf="canUnpublish"> |
|||
Unpublish |
|||
</button> |
|||
|
|||
<button class="btn btn-secondary" (click)="archiveSelected()" *ngIf="!isArchive"> |
|||
Archive |
|||
</h3> |
|||
|
|||
<h3 class="panel-title" *ngIf="isReadOnly"> |
|||
References |
|||
</h3> |
|||
</button> |
|||
|
|||
<button class="btn btn-secondary" (click)="restoreSelected()" *ngIf="isArchive"> |
|||
Restore |
|||
</button> |
|||
|
|||
<button class="btn btn-danger" |
|||
(sqxConfirmClick)="deleteSelected()" |
|||
confirmTitle="Delete content" |
|||
confirmText="Do you really want to delete the selected content items?"> |
|||
Delete |
|||
</button> |
|||
</div> |
|||
|
|||
<a class="panel-close" sqxParentLink> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content grid"> |
|||
<div class="grid-header"> |
|||
<table class="table table-items table-fixed" *ngIf="contentItems"> |
|||
<thead> |
|||
<tr> |
|||
<th class="cell-select" *ngIf="!isReadOnly"> |
|||
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" /> |
|||
</th> |
|||
<th class="cell-auto" *ngFor="let field of contentFields"> |
|||
<span class="field">{{field.displayName}}</span> |
|||
</th> |
|||
<th class="cell-time"> |
|||
Updated |
|||
</th> |
|||
<th class="cell-user"> |
|||
By |
|||
</th> |
|||
<th class="cell-actions" *ngIf="!isReadOnly"> |
|||
Actions |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
<div class="grid-content"> |
|||
<div sqxIgnoreScrollbar> |
|||
<table class="table table-items table-fixed" *ngIf="contentItems" > |
|||
<tbody *ngIf="!isReadOnly"> |
|||
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackByContent"> |
|||
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active" |
|||
[language]="languageSelected" |
|||
[schemaFields]="contentFields" |
|||
[schema]="schema" |
|||
[selected]="isItemSelected(content)" |
|||
(selectedChange)="selectItem(content, $event)" |
|||
(unpublishing)="unpublishContent(content)" |
|||
(publishing)="publishContent(content)" |
|||
(archiving)="archiveContent(content)" |
|||
(restoring)="restoreContent(content)" |
|||
(deleting)="deleteContent(content)" |
|||
(saved)="onContentSaved(content, $event)"></tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
|
|||
<tbody *ngIf="isReadOnly"> |
|||
<ng-template ngFor let-content [ngForOf]="contentItems"> |
|||
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)" |
|||
[language]="languageSelected" |
|||
[schemaFields]="contentFields" |
|||
[schema]="schema" |
|||
isReadOnly="true"></tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="selection" *ngIf="selectionCount > 0"> |
|||
{{selectionCount}} items selected: |
|||
|
|||
<button class="btn btn-secondary" (click)="publishSelected()" *ngIf="canPublish"> |
|||
Publish |
|||
</button> |
|||
|
|||
<button class="btn btn-secondary" (click)="unpublishSelected()" *ngIf="canUnpublish"> |
|||
Unpublish |
|||
</button> |
|||
|
|||
<button class="btn btn-secondary" (click)="archiveSelected()" *ngIf="!isArchive"> |
|||
Archive |
|||
</button> |
|||
|
|||
<button class="btn btn-secondary" (click)="restoreSelected()" *ngIf="isArchive"> |
|||
Restore |
|||
</button> |
|||
|
|||
<button class="btn btn-danger" |
|||
(sqxConfirmClick)="deleteSelected()" |
|||
confirmTitle="Delete content" |
|||
confirmText="Do you really want to delete the selected content items?"> |
|||
Delete |
|||
</button> |
|||
</div> |
|||
|
|||
<div class="grid-content"> |
|||
<div sqxIgnoreScrollbar> |
|||
<table class="table table-items table-fixed" *ngIf="contentItems" > |
|||
<tbody *ngIf="!isReadOnly"> |
|||
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackBy"> |
|||
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active" |
|||
[language]="languageSelected" |
|||
[schemaFields]="contentFields" |
|||
[schema]="schema" |
|||
[selected]="isItemSelected(content)" |
|||
(selectedChange)="selectItem(content, $event)" |
|||
(unpublishing)="unpublishContent(content)" |
|||
(publishing)="publishContent(content)" |
|||
(archiving)="archiveContent(content)" |
|||
(restoring)="restoreContent(content)" |
|||
(deleting)="deleteContent(content)" |
|||
(saved)="onContentSaved(content, $event)"></tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
|
|||
<tbody *ngIf="isReadOnly"> |
|||
<ng-template ngFor let-content [ngForOf]="contentItems"> |
|||
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)" |
|||
[language]="languageSelected" |
|||
[schemaFields]="contentFields" |
|||
[schema]="schema" |
|||
isReadOnly="true"></tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="grid-footer clearfix" *ngIf="contentsPager.numberOfItems > 0"> |
|||
<div class="float-right pagination"> |
|||
<span class="pagination-text">{{contentsPager.itemFirst}}-{{contentsPager.itemLast}} of {{contentsPager.numberOfItems}}</span> |
|||
|
|||
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!contentsPager.canGoPrev" (click)="goPrev()"> |
|||
<i class="icon-angle-left"></i> |
|||
</button> |
|||
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!contentsPager.canGoNext" (click)="goNext()"> |
|||
<i class="icon-angle-right"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="grid-footer"> |
|||
<sqx-pager [pager]="contentsPager"></sqx-pager> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
<router-outlet></router-outlet> |
|||
|
|||
<div class="modal" *sqxModalView="dueTimeDialog;onRoot:true"> |
|||
<div class="modal-backdrop"></div> |
|||
<div class="modal-dialog"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h4 class="modal-title">{{dueTimeAction}} content item(s)</h4> |
|||
<ng-container *sqxModalView="dueTimeDialog;onRoot:true"> |
|||
<sqx-modal-dialog (close)="cancelStatusChange()"> |
|||
<ng-container title> |
|||
{{dueTimeAction}} content item(s) |
|||
</ng-container> |
|||
|
|||
<ng-container content> |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately"> |
|||
<label class="form-check-label" for="immediately"> |
|||
{{dueTimeAction}} content item(s) immediately. |
|||
</label> |
|||
</div> |
|||
|
|||
<div class="modal-body"> |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately"> |
|||
<label class="form-check-label" for="immediately"> |
|||
{{dueTimeAction}} content item(s) immediately. |
|||
</label> |
|||
</div> |
|||
|
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled"> |
|||
<label class="form-check-label" for="scheduled"> |
|||
{{dueTimeAction}} content item(s) at a later point date and time. |
|||
</label> |
|||
</div> |
|||
|
|||
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor> |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled"> |
|||
<label class="form-check-label" for="scheduled"> |
|||
{{dueTimeAction}} content item(s) at a later point date and time. |
|||
</label> |
|||
</div> |
|||
|
|||
<div class="modal-footer"> |
|||
<div class="clearfix"> |
|||
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button> |
|||
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()">Confirm</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor> |
|||
</ng-container> |
|||
|
|||
<ng-container footer> |
|||
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button> |
|||
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()">Confirm</button> |
|||
</ng-container> |
|||
</sqx-modal-dialog> |
|||
</ng-container> |
|||
|
|||
@ -1,35 +1,27 @@ |
|||
<sqx-title message="{app} | Schemas" parameter1="app" [value1]="ctx.appName"></sqx-title> |
|||
|
|||
<sqx-panel theme="dark" desiredWidth="16rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<h3 class="panel-title">Schemas</h3> |
|||
</div> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
<sqx-panel theme="dark" desiredWidth="16rem" showSecondHeader="true"> |
|||
<ng-container title> |
|||
Schemas |
|||
</ng-container> |
|||
|
|||
<div class="panel-header-row"> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
<ng-container secondHeader> |
|||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|||
|
|||
<div class="search-form"> |
|||
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" /> |
|||
<div class="search-form"> |
|||
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" /> |
|||
|
|||
<i class="icon-search"></i> |
|||
</div> |
|||
<i class="icon-search"></i> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
|
|||
<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"> |
|||
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
<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"> |
|||
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a> |
|||
</li> |
|||
</ul> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -1,116 +1,97 @@ |
|||
<sqx-title message="{app} | Rules Events" parameter1="app" [value1]="ctx.appName"></sqx-title> |
|||
|
|||
<sqx-panel desiredWidth="63rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Events (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
<ng-container title> |
|||
Events |
|||
</ng-container> |
|||
|
|||
<ng-container menu> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Events (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
</div> |
|||
|
|||
<h3 class="panel-title">Events</h3> |
|||
</div> |
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
</ng-container> |
|||
|
|||
<ng-container content> |
|||
<table class="table table-items table-fixed"> |
|||
<thead> |
|||
<tr> |
|||
<th class="cell-label"> |
|||
Status |
|||
</th> |
|||
<th class="cell-40"> |
|||
Event |
|||
</th> |
|||
<th class="cell-60"> |
|||
Description |
|||
</th> |
|||
<th class="cell-time"> |
|||
Created |
|||
</th> |
|||
<th class="cell-actions"> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
</th> |
|||
</tr> |
|||
</thead> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-scroll"> |
|||
<table class="table table-items table-fixed"> |
|||
<thead> |
|||
<tr> |
|||
<th class="cell-label"> |
|||
Status |
|||
</th> |
|||
<th class="cell-40"> |
|||
Event |
|||
</th> |
|||
<th class="cell-60"> |
|||
Description |
|||
</th> |
|||
<th class="cell-time"> |
|||
Created |
|||
</th> |
|||
<th class="cell-actions"> |
|||
|
|||
</th> |
|||
<tbody> |
|||
<ng-template ngFor let-event [ngForOf]="eventsItems"> |
|||
<tr [class.expanded]="selectedEventId === event.id"> |
|||
<td class="cell-label"> |
|||
<span class="badge badge-pill badge-{{getBadgeClass(event.jobResult)}}">{{event.jobResult}}</span> |
|||
</td> |
|||
<td class="cell-40"> |
|||
<span class="truncate">{{event.eventName}}</span> |
|||
</td> |
|||
<td class="cell-60"> |
|||
<span class="truncate">{{event.description}}</span> |
|||
</td> |
|||
<td class="cell-time"> |
|||
<small class="item-modified">{{event.created | sqxFromNow}}</small> |
|||
</td> |
|||
<td class="cell-actions"> |
|||
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="selectedEventId === event.id" (click)="selectEvent(event.id)"> |
|||
<i class="icon-settings"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
</thead> |
|||
|
|||
<tbody> |
|||
<ng-template ngFor let-event [ngForOf]="eventsItems"> |
|||
<tr [class.expanded]="selectedEventId === event.id"> |
|||
<td class="cell-label"> |
|||
<span class="badge badge-pill badge-{{getBadgeClass(event.jobResult)}}">{{event.jobResult}}</span> |
|||
</td> |
|||
<td class="cell-40"> |
|||
<span class="truncate">{{event.eventName}}</span> |
|||
</td> |
|||
<td class="cell-60"> |
|||
<span class="truncate">{{event.description}}</span> |
|||
</td> |
|||
<td class="cell-time"> |
|||
<small class="item-modified">{{event.created | sqxFromNow}}</small> |
|||
</td> |
|||
<td class="cell-actions"> |
|||
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="selectedEventId === event.id" (click)="selectEvent(event.id)"> |
|||
<i class="icon-settings"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr *ngIf="selectedEventId === event.id"> |
|||
<td colspan="5"> |
|||
<div class="event-header"> |
|||
<h3>Last Invocation</h3> |
|||
<tr *ngIf="selectedEventId === event.id"> |
|||
<td colspan="5"> |
|||
<div class="event-header"> |
|||
<h3>Last Invocation</h3> |
|||
</div> |
|||
|
|||
<div class="row event-stats"> |
|||
<div class="col-3"> |
|||
<span class="badge badge-pill badge-{{getBadgeClass(event.result)}}">{{event.result}}</span> |
|||
</div> |
|||
|
|||
<div class="row event-stats"> |
|||
<div class="col-3"> |
|||
<span class="badge badge-pill badge-{{getBadgeClass(event.result)}}">{{event.result}}</span> |
|||
</div> |
|||
<div class="col-3"> |
|||
Attempts: {{event.numCalls}} |
|||
</div> |
|||
<div class="col-3"> |
|||
Next: <span *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</span> |
|||
</div> |
|||
<div class="col-3 text-right"> |
|||
<button class="btn btn-success btn-sm" (click)="enqueueEvent(event)"> |
|||
Enqueue |
|||
</button> |
|||
</div> |
|||
<div class="col-3"> |
|||
Attempts: {{event.numCalls}} |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<textarea class="event-dump form-control" readonly>{{event.lastDump}}</textarea> |
|||
</div> |
|||
<div class="col-3"> |
|||
Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
|
|||
<div class="clearfix" *ngIf="eventsPager.numberOfItems > 0"> |
|||
<div class="float-right pagination"> |
|||
<span class="pagination-text">{{eventsPager.itemFirst}}-{{eventsPager.itemLast}} of {{eventsPager.numberOfItems}}</span> |
|||
<div class="col-3 text-right"> |
|||
<button class="btn btn-success btn-sm" (click)="enqueueEvent(event)"> |
|||
Enqueue |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<textarea class="event-dump form-control" readonly>{{event.lastDump}}</textarea> |
|||
</div> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
|
|||
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!eventsPager.canGoPrev" (click)="goPrev()"> |
|||
<i class="icon-angle-left"></i> |
|||
</button> |
|||
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!eventsPager.canGoNext" (click)="goNext()"> |
|||
<i class="icon-angle-right"></i> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<sqx-pager [pager]="eventsPager"></sqx-pager> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -1,127 +1,122 @@ |
|||
<div class="modal-dialog modal-lg"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h4 class="modal-title" *ngIf="mode === 'EditTrigger'"> |
|||
Edit Trigger |
|||
</h4> |
|||
<h4 class="modal-title" *ngIf="mode === 'EditAction'"> |
|||
Edit Action |
|||
</h4> |
|||
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 1"> |
|||
Step 1 of 4: Select Trigger |
|||
</h4> |
|||
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 2"> |
|||
Step 2 of 4: Configure Trigger |
|||
</h4> |
|||
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 3"> |
|||
Step 3 of 4: Select Action |
|||
</h4> |
|||
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 4"> |
|||
Step 4 of 4: Configure Action |
|||
</h4> |
|||
|
|||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="cancel()"> |
|||
<span aria-hidden="true">×</span> |
|||
</button> |
|||
</div> |
|||
<sqx-modal-dialog large="true" (close)="cancel()"> |
|||
<ng-container title> |
|||
<ng-container *ngIf="mode === 'EditTrigger'"> |
|||
Edit Trigger |
|||
</ng-container> |
|||
<ng-container *ngIf="mode === 'EditAction'"> |
|||
Edit Action |
|||
</ng-container> |
|||
<ng-container *ngIf="mode === 'Wizard' && step === 1"> |
|||
Step 1 of 4: Select Trigger |
|||
</ng-container> |
|||
<ng-container *ngIf="mode === 'Wizard' && step === 2"> |
|||
Step 2 of 4: Configure Trigger |
|||
</ng-container> |
|||
<ng-container *ngIf="mode === 'Wizard' && step === 3"> |
|||
Step 3 of 4: Select Action |
|||
</ng-container> |
|||
<ng-container *ngIf="mode === 'Wizard' && step === 4"> |
|||
Step 4 of 4: Configure Action |
|||
</ng-container> |
|||
</ng-container> |
|||
|
|||
<div class="modal-body"> |
|||
<div *ngIf="step === 1"> |
|||
<span *ngFor="let trigger of ruleTriggers | sqxKeys" class="rule-element rule-element-{{trigger}}" (click)="selectTriggerType(trigger)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-trigger-{{trigger}}"></i> |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleTriggers[trigger].name}} |
|||
</span> |
|||
<ng-container content> |
|||
<ng-container *ngIf="step === 1"> |
|||
<span *ngFor="let trigger of ruleTriggers | sqxKeys" class="rule-element rule-element-{{trigger}}" (click)="selectTriggerType(trigger)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-trigger-{{trigger}}"></i> |
|||
</span> |
|||
</div> |
|||
|
|||
<div *ngIf="step === 2 && schemas" class="modal-form"> |
|||
<div [ngSwitch]="triggerType"> |
|||
<div *ngSwitchCase="'AssetChanged'"> |
|||
<sqx-asset-changed-trigger #triggerControl |
|||
[trigger]="trigger" |
|||
(triggerChanged)="selectTrigger($event)"> |
|||
</sqx-asset-changed-trigger> |
|||
</div> |
|||
<div *ngSwitchCase="'ContentChanged'"> |
|||
<sqx-content-changed-trigger #triggerControl |
|||
[schemas]="schemas" |
|||
[trigger]="trigger" |
|||
(triggerChanged)="selectTrigger($event)"> |
|||
</sqx-content-changed-trigger> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<span class="rule-element-text"> |
|||
{{ruleTriggers[trigger].name}} |
|||
</span> |
|||
</span> |
|||
</ng-container> |
|||
|
|||
<div *ngIf="step === 3"> |
|||
<span *ngFor="let action of ruleActions | sqxKeys" class="rule-element rule-element-{{action}}" (click)="selectActionType(action)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-action-{{action}}"></i> |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleActions[action].name}} |
|||
</span> |
|||
<ng-container *ngIf="step === 2 && schemas"> |
|||
<ng-container [ngSwitch]="triggerType"> |
|||
<ng-container *ngSwitchCase="'AssetChanged'"> |
|||
<sqx-asset-changed-trigger #triggerControl |
|||
[trigger]="trigger" |
|||
(triggerChanged)="selectTrigger($event)"> |
|||
</sqx-asset-changed-trigger> |
|||
</ng-container> |
|||
<ng-container *ngSwitchCase="'ContentChanged'"> |
|||
<sqx-content-changed-trigger #triggerControl |
|||
[schemas]="schemas" |
|||
[trigger]="trigger" |
|||
(triggerChanged)="selectTrigger($event)"> |
|||
</sqx-content-changed-trigger> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
|
|||
<ng-container *ngIf="step === 3"> |
|||
<span *ngFor="let action of ruleActions | sqxKeys" class="rule-element rule-element-{{action}}" (click)="selectActionType(action)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-action-{{action}}"></i> |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleActions[action].name}} |
|||
</span> |
|||
</div> |
|||
</span> |
|||
</ng-container> |
|||
|
|||
<ng-container *ngIf="step === 4"> |
|||
<ng-container [ngSwitch]="actionType"> |
|||
<ng-container *ngSwitchCase="'Algolia'"> |
|||
<sqx-algolia-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-algolia-action> |
|||
</ng-container> |
|||
<ng-container *ngSwitchCase="'AzureQueue'"> |
|||
<sqx-azure-queue-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-azure-queue-action> |
|||
</ng-container> |
|||
<ng-container *ngSwitchCase="'ElasticSearch'"> |
|||
<sqx-elastic-search-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-elastic-search-action> |
|||
</ng-container> |
|||
<ng-container *ngSwitchCase="'Fastly'"> |
|||
<sqx-fastly-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-fastly-action> |
|||
</ng-container> |
|||
<ng-container *ngSwitchCase="'Slack'"> |
|||
<sqx-slack-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-slack-action> |
|||
</ng-container> |
|||
<ng-container *ngSwitchCase="'Webhook'"> |
|||
<sqx-webhook-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-webhook-action> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
|
|||
<ng-container footer> |
|||
<ng-container *ngIf="mode === 'Wizard' && step === 2"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Next</button> |
|||
</ng-container> |
|||
|
|||
<div *ngIf="step === 4" class="modal-form"> |
|||
<div [ngSwitch]="actionType"> |
|||
<div *ngSwitchCase="'Algolia'"> |
|||
<sqx-algolia-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-algolia-action> |
|||
</div> |
|||
<div *ngSwitchCase="'AzureQueue'"> |
|||
<sqx-azure-queue-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-azure-queue-action> |
|||
</div> |
|||
<div *ngSwitchCase="'ElasticSearch'"> |
|||
<sqx-elastic-search-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-elastic-search-action> |
|||
</div> |
|||
<div *ngSwitchCase="'Fastly'"> |
|||
<sqx-fastly-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-fastly-action> |
|||
</div> |
|||
<div *ngSwitchCase="'Slack'"> |
|||
<sqx-slack-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-slack-action> |
|||
</div> |
|||
<div *ngSwitchCase="'Webhook'"> |
|||
<sqx-webhook-action #actionControl |
|||
[action]="action" |
|||
(actionChanged)="selectAction($event)"> |
|||
</sqx-webhook-action> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="modal-footer" *ngIf="step === 2 || step === 4"> |
|||
<div class="clearfix" *ngIf="mode === 'Wizard' && step === 2"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Next</button> |
|||
</div> |
|||
|
|||
<div class="clearfix" *ngIf="mode !== 'Wizard' && step === 2"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Save</button> |
|||
</div> |
|||
<ng-container *ngIf="mode !== 'Wizard' && step === 2"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Save</button> |
|||
</ng-container> |
|||
|
|||
<div class="clearfix" *ngIf="step === 4"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary" (click)="actionControl.save()">Save</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<ng-container *ngIf="step === 4"> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-primary" (click)="actionControl.save()">Save</button> |
|||
</ng-container> |
|||
</ng-container> |
|||
</sqx-modal-dialog> |
|||
@ -1,107 +1,97 @@ |
|||
<sqx-title message="{app} | Rules" parameter1="app" [value1]="ctx.appName"></sqx-title> |
|||
|
|||
<sqx-panel desiredWidth="54rem"> |
|||
<div class="panel-header"> |
|||
<div class="panel-title-row"> |
|||
<div class="float-right"> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
<sqx-panel desiredWidth="54rem" showSidebar="true"> |
|||
<ng-container title> |
|||
Rules |
|||
</ng-container> |
|||
|
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> |
|||
|
|||
<button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)"> |
|||
<i class="icon-plus"></i> New |
|||
</button> |
|||
</div> |
|||
<ng-container menu> |
|||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
|||
<i class="icon-reset"></i> Refresh |
|||
</button> |
|||
|
|||
<h3 class="panel-title">Rules</h3> |
|||
</div> |
|||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
|||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> |
|||
|
|||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|||
<i class="icon-close"></i> |
|||
</a> |
|||
</div> |
|||
<button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)"> |
|||
<i class="icon-plus"></i> New |
|||
</button> |
|||
</ng-container> |
|||
|
|||
<div class="panel-main"> |
|||
<div class="panel-content panel-content-scroll"> |
|||
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0"> |
|||
No Rule created yet. |
|||
</div> |
|||
<ng-container content> |
|||
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0"> |
|||
No Rule created yet. |
|||
</div> |
|||
|
|||
<table class="table table-items table-fixed" *ngIf="rules && rules.length > 0"> |
|||
<tbody> |
|||
<ng-template ngFor let-rule [ngForOf]="rules"> |
|||
<tr> |
|||
<td class="cell-separator"> |
|||
<h3>If</h3> |
|||
</td> |
|||
<td class="cell-auto"> |
|||
<span class="rule-element rule-element-{{rule.triggerType}}" (click)="editTrigger(rule)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-trigger-{{rule.triggerType}}"></i> |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleTriggers[rule.triggerType].name}} |
|||
</span> |
|||
<table class="table table-items table-fixed" *ngIf="rules && rules.length > 0"> |
|||
<tbody> |
|||
<ng-template ngFor let-rule [ngForOf]="rules"> |
|||
<tr> |
|||
<td class="cell-separator"> |
|||
<h3>If</h3> |
|||
</td> |
|||
<td class="cell-auto"> |
|||
<span class="rule-element rule-element-{{rule.triggerType}}" (click)="editTrigger(rule)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-trigger-{{rule.triggerType}}"></i> |
|||
</span> |
|||
</td> |
|||
<td class="cell-separator"> |
|||
<h3>then</h3> |
|||
</td> |
|||
<td class="cell-auto"> |
|||
<span class="rule-element rule-element-{{rule.actionType}}" (click)="editAction(rule)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-action-{{rule.actionType}}"></i> |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleActions[rule.actionType].name}} |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleTriggers[rule.triggerType].name}} |
|||
</span> |
|||
</td> |
|||
<td class="cell-actions"> |
|||
<sqx-toggle [ngModel]="rule.isEnabled" (ngModelChange)="toggleRule(rule)"></sqx-toggle> |
|||
</td> |
|||
<td class="cell-actions"> |
|||
<button type="button" class="btn btn-link btn-danger" |
|||
(sqxConfirmClick)="deleteRule(rule)" |
|||
confirmTitle="Delete rule" |
|||
confirmText="Do you really want to delete the rule?"> |
|||
<i class="icon-bin2"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</span> |
|||
</td> |
|||
<td class="cell-separator"> |
|||
<h3>then</h3> |
|||
</td> |
|||
<td class="cell-auto"> |
|||
<span class="rule-element rule-element-{{rule.actionType}}" (click)="editAction(rule)"> |
|||
<span class="rule-element-icon"> |
|||
<i class="icon-action-{{rule.actionType}}"></i> |
|||
</span> |
|||
<span class="rule-element-text"> |
|||
{{ruleActions[rule.actionType].name}} |
|||
</span> |
|||
</span> |
|||
</td> |
|||
<td class="cell-actions"> |
|||
<sqx-toggle [ngModel]="rule.isEnabled" (ngModelChange)="toggleRule(rule)"></sqx-toggle> |
|||
</td> |
|||
<td class="cell-actions"> |
|||
<button type="button" class="btn btn-link btn-danger" |
|||
(sqxConfirmClick)="deleteRule(rule)" |
|||
confirmTitle="Delete rule" |
|||
confirmText="Do you really want to delete the rule?"> |
|||
<i class="icon-bin2"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
<tr class="spacer"></tr> |
|||
</ng-template> |
|||
</tbody> |
|||
</table> |
|||
</ng-container> |
|||
|
|||
<div class="panel-sidebar"> |
|||
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory> |
|||
<i class="icon-time"></i> |
|||
</a> |
|||
|
|||
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp> |
|||
<i class="icon-help"></i> |
|||
</a> |
|||
|
|||
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000"> |
|||
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" target="_blank">https://docs.squidex.io</a> for the full documentation. |
|||
</sqx-onboarding-tooltip> |
|||
</div> |
|||
</div> |
|||
<ng-container sidebar> |
|||
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory> |
|||
<i class="icon-time"></i> |
|||
</a> |
|||
|
|||
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp> |
|||
<i class="icon-help"></i> |
|||
</a> |
|||
|
|||
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000"> |
|||
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" target="_blank">https://docs.squidex.io</a> for the full documentation. |
|||
</sqx-onboarding-tooltip> |
|||
</ng-container> |
|||
</sqx-panel> |
|||
|
|||
<div class="modal" *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false" @fade> |
|||
<div class="modal-backdrop"></div> |
|||
|
|||
<ng-container *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false"> |
|||
<sqx-rule-wizard [schemas]="schemas" [rule]="wizardRule" [mode]="wizardMode" |
|||
(updated)="onRuleUpdated($event)" |
|||
(cancelled)="addRuleDialog.hide()" |
|||
(created)="onRuleCreated($event)"> |
|||
</sqx-rule-wizard> |
|||
</div> |
|||
</ng-container> |
|||
|
|||
<router-outlet></router-outlet> |
|||
@ -0,0 +1,36 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
import { allParams, SchemasState } from '@app/shared'; |
|||
|
|||
@Injectable() |
|||
export class SchemaMustExistGuard implements CanActivate { |
|||
constructor( |
|||
private readonly schemasState: SchemasState, |
|||
private readonly router: Router |
|||
) { |
|||
} |
|||
|
|||
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { |
|||
const schemaName = allParams(route)['schemaName']; |
|||
|
|||
const result = |
|||
this.schemasState.selectSchema(schemaName) |
|||
.do(dto => { |
|||
if (!dto) { |
|||
this.router.navigate(['/404']); |
|||
} |
|||
}) |
|||
.map(u => u !== null); |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
<form [formGroup]="addFieldForm.form" (ngSubmit)="addField(false)"> |
|||
<sqx-modal-dialog (close)="complete()" large="true"> |
|||
<ng-container title> |
|||
Create Field |
|||
</ng-container> |
|||
|
|||
<ng-container content> |
|||
<sqx-form-error [error]="addFieldForm.error | async"></sqx-form-error> |
|||
|
|||
<div class="form-group"> |
|||
<div class="row"> |
|||
<div class="col-4 type" *ngFor="let fieldType of fieldTypes"> |
|||
<label> |
|||
<input type="radio" class="radio-input" formControlName="type" value="{{fieldType.type}}" /> |
|||
|
|||
<div class="row no-gutters"> |
|||
<div class="col col-auto"> |
|||
<div class="type-icon" [class.active]="addFieldForm.form.controls['type'].value === fieldType.type"> |
|||
<i class="icon-type-{{fieldType.type}}"></i> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="type-title">{{fieldType.type}}</div> |
|||
<div class="type-text text-muted">{{fieldType.description}}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<sqx-control-errors for="name" submitOnly="true" [submitted]="addFieldForm.submitted | async"></sqx-control-errors> |
|||
|
|||
<input type="text" class="form-control" formControlName="name" maxlength="40" #nameInput placeholder="Enter field name" sqxFocusOnInit /> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable" /> |
|||
<label class="form-check-label" for="isLocalizable"> |
|||
Localizable |
|||
</label> |
|||
</div> |
|||
|
|||
<small class="form-text text-muted"> |
|||
You can the field as localizable. It means that is dependent on the language, for example a city name. |
|||
</small> |
|||
</div> |
|||
</ng-container> |
|||
|
|||
<ng-container footer> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button> |
|||
|
|||
<div class="float-right"> |
|||
<button class="btn btn-success" (click)="addField(false)">Create and close</button> |
|||
<button class="btn btn-success" (click)="addField(true)">Create and new field</button> |
|||
</div> |
|||
</ng-container> |
|||
</sqx-modal-dialog> |
|||
</form> |
|||
@ -0,0 +1,72 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
$icon-size: 4.5rem; |
|||
|
|||
.form-group { |
|||
margin-bottom: 1.5rem; |
|||
} |
|||
|
|||
.nav-link { |
|||
cursor: default; |
|||
} |
|||
|
|||
.nav-tabs { |
|||
margin-bottom: 1rem; |
|||
} |
|||
|
|||
.type { |
|||
& { |
|||
margin-bottom: .5rem; |
|||
} |
|||
|
|||
&-title { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
&-text { |
|||
font-size: .9rem; |
|||
} |
|||
|
|||
&-icon { |
|||
& { |
|||
@include border-radius; |
|||
height: $icon-size; |
|||
color: $color-theme-blue; |
|||
cursor: pointer; |
|||
border: 1px solid $color-border; |
|||
background: transparent; |
|||
margin-right: .5rem; |
|||
line-height: $icon-size; |
|||
font-size: 1.75rem; |
|||
font-weight: normal; |
|||
text-align: center; |
|||
width: $icon-size; |
|||
} |
|||
|
|||
.radio-input { |
|||
display: none; |
|||
} |
|||
|
|||
&.active { |
|||
& { |
|||
@include box-shadow(0, 0, 10px, .5); |
|||
background: $color-theme-blue; |
|||
border-color: $color-theme-blue; |
|||
color: $color-dark-foreground; |
|||
} |
|||
|
|||
&:hover { |
|||
color: $color-dark-foreground; |
|||
} |
|||
} |
|||
|
|||
&:hover { |
|||
border-color: $color-border-dark; |
|||
} |
|||
} |
|||
|
|||
.radio-input { |
|||
display: none; |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; |
|||
import { FormBuilder } from '@angular/forms'; |
|||
|
|||
import { |
|||
AddFieldForm, |
|||
fieldTypes, |
|||
SchemaDetailsDto, |
|||
SchemasState |
|||
} from '@app/shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-field-wizard', |
|||
styleUrls: ['./field-wizard.component.scss'], |
|||
templateUrl: './field-wizard.component.html' |
|||
}) |
|||
export class FieldWizardComponent { |
|||
@ViewChild('nameInput') |
|||
public nameInput: ElementRef; |
|||
|
|||
@Input() |
|||
public schema: SchemaDetailsDto; |
|||
|
|||
@Output() |
|||
public completed = new EventEmitter(); |
|||
|
|||
public fieldTypes = fieldTypes; |
|||
|
|||
public addFieldForm: AddFieldForm; |
|||
|
|||
constructor(formBuilder: FormBuilder, |
|||
private readonly schemasState: SchemasState |
|||
) { |
|||
this.addFieldForm = new AddFieldForm(formBuilder); |
|||
} |
|||
|
|||
public complete() { |
|||
this.completed.emit(); |
|||
} |
|||
|
|||
public addField(next: boolean) { |
|||
const value = this.addFieldForm.submit(); |
|||
|
|||
if (value) { |
|||
this.schemasState.addField(this.schema, value) |
|||
.subscribe(dto => { |
|||
this.addFieldForm.submitCompleted({ type: fieldTypes[0].type }); |
|||
|
|||
if (next) { |
|||
this.nameInput.nativeElement.focus(); |
|||
} else { |
|||
this.complete(); |
|||
} |
|||
}, error => { |
|||
this.addFieldForm.submitFailed(error); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,56 @@ |
|||
<div [formGroup]="editForm"> |
|||
<div class="form-group row" *ngIf="showName"> |
|||
<label class="col col-3 col-form-label" for="fieldName">Name</label> |
|||
|
|||
<div class="col col-6"> |
|||
<input type="text" class="form-control" id="fieldName" readonly [ngModel]="field.name" [ngModelOptions]="{standalone: true}" /> |
|||
|
|||
<small class="form-text text-muted"> |
|||
The name of the field in the API response. |
|||
</small> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-3 col-form-label" for="fieldLabel">Label</label> |
|||
|
|||
<div class="col col-6"> |
|||
<sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors> |
|||
|
|||
<input type="text" class="form-control" id="fieldLabel" maxlength="100" formControlName="label" /> |
|||
|
|||
<small class="form-text text-muted"> |
|||
Define the display name for the field for documentation and user interfaces. |
|||
</small> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<label class="col col-3 col-form-label" for="fieldHints">Hints</label> |
|||
|
|||
<div class="col col-6"> |
|||
<sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors> |
|||
|
|||
<input type="text" class="form-control" id="fieldHints" maxlength="100" formControlName="hints" /> |
|||
|
|||
<small class="form-text text-muted"> |
|||
Define some hints for the user and editor for the field for documentation and user interfaces. |
|||
</small> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-group row"> |
|||
<div class="col col-6 offset-3"> |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="checkbox" id="fieldListfield" formControlName="isListField" /> |
|||
<label class="form-check-label" for="fieldListfield"> |
|||
List Field |
|||
</label> |
|||
</div> |
|||
|
|||
<small class="form-text text-muted"> |
|||
List fields are shown as a column in the content list.<br />When no list field is defined, the first field is used. |
|||
</small> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,2 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
@ -0,0 +1,31 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; |
|||
import { FormGroup } from '@angular/forms'; |
|||
|
|||
import { FieldDto } from '@app/shared'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-field-form-common', |
|||
styleUrls: ['field-form-common.component.scss'], |
|||
templateUrl: 'field-form-common.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class FieldFormCommonComponent { |
|||
@Input() |
|||
public editForm: FormGroup; |
|||
|
|||
@Input() |
|||
public editFormSubmitted = false; |
|||
|
|||
@Input() |
|||
public showName = true; |
|||
|
|||
@Input() |
|||
public field: FieldDto; |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<div [ngSwitch]="field.properties.fieldType"> |
|||
<div *ngSwitchCase="'Number'"> |
|||
<sqx-number-ui [editForm]="editForm" [properties]="field.properties"></sqx-number-ui> |
|||
</div> |
|||
<div *ngSwitchCase="'String'"> |
|||
<sqx-string-ui [editForm]="editForm" [properties]="field.properties"></sqx-string-ui> |
|||
</div> |
|||
<div *ngSwitchCase="'Boolean'"> |
|||
<sqx-boolean-ui [editForm]="editForm" [properties]="field.properties"></sqx-boolean-ui> |
|||
</div> |
|||
<div *ngSwitchCase="'DateTime'"> |
|||
<sqx-date-time-ui [editForm]="editForm" [properties]="field.properties"></sqx-date-time-ui> |
|||
</div> |
|||
<div *ngSwitchCase="'Geolocation'"> |
|||
<sqx-geolocation-ui [editForm]="editForm" [properties]="field.properties"></sqx-geolocation-ui> |
|||
</div> |
|||
<div *ngSwitchCase="'Json'"> |
|||
<sqx-json-ui [editForm]="editForm" [properties]="field.properties"></sqx-json-ui> |
|||
</div> |
|||
<div *ngSwitchCase="'Assets'"> |
|||
<sqx-assets-ui [editForm]="editForm" [properties]="field.properties"></sqx-assets-ui> |
|||
</div> |
|||
<div *ngSwitchCase="'References'"> |
|||
<sqx-references-ui [editForm]="editForm" [properties]="field.properties"></sqx-references-ui> |
|||
</div> |
|||
<div *ngSwitchCase="'Tags'"> |
|||
<sqx-tags-ui [editForm]="editForm" [properties]="field.properties"></sqx-tags-ui> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,2 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue