mirror of https://github.com/Squidex/squidex.git
58 changed files with 1132 additions and 465 deletions
@ -0,0 +1,32 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppLanguageDto.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Squidex.Controllers.Api.Apps.Models |
||||
|
{ |
||||
|
public class AppLanguageDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The iso code of the language.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string Iso2Code { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The english name of the language.
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string EnglishName { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates if the language is the master language.
|
||||
|
/// </summary>
|
||||
|
public bool IsMasterLanguage { get; set; } |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.5 KiB |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,80 @@ |
|||||
|
<div class="table-items-row"> |
||||
|
<table class="table table-middle table-sm table-borderless table-fixed client-info"> |
||||
|
<colgroup> |
||||
|
<col style="width: 160px; text-align: right;" /> |
||||
|
<col style="width: 100%" /> |
||||
|
<col style="width: 40px" /> |
||||
|
</colgroup> |
||||
|
|
||||
|
<tr> |
||||
|
<td colspan="2"> |
||||
|
<div class="float-xs-right"> |
||||
|
<button class="btn btn-default" (click)="createToken(client)">Create Token</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="client-name"> |
||||
|
<form *ngIf="isRenaming" class="form-inline" (submit)="rename()"> |
||||
|
<div class="form-group"> |
||||
|
<input type="text" class="form-control" name="client-name" id="client-name" sqxFocusOnInit [(ngModel)]="client.name" /> |
||||
|
</div> |
||||
|
|
||||
|
<button type="submit" class="btn btn-primary">Save</button> |
||||
|
|
||||
|
<a class="btn btn-default" (click)="cancelRename()">Cancel</a> |
||||
|
</form> |
||||
|
|
||||
|
<h3 *ngIf="!isRenaming"> |
||||
|
{{client.name}} <i class="client-edit icon-pencil" (click)="startRename()"></i> |
||||
|
</h3> |
||||
|
</div> |
||||
|
|
||||
|
<div class="client-expires">Expires: {{client.expiresUtc}}</div> |
||||
|
</td> |
||||
|
<td class="client-delete"> |
||||
|
<button type="button" class="btn btn-link btn-danger" (click)="revokeClient(client)"> |
||||
|
<i class="icon-bin"></i> |
||||
|
</button> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>Client Id:</td> |
||||
|
<td> |
||||
|
<input readonly class="form-control" #inputId [attr.value]="appName + ':' + client.id" /> |
||||
|
</td> |
||||
|
<td> |
||||
|
<button type="button" class="btn btn-primary btn-link" (click)="copyId()"> |
||||
|
<i class="icon-copy"></i> |
||||
|
</button> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>Client Secret:</td> |
||||
|
<td> |
||||
|
<input readonly class="form-control" name="inputSecret" [attr.value]="client.secret" /> |
||||
|
</td> |
||||
|
<td> |
||||
|
<button type="button" class="btn btn-primary btn-link" (click)="copySecret()"> |
||||
|
<i class="icon-copy"></i> |
||||
|
</button> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</div> |
||||
|
|
||||
|
<div class="modal" *sqxModalView="modalDialog" [@fade]> |
||||
|
<div class="modal-dialog" role="document"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="modalDialog.hide()"> |
||||
|
<span aria-hidden="true">×</span> |
||||
|
</button> |
||||
|
|
||||
|
<h4 class="modal-title">Access Token</h4> |
||||
|
</div> |
||||
|
|
||||
|
<div class="modal-body"> |
||||
|
<textarea class="form-control access-token">{{appClientToken.tokenType}} {{appClientToken.accessToken}}</textarea> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,42 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.client { |
||||
|
&-info { |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
&-delete { |
||||
|
vertical-align: top; |
||||
|
} |
||||
|
|
||||
|
&-expires { |
||||
|
font-size: .8rem; |
||||
|
} |
||||
|
|
||||
|
&-edit { |
||||
|
color: darken($color-border, 20%); |
||||
|
display: none; |
||||
|
font-size: .9rem; |
||||
|
font-weight: normal; |
||||
|
padding: .3rem; |
||||
|
background: transparent; |
||||
|
} |
||||
|
|
||||
|
&-name { |
||||
|
& { |
||||
|
height: 40px; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
.client-edit { |
||||
|
display: inline-block; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.access-token { |
||||
|
resize: none; |
||||
|
height: 300px; |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import * as Ng2 from '@angular/core'; |
||||
|
import * as Ng2Forms from '@angular/forms'; |
||||
|
|
||||
|
import { |
||||
|
AccessTokenDto, |
||||
|
AppsStoreService, |
||||
|
AppClientDto, |
||||
|
AppClientCreateDto, |
||||
|
AppClientsService, |
||||
|
fadeAnimation, |
||||
|
ModalView, |
||||
|
Notification, |
||||
|
NotificationService, |
||||
|
TitleService |
||||
|
} from 'shared'; |
||||
|
|
||||
|
@Ng2.Component({ |
||||
|
selector: 'sqx-client', |
||||
|
styles, |
||||
|
template, |
||||
|
animations: [ |
||||
|
fadeAnimation |
||||
|
] |
||||
|
}) |
||||
|
export class ClientComponent { |
||||
|
private oldName: string; |
||||
|
|
||||
|
public isRenaming = false; |
||||
|
|
||||
|
public appClientToken: AccessTokenDto; |
||||
|
|
||||
|
@Ng2.Input('appName') |
||||
|
public appName: string; |
||||
|
|
||||
|
@Ng2.Input('client') |
||||
|
public client: AppClientDto; |
||||
|
|
||||
|
@Ng2.ViewChild('inputId') |
||||
|
public inputId: Ng2.ElementRef; |
||||
|
|
||||
|
@Ng2.ViewChild('inputSecret') |
||||
|
public inputSecret: Ng2.ElementRef; |
||||
|
|
||||
|
public modalDialog = new ModalView(); |
||||
|
|
||||
|
constructor( |
||||
|
private readonly appClientsService: AppClientsService, |
||||
|
private readonly notifications: NotificationService |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public rename() { |
||||
|
this.appClientsService.renameClient(this.appName, this.client.id, this.client.name) |
||||
|
.subscribe(() => { |
||||
|
this.stopRename(); |
||||
|
}, error => { |
||||
|
this.notifications.notify(Notification.error(error.displayMessage)); |
||||
|
this.cancelRename(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public cancelRename() { |
||||
|
this.client.name = this.oldName; |
||||
|
|
||||
|
this.isRenaming = false; |
||||
|
} |
||||
|
|
||||
|
public stopRename() { |
||||
|
this.client.name = this.client.name || this.client.id; |
||||
|
|
||||
|
this.isRenaming = false; |
||||
|
} |
||||
|
|
||||
|
public startRename() { |
||||
|
this.oldName = this.client.name; |
||||
|
|
||||
|
this.isRenaming = true; |
||||
|
} |
||||
|
|
||||
|
public createToken(client: AppClientDto) { |
||||
|
this.appClientsService.createToken(this.appName, client) |
||||
|
.subscribe(token => { |
||||
|
this.appClientToken = token; |
||||
|
this.modalDialog.show(); |
||||
|
}, error => { |
||||
|
this.notifications.notify(Notification.error('Failed to retrieve access token. Please retry.')); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public copyId() { |
||||
|
this.copyToClipbord(this.inputId.nativeElement); |
||||
|
} |
||||
|
|
||||
|
public copySecret() { |
||||
|
this.copyToClipbord(this.inputSecret.nativeElement); |
||||
|
} |
||||
|
|
||||
|
private copyToClipbord(element: HTMLInputElement | HTMLTextAreaElement) { |
||||
|
const currentFocus: any = document.activeElement; |
||||
|
|
||||
|
const prevSelectionStart = element.selectionStart; |
||||
|
const prevSelectionEnd = element.selectionEnd; |
||||
|
|
||||
|
element.focus(); |
||||
|
element.setSelectionRange(0, element.value.length); |
||||
|
|
||||
|
try { |
||||
|
document.execCommand('copy'); |
||||
|
} catch (e) { |
||||
|
console.log('Copy failed'); |
||||
|
} |
||||
|
|
||||
|
if (currentFocus && typeof currentFocus.focus === 'function') { |
||||
|
currentFocus.focus(); |
||||
|
} |
||||
|
|
||||
|
element.setSelectionRange(prevSelectionStart, prevSelectionEnd); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,51 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import * as Ng2 from '@angular/core'; |
||||
|
|
||||
|
import { FocusOnInitDirective } from './focus-on-init.directive'; |
||||
|
|
||||
|
describe('FocusOnInitDirective', () => { |
||||
|
let originalTimeout = 0; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; |
||||
|
|
||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 800; |
||||
|
}); |
||||
|
|
||||
|
it('should call focus on element when init', (done: any) => { |
||||
|
const calledMethods: string[] = []; |
||||
|
const calledElements: any[] = []; |
||||
|
|
||||
|
const renderer = { |
||||
|
invokeElementMethod: (element: any, method: any, args: any) => { |
||||
|
calledElements.push(element); |
||||
|
calledMethods.push(method); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const element: Ng2.ElementRef = { |
||||
|
nativeElement: {} |
||||
|
}; |
||||
|
|
||||
|
new FocusOnInitDirective(element, renderer as Ng2.Renderer).ngOnInit(); |
||||
|
|
||||
|
expect(calledMethods).toEqual([]); |
||||
|
|
||||
|
setTimeout(() => { |
||||
|
expect(calledMethods).toEqual(['focus', 'select']); |
||||
|
expect(calledElements).toEqual([element.nativeElement, element.nativeElement]); |
||||
|
|
||||
|
done(); |
||||
|
}, 400); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,29 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import * as Ng2 from '@angular/core'; |
||||
|
|
||||
|
@Ng2.Directive({ |
||||
|
selector: '[sqxFocusOnInit]' |
||||
|
}) |
||||
|
export class FocusOnInitDirective implements Ng2.OnInit { |
||||
|
@Ng2.Input() |
||||
|
public gpFocusOnChange: any; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly elementRef: Ng2.ElementRef, |
||||
|
private readonly renderer: Ng2.Renderer |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
setTimeout(() => { |
||||
|
this.renderer.invokeElementMethod(this.elementRef.nativeElement, 'focus', []); |
||||
|
this.renderer.invokeElementMethod(this.elementRef.nativeElement, 'select', []); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Sebastian Stehle. All rights reserved |
||||
|
*/ |
||||
|
|
||||
|
import * as Ng2Http from '@angular/http'; |
||||
|
|
||||
|
import { Observable } from 'rxjs'; |
||||
|
|
||||
|
export class ErrorDto { |
||||
|
public get displayMessage(): string { |
||||
|
let result = this.message; |
||||
|
|
||||
|
if (this.details && this.details.length > 0) { |
||||
|
const detailMessage = this.details[0]; |
||||
|
|
||||
|
const lastChar = result[result.length - 1]; |
||||
|
|
||||
|
if (lastChar !== '.' && lastChar !== ',') { |
||||
|
result += '.'; |
||||
|
} |
||||
|
|
||||
|
result += ' '; |
||||
|
result += detailMessage; |
||||
|
} |
||||
|
|
||||
|
const lastChar = result[result.length - 1]; |
||||
|
|
||||
|
if (lastChar !== '.') { |
||||
|
result += '.'; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
constructor( |
||||
|
public readonly statusCode: number, |
||||
|
public readonly message: string, |
||||
|
public readonly details: string[] = [] |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function handleError(message: string, error: Ng2Http.Response | any) { |
||||
|
let result = new ErrorDto(500, message); |
||||
|
|
||||
|
if (error instanceof Ng2Http.Response && error.status !== 500) { |
||||
|
const body = error.json(); |
||||
|
|
||||
|
result = new ErrorDto(error.status, body.message, body.details); |
||||
|
} |
||||
|
|
||||
|
return Observable.throw(result); |
||||
|
} |
||||
Loading…
Reference in new issue