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> |
<main> |
||||
<router-outlet (activate)="isLoaded = true"> |
<sqx-root-view> |
||||
<div class="loading" *ngIf="!isLoaded"> |
<sqx-dialog-renderer> |
||||
<img src="/images/loader.gif" /> |
<router-outlet (activate)="isLoaded = true"> |
||||
|
<div class="loading" *ngIf="!isLoaded"> |
||||
|
<img src="/images/loader.gif" /> |
||||
|
|
||||
<div>Loading Squidex</div> |
<div>Loading Squidex</div> |
||||
</div> |
</div> |
||||
</router-outlet> |
</router-outlet> |
||||
|
</sqx-dialog-renderer> |
||||
|
</sqx-root-view> |
||||
</main> |
</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-title message="Event Consumers"></sqx-title> |
||||
|
|
||||
<sqx-panel theme="light" desiredWidth="50rem"> |
<sqx-panel theme="light" desiredWidth="50rem"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
Consumers |
||||
<div class="float-right"> |
</ng-container> |
||||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Event Consumers (CTRL + SHIFT + R)"> |
|
||||
<i class="icon-reset"></i> Refresh |
|
||||
</button> |
|
||||
|
|
||||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
<ng-container menu> |
||||
</div> |
<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> |
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
||||
</div> |
</ng-container> |
||||
|
|
||||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
<ng-container content> |
||||
<i class="icon-close"></i> |
<table class="table table-items table-fixed"> |
||||
</a> |
<thead> |
||||
</div> |
<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"> |
<tbody> |
||||
<div class="panel-content panel-content-scroll"> |
<ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers"> |
||||
<table class="table table-items table-fixed"> |
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0"> |
||||
<thead> |
<td class="auto-auto"> |
||||
<tr> |
<span class="truncate"> |
||||
<th class="cell-auto"> |
<i class="faulted-icon icon icon-bug" (click)="showError(eventConsumer)" [class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"></i> |
||||
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> |
|
||||
|
|
||||
{{eventConsumer.name}} |
{{eventConsumer.name}} |
||||
</span> |
</span> |
||||
</td> |
</td> |
||||
<td class="cell-auto-right"> |
<td class="cell-auto-right"> |
||||
<span>{{eventConsumer.position}}</span> |
<span>{{eventConsumer.position}}</span> |
||||
</td> |
</td> |
||||
<td class="cell-actions-lg"> |
<td class="cell-actions-lg"> |
||||
<button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer"> |
<button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer"> |
||||
<i class="icon icon-reset"></i> |
<i class="icon icon-reset"></i> |
||||
</button> |
</button> |
||||
<button class="btn btn-link" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer"> |
<button class="btn btn-link" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer"> |
||||
<i class="icon icon-play"></i> |
<i class="icon icon-play"></i> |
||||
</button> |
</button> |
||||
<button class="btn btn-link" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer"> |
<button class="btn btn-link" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer"> |
||||
<i class="icon icon-pause"></i> |
<i class="icon icon-pause"></i> |
||||
</button> |
</button> |
||||
</td> |
</td> |
||||
</tr> |
</tr> |
||||
<tr class="spacer"></tr> |
<tr class="spacer"></tr> |
||||
</ng-template> |
</ng-template> |
||||
</tbody> |
</tbody> |
||||
</table> |
</table> |
||||
</div> |
</ng-container> |
||||
</div> |
|
||||
</sqx-panel> |
</sqx-panel> |
||||
|
|
||||
<div class="modal" *sqxModalView="eventConsumerErrorDialog;onRoot:true" @fade> |
<sqx-modal-dialog *sqxModalView="eventConsumerErrorDialog;onRoot:true" (close)="eventConsumerErrorDialog.hide()"> |
||||
<div class="modal-backdrop"></div> |
<ng-container #title> |
||||
<div class="modal-dialog"> |
Error |
||||
<div class="modal-content"> |
</ng-container> |
||||
<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> |
|
||||
|
|
||||
<div class="modal-body"> |
<ng-container #content> |
||||
<textarea readonly class="form-control error-message">{{eventConsumerError}}</textarea> |
<textarea readonly class="form-control error-message">{{eventConsumerError}}</textarea> |
||||
</div> |
</ng-container> |
||||
</div> |
</sqx-modal-dialog> |
||||
</div> |
|
||||
</div> |
|
||||
@ -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> |
<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"/> |
<input style="display:none" type="password" name="foilautofill"/> |
||||
|
|
||||
<sqx-panel desiredWidth="26rem"> |
<sqx-panel desiredWidth="26rem" isBlank="true"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
<ng-container *ngIf="usersState.selectedUser | async; else noUser"> |
||||
<div class="float-right"> |
Edit User |
||||
<button type="submit" class="btn btn-primary" title="CTRL + S"> |
</ng-container> |
||||
Save |
|
||||
</button> |
<ng-template #noUser> |
||||
</div> |
New User |
||||
|
</ng-template> |
||||
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> |
</ng-container> |
||||
|
|
||||
<h3 class="panel-title" *ngIf="isNewMode"> |
<ng-container menu> |
||||
New User |
<button type="submit" class="btn btn-primary" title="CTRL + S"> |
||||
</h3> |
Save |
||||
<h3 class="panel-title" *ngIf="!isNewMode"> |
</button> |
||||
Edit User |
|
||||
</h3> |
|
||||
</div> |
|
||||
|
|
||||
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> |
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> |
||||
|
</ng-container> |
||||
|
|
||||
<a class="panel-close" sqxParentLink> |
<ng-container content> |
||||
<i class="icon-close"></i> |
<sqx-form-error [error]="userForm.error | async"></sqx-form-error> |
||||
</a> |
|
||||
</div> |
|
||||
|
|
||||
<div class="panel-main"> |
<div class="form-group"> |
||||
<div class="panel-content panel-content-blank"> |
<label for="email">Email</label> |
||||
<div *ngIf="userFormError"> |
|
||||
<div class="form-alert form-alert-error" [innerHTML]="userFormError"></div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="form-group"> |
<sqx-control-errors for="email" [submitted]="userForm.submitted | async"></sqx-control-errors> |
||||
<label for="email">Email</label> |
|
||||
|
|
||||
<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> |
||||
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" autocomplete="false" /> |
<div class="form-group"> |
||||
</div> |
<label for="displayName">Display Name</label> |
||||
<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" /> |
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" /> |
||||
</div> |
</div> |
||||
|
|
||||
<div class="form-group form-group-password" [class.hidden]="user.id === ctx.userId"> |
<div class="form-group form-group-password" [class.hidden]="usersState.isCurrentUser | async"> |
||||
<div class="form-group"> |
<div class="form-group"> |
||||
<label for="password">Password</label> |
<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" /> |
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="false" /> |
||||
</div> |
</div> |
||||
|
|
||||
<div class="form-group"> |
<div class="form-group"> |
||||
<label for="password">Confirm Password</label> |
<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" /> |
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="false" /> |
||||
</div> |
|
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
</div> |
</ng-container> |
||||
</sqx-panel> |
</sqx-panel> |
||||
</form> |
</form> |
||||
@ -1,107 +1,90 @@ |
|||||
<sqx-title message="User Management"></sqx-title> |
<sqx-title message="User Management"></sqx-title> |
||||
|
|
||||
<sqx-panel desiredWidth="50rem"> |
<sqx-panel desiredWidth="50rem" contentClass="grid"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
Users |
||||
<div class="float-right"> |
</ng-container> |
||||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)"> |
|
||||
<i class="icon-reset"></i> Refresh |
|
||||
</button> |
|
||||
|
|
||||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
<ng-container menu> |
||||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)"> |
||||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> |
<i class="icon-reset"></i> Refresh |
||||
|
</button> |
||||
|
|
||||
<form class="form-inline" (ngSubmit)="search()"> |
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
||||
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> |
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
||||
</form> |
<sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut> |
||||
|
|
||||
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + M)"> |
<form class="form-inline" (ngSubmit)="search()"> |
||||
<i class="icon-plus"></i> New |
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> |
||||
</button> |
</form> |
||||
</div> |
|
||||
|
|
||||
<h3 class="panel-title">Users</h3> |
|
||||
</div> |
|
||||
|
|
||||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)"> |
||||
<i class="icon-close"></i> |
<i class="icon-plus"></i> New |
||||
</a> |
</button> |
||||
</div> |
</ng-container> |
||||
|
|
||||
<div class="panel-main"> |
<ng-container content> |
||||
<div class="panel-content grid"> |
<div class="grid-header"> |
||||
<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"> |
<table class="table table-items table-fixed"> |
||||
<thead> |
<tbody> |
||||
<tr> |
<ng-template ngFor let-user [ngForOf]="usersState.users | async" [ngForTrackBy]="trackByUser"> |
||||
<th class="cell-user"> |
<tr [routerLink]="user.id" routerLinkActive="active"> |
||||
|
<td class="cell-user"> |
||||
</th> |
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" /> |
||||
<th class="cell-auto"> |
</td> |
||||
Name |
<td class="cell-auto"> |
||||
</th> |
<span class="user-name table-cell">{{user.displayName}}</span> |
||||
<th class="cell-auto"> |
</td> |
||||
Email |
<td class="cell-auto"> |
||||
</th> |
<span class="user-email table-cell">{{user.email}}</span> |
||||
<th class="cell-actions"> |
</td> |
||||
Actions |
<td class="cell-actions"> |
||||
</th> |
<ng-container *ngIf="user.id !== authState.user?.id"> |
||||
</tr> |
<button class="btn btn-link" (click)="lock(user); $event.stopPropagation();" *ngIf="!user.isLocked" title="Lock User"> |
||||
</thead> |
<i class="icon icon-unlocked"></i> |
||||
</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"> |
|
||||
|
|
||||
</button> |
</button> |
||||
</td> |
<button class="btn btn-link" (click)="unlock(user); $event.stopPropagation();" *ngIf="user.isLocked" title="Unlock User"> |
||||
</tr> |
<i class="icon icon-lock"></i> |
||||
<tr class="spacer"></tr> |
</button> |
||||
</ng-template> |
</ng-container> |
||||
</tbody> |
<button *ngIf="user.id === authState.user?.id" class="btn btn-link invisible"> |
||||
</table> |
|
||||
</div> |
</button> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr class="spacer"></tr> |
||||
|
</ng-template> |
||||
|
</tbody> |
||||
|
</table> |
||||
</div> |
</div> |
||||
|
</div> |
||||
|
|
||||
<div class="grid-footer clearfix" *ngIf="usersPager.numberOfItems > 0"> |
<div class="grid-footer"> |
||||
<div class="float-right pagination"> |
<sqx-pager [pager]="usersState.usersPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager> |
||||
<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> |
</div> |
||||
</div> |
</ng-container> |
||||
</sqx-panel> |
</sqx-panel> |
||||
|
|
||||
<router-outlet></router-outlet> |
<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"> |
<sqx-panel theme="dark" desiredWidth="12rem"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
API |
||||
<h3 class="panel-title">API</h3> |
</ng-container> |
||||
</div> |
|
||||
|
|
||||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
<ng-container content> |
||||
<i class="icon-close"></i> |
<ul class="nav nav-panel nav-dark flex-column"> |
||||
</a> |
<li class="nav-item"> |
||||
</div> |
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> |
||||
|
GraphQL |
||||
<div class="panel-main"> |
<i class="icon-angle-right"></i> |
||||
<div class="panel-content"> |
</a> |
||||
<ul class="nav nav-panel nav-dark flex-column"> |
</li> |
||||
<li class="nav-item"> |
<li class="nav-item"> |
||||
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> |
<a class="nav-link" href="/api/content/{{appsState.appName}}/docs" target="_blank"> |
||||
GraphQL |
Swagger |
||||
<i class="icon-angle-right"></i> |
</a> |
||||
</a> |
</li> |
||||
</li> |
</ul> |
||||
<li class="nav-item"> |
</ng-container> |
||||
<a class="nav-link" href="/api/content/{{ctx.appName}}/docs" target="_blank"> |
|
||||
Swagger |
|
||||
</a> |
|
||||
</li> |
|
||||
</ul> |
|
||||
</div> |
|
||||
</div> |
|
||||
</sqx-panel> |
</sqx-panel> |
||||
|
|
||||
<router-outlet></router-outlet> |
<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="*"> |
<sqx-panel desiredWidth="*" isFullSize="true"> |
||||
<div #graphiQLContainer></div> |
<div inner #graphiQLContainer></div> |
||||
</sqx-panel> |
</sqx-panel> |
||||
@ -1,70 +1,24 @@ |
|||||
<sqx-title message="{app} | Assets" parameter1="app" [value1]="ctx.appName"></sqx-title> |
<sqx-title message="{app} | Assets" parameter1="app" [value1]="appsState.appName"></sqx-title> |
||||
|
|
||||
<sqx-panel desiredWidth="60rem"> |
<sqx-panel desiredWidth="*"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
Assets |
||||
<div class="float-right"> |
</ng-container> |
||||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
|
||||
<i class="icon-reset"></i> Refresh |
<ng-container menu> |
||||
</button> |
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
||||
|
<i class="icon-reset"></i> Refresh |
||||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
</button> |
||||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
|
||||
|
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
||||
<form class="form-inline" (ngSubmit)="search()"> |
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
||||
<input class="form-control" #inputFind [formControl]="assetsFilter" placeholder="Search for assets" /> |
|
||||
</form> |
<form class="form-inline" (ngSubmit)="search()"> |
||||
</div> |
<input class="form-control" #inputFind [formControl]="assetsFilter" placeholder="Search for assets" /> |
||||
|
</form> |
||||
<h3 class="panel-title">Assets</h3> |
</ng-container> |
||||
</div> |
|
||||
|
<ng-container content> |
||||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
<sqx-assets-list [state]="assetsState"></sqx-assets-list> |
||||
<i class="icon-close"></i> |
</ng-container> |
||||
</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-panel> |
</sqx-panel> |
||||
|
|||||
@ -1,50 +1,2 @@ |
|||||
@import '_vars'; |
@import '_vars'; |
||||
@import '_mixins'; |
@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; |
|
||||
} |
|
||||
@ -1,29 +1,21 @@ |
|||||
<sqx-panel desiredWidth="16rem"> |
<sqx-panel desiredWidth="16rem" isBlank="true"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
Activity |
||||
<h3 class="panel-title">Activity</h3> |
</ng-container> |
||||
</div> |
|
||||
|
|
||||
<a class="panel-close" sqxParentLink> |
|
||||
<i class="icon-close"></i> |
|
||||
</a> |
|
||||
</div> |
|
||||
|
|
||||
<div class="panel-main"> |
<ng-container content> |
||||
<div class="panel-content panel-content-blank"> |
<div *ngFor="let event of events | async" class="event"> |
||||
<div *ngFor="let event of events | async" class="event"> |
<div class="event-left"> |
||||
<div class="event-left"> |
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" /> |
||||
<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> |
||||
<div class="event-main"> |
<div class="event-created">{{event.created | sqxFromNow}}</div> |
||||
<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> |
|
||||
|
|
||||
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a> |
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a> |
||||
</div> |
</div> |
||||
</div> |
|
||||
</div> |
</div> |
||||
</div> |
</ng-container> |
||||
</sqx-panel> |
</sqx-panel> |
||||
@ -1,70 +1,60 @@ |
|||||
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title> |
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title> |
||||
|
|
||||
<form [formGroup]="contentForm" (ngSubmit)="saveAndPublish()"> |
<form [formGroup]="contentForm" (ngSubmit)="saveAndPublish()"> |
||||
<sqx-panel desiredWidth="53rem"> |
<sqx-panel desiredWidth="53rem" showSidebar="true"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
<ng-container *ngIf="isNewMode"> |
||||
<div class="float-right" *ngIf="!content || content.status !== 'Archived'"> |
New Content |
||||
<span *ngIf="isNewMode"> |
</ng-container> |
||||
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S"> |
<ng-container *ngIf="!isNewMode && content.status !== 'Archived'"> |
||||
Save as Draft |
Edit Content |
||||
</button> |
</ng-container> |
||||
|
<ng-container *ngIf="!isNewMode && content.status === 'Archived'"> |
||||
|
Show Content |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
|
||||
<button type="submit" class="btn btn-primary"> |
<ng-container menu> |
||||
Save and Publish |
<ng-container *ngIf="isNewMode; else notNew"> |
||||
</button> |
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S"> |
||||
</span> |
Save as Draft |
||||
<span *ngIf="!isNewMode"> |
</button> |
||||
<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> |
|
||||
|
|
||||
<a class="panel-close" sqxParentLink> |
<button type="submit" class="btn btn-primary"> |
||||
<i class="icon-close"></i> |
Save and Publish |
||||
</a> |
</button> |
||||
</div> |
</ng-container> |
||||
|
<ng-template #notNew> |
||||
|
<button type="submit" class="btn btn-primary" title="CTRL + S"> |
||||
|
Save |
||||
|
</button> |
||||
|
</ng-template> |
||||
|
|
||||
<div class="panel-main"> |
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut> |
||||
<div class="panel-content panel-content-scroll"> |
</ng-container> |
||||
<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 *ngFor="let field of schema.fields"> |
<ng-container content> |
||||
<sqx-content-field [field]="field" [fieldForm]="contentForm.controls[field.name]" [languages]="languages" [contentFormSubmitted]="contentFormSubmitted"></sqx-content-field> |
<div class="panel-alert panel-alert-danger" *ngIf="contentOld"> |
||||
|
<div class="float-right"> |
||||
|
<a class="force" (click)="showLatest()">View latest</a> |
||||
</div> |
</div> |
||||
|
Viewing <strong>{{content.lastModifiedBy | sqxUserNameRef:null}}'s</strong> changes of {{content.lastModified | sqxShortDate}}. |
||||
</div> |
</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"> |
<div *ngFor="let field of schema.fields"> |
||||
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. |
<sqx-content-field [field]="field" [fieldForm]="contentForm.controls[field.name]" [languages]="languages" [contentFormSubmitted]="contentFormSubmitted"></sqx-content-field> |
||||
</sqx-onboarding-tooltip> |
|
||||
</div> |
</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> |
</sqx-panel> |
||||
</form> |
</form> |
||||
|
|
||||
|
|||||
@ -1,208 +1,184 @@ |
|||||
<sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title> |
<sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title> |
||||
|
|
||||
<sqx-panel [desiredWidth]="isReadOnly ? '40rem' : '60rem'"> |
<sqx-panel [desiredWidth]="isReadOnly ? '40rem' : '60rem'" contentClass="grid"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
<ng-container *ngIf="!isReadOnly && !isArchive"> |
||||
<div class="float-right"> |
Contents |
||||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)"> |
</ng-container> |
||||
<i class="icon-reset"></i> Refresh |
<ng-container *ngIf="isArchive"> |
||||
</button> |
Archive |
||||
|
</ng-container> |
||||
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
<ng-container *ngIf="isReadOnly"> |
||||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
References |
||||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()" *ngIf="!isReadOnly"></sqx-shortcut> |
</ng-container> |
||||
|
</ng-container> |
||||
<form class="form-inline" (ngSubmit)="search()"> |
|
||||
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" placeholder="Search for content" /> |
<ng-container menu> |
||||
|
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)"> |
||||
<a class="expand-search" (click)="searchModal.toggle()" #archive> |
<i class="icon-reset"></i> Refresh |
||||
<i class="icon-caret-down"></i> |
</button> |
||||
</a> |
|
||||
</form> |
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
||||
|
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
||||
<sqx-onboarding-tooltip id="contentArchive" [for]="archive" position="bottomRight" after="60000"> |
<sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()" *ngIf="!isReadOnly"></sqx-shortcut> |
||||
Click this icon to show the advanced search menu and to show the archive! |
|
||||
</sqx-onboarding-tooltip> |
<form class="form-inline" (ngSubmit)="search()"> |
||||
|
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" placeholder="Search for content" /> |
||||
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000"> |
|
||||
Search for content using full text search over all fields and languages! |
<a class="expand-search" (click)="searchModal.toggle()" #archive> |
||||
</sqx-onboarding-tooltip> |
<i class="icon-caret-down"></i> |
||||
|
</a> |
||||
<div class="dropdown-menu" *sqxModalView="searchModal" [sqxModalTarget]="inputFind"> |
</form> |
||||
<sqx-search-form |
|
||||
[canArchive]="!isReadOnly" |
<sqx-onboarding-tooltip id="contentArchive" [for]="archive" position="bottomRight" after="60000"> |
||||
(queryChanged)="contentsFilter.setValue($event, { emitEvent: false })" |
Click this icon to show the advanced search menu and to show the archive! |
||||
[query]="contentsFilter.value" |
</sqx-onboarding-tooltip> |
||||
(archivedChanged)="updateArchive($event)" |
|
||||
[archived]="isArchive"> |
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000"> |
||||
</sqx-search-form> |
Search for content using full text search over all fields and languages! |
||||
</div> |
</sqx-onboarding-tooltip> |
||||
|
|
||||
<span *ngIf="!isReadOnly && languages.length > 1"> |
<div class="dropdown-menu" *sqxModalView="searchModal" [sqxModalTarget]="inputFind"> |
||||
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector> |
<sqx-search-form |
||||
</span> |
[canArchive]="!isReadOnly" |
||||
|
(queryChanged)="contentsFilter.setValue($event, { emitEvent: false })" |
||||
<button *ngIf="!isReadOnly" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)"> |
[query]="contentsFilter.value" |
||||
<i class="icon-plus"></i> New |
(archivedChanged)="updateArchive($event)" |
||||
</button> |
[archived]="isArchive"> |
||||
</div> |
</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"> |
<button class="btn btn-secondary" (click)="publishSelected()" *ngIf="canPublish"> |
||||
Contents |
Publish |
||||
</h3> |
</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 |
Archive |
||||
</h3> |
</button> |
||||
|
|
||||
<h3 class="panel-title" *ngIf="isReadOnly"> |
<button class="btn btn-secondary" (click)="restoreSelected()" *ngIf="isArchive"> |
||||
References |
Restore |
||||
</h3> |
</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> |
||||
|
|
||||
<a class="panel-close" sqxParentLink> |
<div class="grid-content"> |
||||
<i class="icon-close"></i> |
<div sqxIgnoreScrollbar> |
||||
</a> |
<table class="table table-items table-fixed" *ngIf="contentItems" > |
||||
</div> |
<tbody *ngIf="!isReadOnly"> |
||||
|
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackByContent"> |
||||
<div class="panel-main"> |
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active" |
||||
<div class="panel-content grid"> |
[language]="languageSelected" |
||||
<div class="grid-header"> |
[schemaFields]="contentFields" |
||||
<table class="table table-items table-fixed" *ngIf="contentItems"> |
[schema]="schema" |
||||
<thead> |
[selected]="isItemSelected(content)" |
||||
<tr> |
(selectedChange)="selectItem(content, $event)" |
||||
<th class="cell-select" *ngIf="!isReadOnly"> |
(unpublishing)="unpublishContent(content)" |
||||
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" /> |
(publishing)="publishContent(content)" |
||||
</th> |
(archiving)="archiveContent(content)" |
||||
<th class="cell-auto" *ngFor="let field of contentFields"> |
(restoring)="restoreContent(content)" |
||||
<span class="field">{{field.displayName}}</span> |
(deleting)="deleteContent(content)" |
||||
</th> |
(saved)="onContentSaved(content, $event)"></tr> |
||||
<th class="cell-time"> |
<tr class="spacer"></tr> |
||||
Updated |
</ng-template> |
||||
</th> |
</tbody> |
||||
<th class="cell-user"> |
|
||||
By |
<tbody *ngIf="isReadOnly"> |
||||
</th> |
<ng-template ngFor let-content [ngForOf]="contentItems"> |
||||
<th class="cell-actions" *ngIf="!isReadOnly"> |
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)" |
||||
Actions |
[language]="languageSelected" |
||||
</th> |
[schemaFields]="contentFields" |
||||
</tr> |
[schema]="schema" |
||||
</thead> |
isReadOnly="true"></tr> |
||||
|
<tr class="spacer"></tr> |
||||
|
</ng-template> |
||||
|
</tbody> |
||||
</table> |
</table> |
||||
</div> |
</div> |
||||
|
</div> |
||||
|
|
||||
<div class="selection" *ngIf="selectionCount > 0"> |
<div class="grid-footer"> |
||||
{{selectionCount}} items selected: |
<sqx-pager [pager]="contentsPager"></sqx-pager> |
||||
|
|
||||
<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> |
</div> |
||||
</div> |
</ng-container> |
||||
</sqx-panel> |
</sqx-panel> |
||||
|
|
||||
<router-outlet></router-outlet> |
<router-outlet></router-outlet> |
||||
|
|
||||
<div class="modal" *sqxModalView="dueTimeDialog;onRoot:true"> |
<ng-container *sqxModalView="dueTimeDialog;onRoot:true"> |
||||
<div class="modal-backdrop"></div> |
<sqx-modal-dialog (close)="cancelStatusChange()"> |
||||
<div class="modal-dialog"> |
<ng-container title> |
||||
<div class="modal-content"> |
{{dueTimeAction}} content item(s) |
||||
<div class="modal-header"> |
</ng-container> |
||||
<h4 class="modal-title">{{dueTimeAction}} content item(s)</h4> |
|
||||
|
<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> |
||||
|
|
||||
<div class="modal-body"> |
<div class="form-check"> |
||||
<div class="form-check"> |
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled"> |
||||
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately"> |
<label class="form-check-label" for="scheduled"> |
||||
<label class="form-check-label" for="immediately"> |
{{dueTimeAction}} content item(s) at a later point date and time. |
||||
{{dueTimeAction}} content item(s) immediately. |
</label> |
||||
</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> |
</div> |
||||
|
|
||||
<div class="modal-footer"> |
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor> |
||||
<div class="clearfix"> |
</ng-container> |
||||
<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 footer> |
||||
</div> |
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button> |
||||
</div> |
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()">Confirm</button> |
||||
</div> |
</ng-container> |
||||
</div> |
</sqx-modal-dialog> |
||||
</div> |
</ng-container> |
||||
|
|||||
@ -1,35 +1,27 @@ |
|||||
<sqx-title message="{app} | Schemas" parameter1="app" [value1]="ctx.appName"></sqx-title> |
<sqx-title message="{app} | Schemas" parameter1="app" [value1]="ctx.appName"></sqx-title> |
||||
|
|
||||
<sqx-panel theme="dark" desiredWidth="16rem"> |
<sqx-panel theme="dark" desiredWidth="16rem" showSecondHeader="true"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
Schemas |
||||
<h3 class="panel-title">Schemas</h3> |
</ng-container> |
||||
</div> |
|
||||
|
|
||||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
|
||||
<i class="icon-close"></i> |
|
||||
</a> |
|
||||
|
|
||||
<div class="panel-header-row"> |
<ng-container secondHeader> |
||||
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> |
||||
|
|
||||
<div class="search-form"> |
<div class="search-form"> |
||||
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" /> |
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" /> |
||||
|
|
||||
<i class="icon-search"></i> |
<i class="icon-search"></i> |
||||
</div> |
|
||||
</div> |
</div> |
||||
</div> |
</ng-container> |
||||
|
|
||||
<div class="panel-main"> |
<ng-container content> |
||||
<div class="panel-content"> |
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column"> |
||||
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column"> |
<li class="nav-item" *ngFor="let schema of schemasFiltered | async"> |
||||
<li class="nav-item" *ngFor="let schema of schemasFiltered | async"> |
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a> |
||||
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a> |
</li> |
||||
</li> |
</ul> |
||||
</ul> |
</ng-container> |
||||
</div> |
|
||||
</div> |
|
||||
</sqx-panel> |
</sqx-panel> |
||||
|
|
||||
<router-outlet></router-outlet> |
<router-outlet></router-outlet> |
||||
@ -1,116 +1,97 @@ |
|||||
<sqx-title message="{app} | Rules Events" parameter1="app" [value1]="ctx.appName"></sqx-title> |
<sqx-title message="{app} | Rules Events" parameter1="app" [value1]="ctx.appName"></sqx-title> |
||||
|
|
||||
<sqx-panel desiredWidth="63rem"> |
<sqx-panel desiredWidth="63rem"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
Events |
||||
<div class="float-right"> |
</ng-container> |
||||
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Events (CTRL + SHIFT + R)"> |
|
||||
<i class="icon-reset"></i> Refresh |
<ng-container menu> |
||||
</button> |
<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> |
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
||||
</div> |
</ng-container> |
||||
|
|
||||
<h3 class="panel-title">Events</h3> |
<ng-container content> |
||||
</div> |
<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"> |
</th> |
||||
<i class="icon-close"></i> |
</tr> |
||||
</a> |
</thead> |
||||
</div> |
|
||||
|
|
||||
<div class="panel-main"> |
<tbody> |
||||
<div class="panel-content panel-content-scroll"> |
<ng-template ngFor let-event [ngForOf]="eventsItems"> |
||||
<table class="table table-items table-fixed"> |
<tr [class.expanded]="selectedEventId === event.id"> |
||||
<thead> |
<td class="cell-label"> |
||||
<tr> |
<span class="badge badge-pill badge-{{getBadgeClass(event.jobResult)}}">{{event.jobResult}}</span> |
||||
<th class="cell-label"> |
</td> |
||||
Status |
<td class="cell-40"> |
||||
</th> |
<span class="truncate">{{event.eventName}}</span> |
||||
<th class="cell-40"> |
</td> |
||||
Event |
<td class="cell-60"> |
||||
</th> |
<span class="truncate">{{event.description}}</span> |
||||
<th class="cell-60"> |
</td> |
||||
Description |
<td class="cell-time"> |
||||
</th> |
<small class="item-modified">{{event.created | sqxFromNow}}</small> |
||||
<th class="cell-time"> |
</td> |
||||
Created |
<td class="cell-actions"> |
||||
</th> |
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="selectedEventId === event.id" (click)="selectEvent(event.id)"> |
||||
<th class="cell-actions"> |
<i class="icon-settings"></i> |
||||
|
</button> |
||||
</th> |
</td> |
||||
</tr> |
</tr> |
||||
</thead> |
<tr *ngIf="selectedEventId === event.id"> |
||||
|
<td colspan="5"> |
||||
<tbody> |
<div class="event-header"> |
||||
<ng-template ngFor let-event [ngForOf]="eventsItems"> |
<h3>Last Invocation</h3> |
||||
<tr [class.expanded]="selectedEventId === event.id"> |
</div> |
||||
<td class="cell-label"> |
|
||||
<span class="badge badge-pill badge-{{getBadgeClass(event.jobResult)}}">{{event.jobResult}}</span> |
<div class="row event-stats"> |
||||
</td> |
<div class="col-3"> |
||||
<td class="cell-40"> |
<span class="badge badge-pill badge-{{getBadgeClass(event.result)}}">{{event.result}}</span> |
||||
<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> |
|
||||
</div> |
</div> |
||||
|
<div class="col-3"> |
||||
<div class="row event-stats"> |
Attempts: {{event.numCalls}} |
||||
<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> |
</div> |
||||
<div class="row"> |
<div class="col-3"> |
||||
<div class="col-12"> |
Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container> |
||||
<textarea class="event-dump form-control" readonly>{{event.lastDump}}</textarea> |
|
||||
</div> |
|
||||
</div> |
</div> |
||||
</td> |
<div class="col-3 text-right"> |
||||
</tr> |
<button class="btn btn-success btn-sm" (click)="enqueueEvent(event)"> |
||||
<tr class="spacer"></tr> |
Enqueue |
||||
</ng-template> |
</button> |
||||
</tbody> |
</div> |
||||
</table> |
</div> |
||||
|
<div class="row"> |
||||
<div class="clearfix" *ngIf="eventsPager.numberOfItems > 0"> |
<div class="col-12"> |
||||
<div class="float-right pagination"> |
<textarea class="event-dump form-control" readonly>{{event.lastDump}}</textarea> |
||||
<span class="pagination-text">{{eventsPager.itemFirst}}-{{eventsPager.itemLast}} of {{eventsPager.numberOfItems}}</span> |
</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()"> |
<sqx-pager [pager]="eventsPager"></sqx-pager> |
||||
<i class="icon-angle-left"></i> |
</ng-container> |
||||
</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-panel> |
</sqx-panel> |
||||
|
|
||||
<router-outlet></router-outlet> |
<router-outlet></router-outlet> |
||||
@ -1,127 +1,122 @@ |
|||||
<div class="modal-dialog modal-lg"> |
<sqx-modal-dialog large="true" (close)="cancel()"> |
||||
<div class="modal-content"> |
<ng-container title> |
||||
<div class="modal-header"> |
<ng-container *ngIf="mode === 'EditTrigger'"> |
||||
<h4 class="modal-title" *ngIf="mode === 'EditTrigger'"> |
Edit Trigger |
||||
Edit Trigger |
</ng-container> |
||||
</h4> |
<ng-container *ngIf="mode === 'EditAction'"> |
||||
<h4 class="modal-title" *ngIf="mode === 'EditAction'"> |
Edit Action |
||||
Edit Action |
</ng-container> |
||||
</h4> |
<ng-container *ngIf="mode === 'Wizard' && step === 1"> |
||||
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 1"> |
Step 1 of 4: Select Trigger |
||||
Step 1 of 4: Select Trigger |
</ng-container> |
||||
</h4> |
<ng-container *ngIf="mode === 'Wizard' && step === 2"> |
||||
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 2"> |
Step 2 of 4: Configure Trigger |
||||
Step 2 of 4: Configure Trigger |
</ng-container> |
||||
</h4> |
<ng-container *ngIf="mode === 'Wizard' && step === 3"> |
||||
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 3"> |
Step 3 of 4: Select Action |
||||
Step 3 of 4: Select Action |
</ng-container> |
||||
</h4> |
<ng-container *ngIf="mode === 'Wizard' && step === 4"> |
||||
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 4"> |
Step 4 of 4: Configure Action |
||||
Step 4 of 4: Configure Action |
</ng-container> |
||||
</h4> |
</ng-container> |
||||
|
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="cancel()"> |
|
||||
<span aria-hidden="true">×</span> |
|
||||
</button> |
|
||||
</div> |
|
||||
|
|
||||
<div class="modal-body"> |
<ng-container content> |
||||
<div *ngIf="step === 1"> |
<ng-container *ngIf="step === 1"> |
||||
<span *ngFor="let trigger of ruleTriggers | sqxKeys" class="rule-element rule-element-{{trigger}}" (click)="selectTriggerType(trigger)"> |
<span *ngFor="let trigger of ruleTriggers | sqxKeys" class="rule-element rule-element-{{trigger}}" (click)="selectTriggerType(trigger)"> |
||||
<span class="rule-element-icon"> |
<span class="rule-element-icon"> |
||||
<i class="icon-trigger-{{trigger}}"></i> |
<i class="icon-trigger-{{trigger}}"></i> |
||||
</span> |
|
||||
<span class="rule-element-text"> |
|
||||
{{ruleTriggers[trigger].name}} |
|
||||
</span> |
|
||||
</span> |
</span> |
||||
</div> |
<span class="rule-element-text"> |
||||
|
{{ruleTriggers[trigger].name}} |
||||
<div *ngIf="step === 2 && schemas" class="modal-form"> |
</span> |
||||
<div [ngSwitch]="triggerType"> |
</span> |
||||
<div *ngSwitchCase="'AssetChanged'"> |
</ng-container> |
||||
<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> |
|
||||
|
|
||||
<div *ngIf="step === 3"> |
<ng-container *ngIf="step === 2 && schemas"> |
||||
<span *ngFor="let action of ruleActions | sqxKeys" class="rule-element rule-element-{{action}}" (click)="selectActionType(action)"> |
<ng-container [ngSwitch]="triggerType"> |
||||
<span class="rule-element-icon"> |
<ng-container *ngSwitchCase="'AssetChanged'"> |
||||
<i class="icon-action-{{action}}"></i> |
<sqx-asset-changed-trigger #triggerControl |
||||
</span> |
[trigger]="trigger" |
||||
<span class="rule-element-text"> |
(triggerChanged)="selectTrigger($event)"> |
||||
{{ruleActions[action].name}} |
</sqx-asset-changed-trigger> |
||||
</span> |
</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> |
</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"> |
<ng-container *ngIf="mode !== 'Wizard' && step === 2"> |
||||
<div [ngSwitch]="actionType"> |
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
||||
<div *ngSwitchCase="'Algolia'"> |
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Save</button> |
||||
<sqx-algolia-action #actionControl |
</ng-container> |
||||
[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> |
|
||||
|
|
||||
<div class="clearfix" *ngIf="step === 4"> |
<ng-container *ngIf="step === 4"> |
||||
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button> |
<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> |
<button type="submit" class="float-right btn btn-primary" (click)="actionControl.save()">Save</button> |
||||
</div> |
</ng-container> |
||||
</div> |
</ng-container> |
||||
</div> |
</sqx-modal-dialog> |
||||
</div> |
|
||||
@ -1,107 +1,97 @@ |
|||||
<sqx-title message="{app} | Rules" parameter1="app" [value1]="ctx.appName"></sqx-title> |
<sqx-title message="{app} | Rules" parameter1="app" [value1]="ctx.appName"></sqx-title> |
||||
|
|
||||
<sqx-panel desiredWidth="54rem"> |
<sqx-panel desiredWidth="54rem" showSidebar="true"> |
||||
<div class="panel-header"> |
<ng-container title> |
||||
<div class="panel-title-row"> |
Rules |
||||
<div class="float-right"> |
</ng-container> |
||||
<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> |
<ng-container menu> |
||||
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> |
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)"> |
||||
|
<i class="icon-reset"></i> Refresh |
||||
<button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)"> |
</button> |
||||
<i class="icon-plus"></i> New |
|
||||
</button> |
|
||||
</div> |
|
||||
|
|
||||
<h3 class="panel-title">Rules</h3> |
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> |
||||
</div> |
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> |
||||
|
|
||||
<a class="panel-close" sqxParentLink isLazyLoaded="true"> |
<button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)"> |
||||
<i class="icon-close"></i> |
<i class="icon-plus"></i> New |
||||
</a> |
</button> |
||||
</div> |
</ng-container> |
||||
|
|
||||
<div class="panel-main"> |
<ng-container content> |
||||
<div class="panel-content panel-content-scroll"> |
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0"> |
||||
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0"> |
No Rule created yet. |
||||
No Rule created yet. |
</div> |
||||
</div> |
|
||||
|
|
||||
<table class="table table-items table-fixed" *ngIf="rules && rules.length > 0"> |
<table class="table table-items table-fixed" *ngIf="rules && rules.length > 0"> |
||||
<tbody> |
<tbody> |
||||
<ng-template ngFor let-rule [ngForOf]="rules"> |
<ng-template ngFor let-rule [ngForOf]="rules"> |
||||
<tr> |
<tr> |
||||
<td class="cell-separator"> |
<td class="cell-separator"> |
||||
<h3>If</h3> |
<h3>If</h3> |
||||
</td> |
</td> |
||||
<td class="cell-auto"> |
<td class="cell-auto"> |
||||
<span class="rule-element rule-element-{{rule.triggerType}}" (click)="editTrigger(rule)"> |
<span class="rule-element rule-element-{{rule.triggerType}}" (click)="editTrigger(rule)"> |
||||
<span class="rule-element-icon"> |
<span class="rule-element-icon"> |
||||
<i class="icon-trigger-{{rule.triggerType}}"></i> |
<i class="icon-trigger-{{rule.triggerType}}"></i> |
||||
</span> |
|
||||
<span class="rule-element-text"> |
|
||||
{{ruleTriggers[rule.triggerType].name}} |
|
||||
</span> |
|
||||
</span> |
</span> |
||||
</td> |
<span class="rule-element-text"> |
||||
<td class="cell-separator"> |
{{ruleTriggers[rule.triggerType].name}} |
||||
<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> |
</span> |
||||
</td> |
</span> |
||||
<td class="cell-actions"> |
</td> |
||||
<sqx-toggle [ngModel]="rule.isEnabled" (ngModelChange)="toggleRule(rule)"></sqx-toggle> |
<td class="cell-separator"> |
||||
</td> |
<h3>then</h3> |
||||
<td class="cell-actions"> |
</td> |
||||
<button type="button" class="btn btn-link btn-danger" |
<td class="cell-auto"> |
||||
(sqxConfirmClick)="deleteRule(rule)" |
<span class="rule-element rule-element-{{rule.actionType}}" (click)="editAction(rule)"> |
||||
confirmTitle="Delete rule" |
<span class="rule-element-icon"> |
||||
confirmText="Do you really want to delete the rule?"> |
<i class="icon-action-{{rule.actionType}}"></i> |
||||
<i class="icon-bin2"></i> |
</span> |
||||
</button> |
<span class="rule-element-text"> |
||||
</td> |
{{ruleActions[rule.actionType].name}} |
||||
</tr> |
</span> |
||||
<tr class="spacer"></tr> |
</span> |
||||
</ng-template> |
</td> |
||||
</tbody> |
<td class="cell-actions"> |
||||
</table> |
<sqx-toggle [ngModel]="rule.isEnabled" (ngModelChange)="toggleRule(rule)"></sqx-toggle> |
||||
</div> |
</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"> |
<ng-container sidebar> |
||||
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory> |
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory> |
||||
<i class="icon-time"></i> |
<i class="icon-time"></i> |
||||
</a> |
</a> |
||||
|
|
||||
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp> |
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp> |
||||
<i class="icon-help"></i> |
<i class="icon-help"></i> |
||||
</a> |
</a> |
||||
|
|
||||
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000"> |
<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. |
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> |
</sqx-onboarding-tooltip> |
||||
</div> |
</ng-container> |
||||
</div> |
|
||||
</sqx-panel> |
</sqx-panel> |
||||
|
|
||||
<div class="modal" *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false" @fade> |
<ng-container *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false"> |
||||
<div class="modal-backdrop"></div> |
|
||||
|
|
||||
<sqx-rule-wizard [schemas]="schemas" [rule]="wizardRule" [mode]="wizardMode" |
<sqx-rule-wizard [schemas]="schemas" [rule]="wizardRule" [mode]="wizardMode" |
||||
(updated)="onRuleUpdated($event)" |
(updated)="onRuleUpdated($event)" |
||||
(cancelled)="addRuleDialog.hide()" |
(cancelled)="addRuleDialog.hide()" |
||||
(created)="onRuleCreated($event)"> |
(created)="onRuleCreated($event)"> |
||||
</sqx-rule-wizard> |
</sqx-rule-wizard> |
||||
</div> |
</ng-container> |
||||
|
|
||||
<router-outlet></router-outlet> |
<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