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