Browse Source

Components refactored

pull/1/head
Sebastian 9 years ago
parent
commit
0d8e3470ee
  1. 42
      src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs
  2. 6
      src/Squidex.Write/Apps/AppContributors.cs
  3. 4
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  4. 2
      src/Squidex/Controllers/Api/Apps/AppContributorsController.cs
  5. 4
      src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs
  6. 4
      src/Squidex/Controllers/Api/Apps/Models/AddAppLanguageDto.cs
  7. 6
      src/Squidex/Controllers/Api/Apps/Models/AssignAppContributorDto.cs
  8. 4
      src/Squidex/Controllers/Api/Apps/Models/CreateAppClientDto.cs
  9. 4
      src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs
  10. 5
      src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs
  11. 9
      src/Squidex/app/app.module.ts
  12. 4
      src/Squidex/app/app.routes.ts
  13. 12
      src/Squidex/app/components/internal/app/settings/client.component.html
  14. 3
      src/Squidex/app/components/internal/app/settings/client.component.scss
  15. 102
      src/Squidex/app/components/internal/app/settings/client.component.ts
  16. 4
      src/Squidex/app/components/internal/app/settings/clients-page.component.html
  17. 77
      src/Squidex/app/components/internal/app/settings/clients-page.component.ts
  18. 10
      src/Squidex/app/components/internal/app/settings/contributors-page.component.html
  19. 6
      src/Squidex/app/components/internal/app/settings/contributors-page.component.scss
  20. 101
      src/Squidex/app/components/internal/app/settings/contributors-page.component.ts
  21. 2
      src/Squidex/app/components/internal/app/settings/languages-page.component.html
  22. 92
      src/Squidex/app/components/internal/app/settings/languages-page.component.ts
  23. 4
      src/Squidex/app/components/internal/module.ts
  24. 4
      src/Squidex/app/components/layout/app-form.component.ts
  25. 6
      src/Squidex/app/components/layout/profile-menu.component.scss
  26. 45
      src/Squidex/app/framework/angular/copy.directive.ts
  27. 44
      src/Squidex/app/framework/angular/title.component.ts
  28. 3
      src/Squidex/app/framework/declarations.ts
  29. 10
      src/Squidex/app/framework/module.ts
  30. 2
      src/Squidex/app/framework/services/drag.service.ts
  31. 74
      src/Squidex/app/framework/utils/array-helper.spec.ts
  32. 74
      src/Squidex/app/framework/utils/array-helper.ts
  33. 2
      src/Squidex/app/main.ts
  34. 54
      src/Squidex/app/shared/app-component-base.ts
  35. 2
      src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts
  36. 2
      src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
  37. 2
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
  38. 4
      src/Squidex/app/shared/index.ts
  39. 19
      src/Squidex/app/shared/services/app-clients.service.spec.ts
  40. 22
      src/Squidex/app/shared/services/app-clients.service.ts
  41. 2
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  42. 6
      src/Squidex/app/shared/services/app-contributors.service.ts
  43. 16
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  44. 28
      src/Squidex/app/shared/services/app-languages.service.ts
  45. 17
      src/Squidex/app/shared/services/apps-store.service.spec.ts
  46. 22
      src/Squidex/app/shared/services/apps-store.service.ts
  47. 18
      src/Squidex/app/shared/services/apps.service.spec.ts
  48. 17
      src/Squidex/app/shared/services/apps.service.ts
  49. 2
      src/Squidex/app/shared/services/auth.service.ts
  50. 7
      src/Squidex/app/shared/services/common.ts
  51. 2
      src/Squidex/app/shared/services/languages.service.spec.ts
  52. 3
      src/Squidex/app/shared/services/languages.service.ts
  53. 187
      src/Squidex/app/shared/services/schemas.service.ts
  54. 3
      src/Squidex/app/shared/services/users-provider.service.ts
  55. 2
      src/Squidex/app/shared/services/users.service.spec.ts
  56. 2
      src/Squidex/app/vendor.ts
  57. 4
      src/Squidex/tslint.json

42
src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs

@ -30,6 +30,7 @@ namespace Squidex.Infrastructure.CQRS.EventStore
private readonly IStreamPositionStorage positions;
private readonly List<EventStoreCatchUpSubscription> catchSubscriptions = new List<EventStoreCatchUpSubscription>();
private EventStoreSubscription liveSubscription;
private string streamName;
private bool isSubscribed;
public EventStoreBus(
@ -71,22 +72,24 @@ namespace Squidex.Infrastructure.CQRS.EventStore
}
}
public void Subscribe(string streamName = "$all")
public void Subscribe(string streamToConnect = "$all")
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
Guard.NotNullOrEmpty(streamToConnect, nameof(streamToConnect));
if (isSubscribed)
{
return;
}
SubscribeLive(streamName);
SubscribeCatch(streamName);
this.streamName = streamToConnect;
SubscribeLive();
SubscribeCatch();
isSubscribed = true;
}
private void SubscribeLive(string streamName)
private void SubscribeLive()
{
Task.Run(async () =>
{
@ -94,20 +97,37 @@ namespace Squidex.Infrastructure.CQRS.EventStore
await connection.SubscribeToStreamAsync(streamName, true,
(subscription, resolvedEvent) =>
{
OnLiveEvent(streamName, resolvedEvent);
}, userCredentials: credentials);
OnLiveEvent(resolvedEvent);
}, (subscription, dropped, ex) =>
{
OnConnectionDropped();
}, credentials);
}).Wait();
}
private void SubscribeCatch(string streamName)
private void OnConnectionDropped()
{
try
{
liveSubscription.Close();
logger.LogError("Subscription closed");
}
finally
{
SubscribeLive();
}
}
private void SubscribeCatch()
{
foreach (var catchConsumer in catchConsumers)
{
SubscribeCatchFor(catchConsumer, streamName);
SubscribeCatchFor(catchConsumer);
}
}
private void SubscribeCatchFor(IEventConsumer consumer, string streamName)
private void SubscribeCatchFor(IEventConsumer consumer)
{
var subscriptionName = consumer.GetType().GetTypeInfo().Name;
@ -134,7 +154,7 @@ namespace Squidex.Infrastructure.CQRS.EventStore
}
}
private void OnLiveEvent(string streamName, ResolvedEvent resolvedEvent)
private void OnLiveEvent(ResolvedEvent resolvedEvent)
{
Envelope<IEvent> @event = null;

6
src/Squidex.Write/Apps/AppContributors.cs

@ -24,7 +24,7 @@ namespace Squidex.Write.Apps
{
Func<string> message = () => "Cannot assign contributor";
ThrowIfFound(contributorId, message);
ThrowIfFound(contributorId, permission, message);
ThrowIfNoOwner(c => c[contributorId] = permission, message);
contributors[contributorId] = permission;
@ -48,11 +48,11 @@ namespace Squidex.Write.Apps
}
}
private void ThrowIfFound(string contributorId, Func<string> message)
private void ThrowIfFound(string contributorId, PermissionLevel permission, Func<string> message)
{
PermissionLevel currentPermission;
if (contributors.TryGetValue(contributorId, out currentPermission))
if (contributors.TryGetValue(contributorId, out currentPermission) && currentPermission == permission)
{
var error = new ValidationError("Contributor is already part of the app with same permissions", "ContributorId");

4
src/Squidex/Controllers/Api/Apps/AppClientsController.cs

@ -82,7 +82,7 @@ namespace Squidex.Controllers.Api.Apps
[HttpPost]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 201)]
public async Task<IActionResult> PostClient(string app, [FromBody] AttachClientDto request)
public async Task<IActionResult> PostClient(string app, [FromBody] CreateAppClientDto request)
{
var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new AttachClient()));
var result = context.Result<AppClient>();
@ -105,7 +105,7 @@ namespace Squidex.Controllers.Api.Apps
[HttpPut]
[Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientDto[]), 201)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] RenameClientDto request)
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request)
{
await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId }));

2
src/Squidex/Controllers/Api/Apps/AppContributorsController.cs

@ -75,7 +75,7 @@ namespace Squidex.Controllers.Api.Apps
[HttpPost]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignContributorDto request)
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignAppContributorDto request)
{
await CommandBus.PublishAsync(SimpleMapper.Map(request, new AssignContributor()));

4
src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs

@ -82,7 +82,7 @@ namespace Squidex.Controllers.Api.Apps
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguageDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PostLanguage(string app, [FromBody] AddLanguageDto request)
public async Task<IActionResult> PostLanguage(string app, [FromBody] AddAppLanguageDto request)
{
await CommandBus.PublishAsync(SimpleMapper.Map(request, new AddLanguage()));
@ -104,7 +104,7 @@ namespace Squidex.Controllers.Api.Apps
/// </returns>
[HttpPut]
[Route("apps/{app}/languages/{language}")]
public async Task<IActionResult> Update(string app, string language, [FromBody] SetMasterLanguageDto model)
public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateAppLanguageDto model)
{
if (model.IsMasterLanguage)
{

4
src/Squidex/Controllers/Api/Apps/Models/AddLanguageDto.cs → src/Squidex/Controllers/Api/Apps/Models/AddAppLanguageDto.cs

@ -1,5 +1,5 @@
// ==========================================================================
// AddLanguageDto.cs
// AddAppLanguageDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Controllers.Api.Apps.Models
{
public class AddLanguageDto
public class AddAppLanguageDto
{
/// <summary>
/// The language to add.

6
src/Squidex/Controllers/Api/Apps/Models/AssignContributorDto.cs → src/Squidex/Controllers/Api/Apps/Models/AssignAppContributorDto.cs

@ -1,19 +1,19 @@
// ==========================================================================
// AssignContributorDto.cs
// AssignAppContributorDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Core.Apps;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models
{
public sealed class AssignContributorDto
public sealed class AssignAppContributorDto
{
/// <summary>
/// The id of the user to add to the app (GUID).

4
src/Squidex/Controllers/Api/Apps/Models/AttachClientDto.cs → src/Squidex/Controllers/Api/Apps/Models/CreateAppClientDto.cs

@ -1,5 +1,5 @@
// ==========================================================================
// AttachClientDto.cs
// CreateAppClientDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models
{
public sealed class AttachClientDto
public sealed class CreateAppClientDto
{
/// <summary>
/// The id of the client.

4
src/Squidex/Controllers/Api/Apps/Models/RenameClientDto.cs → src/Squidex/Controllers/Api/Apps/Models/UpdateAppClientDto.cs

@ -1,5 +1,5 @@
// ==========================================================================
// RenameClientDto.cs
// UpdateAppClientDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models
{
public class RenameClientDto
public class UpdateAppClientDto
{
/// <summary>
/// The new display name of the client.

5
src/Squidex/Controllers/Api/Apps/Models/SetMasterLanguageDto.cs → src/Squidex/Controllers/Api/Apps/Models/UpdateAppLanguageDto.cs

@ -1,14 +1,13 @@
// ==========================================================================
// SetMasterLanguageDto.cs
// UpdateAppLanguageDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Controllers.Api.Apps.Models
{
public class SetMasterLanguageDto
public class UpdateAppLanguageDto
{
/// <summary>
/// Set the value to true to make the language to the master language.

9
src/Squidex/app/app.module.ts

@ -16,18 +16,17 @@ import {
AppContributorsService,
AppLanguagesService,
AppMustExistGuard,
AppsStoreService,
AppsService,
AppsStoreService,
AuthService,
CurrencyConfig,
DragService,
DragServiceFactory,
DecimalSeparatorConfig,
DragService,
LanguageService,
LocalStoreService,
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
NotificationService,
LanguageService,
LocalStoreService,
SqxFrameworkModule,
TitlesConfig,
TitleService,

4
src/Squidex/app/app.routes.ts

@ -9,13 +9,13 @@ import * as Ng2 from '@angular/core';
import * as Ng2Router from '@angular/router';
import {
AppsPageComponent,
AppAreaComponent,
AppsPageComponent,
ClientsPageComponent,
ContributorsPageComponent,
DashboardPageComponent,
InternalAreaComponent,
HomePageComponent,
InternalAreaComponent,
LanguagesPageComponent,
LogoutPageComponent,
NotFoundPageComponent,

12
src/Squidex/app/components/internal/app/settings/client.component.html

@ -13,9 +13,9 @@
</div>
<div class="client-name">
<form *ngIf="isRenaming" class="form-inline" (submit)="rename()">
<form *ngIf="isRenaming" class="form-inline" [formGroup]="renameForm" (submit)="rename()">
<div class="form-group">
<input type="text" class="form-control" name="client-name" id="client-name" sqxFocusOnInit [(ngModel)]="client.name" />
<input type="text" class="form-control" formControlName="name" sqxFocusOnInit />
</div>
<button type="submit" class="btn btn-primary">Save</button>
@ -39,10 +39,10 @@
<tr>
<td>Client Id:</td>
<td>
<input readonly class="form-control" #inputId [attr.value]="appName + ':' + client.id" />
<input readonly class="form-control" #inputId [attr.value]="clientName" #inputName />
</td>
<td>
<button type="button" class="btn btn-primary btn-link" (click)="copyId()">
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputName">
<i class="icon-copy"></i>
</button>
</td>
@ -50,10 +50,10 @@
<tr>
<td>Client Secret:</td>
<td>
<input readonly class="form-control" name="inputSecret" [attr.value]="client.secret" />
<input readonly class="form-control" name="inputSecret" [attr.value]="clientSecret" #inputSecret />
</td>
<td>
<button type="button" class="btn btn-primary btn-link" (click)="copySecret()">
<button type="button" class="btn btn-primary btn-link" [sqxCopy]="inputSecret">
<i class="icon-copy"></i>
</button>
</td>

3
src/Squidex/app/components/internal/app/settings/client.component.scss

@ -19,8 +19,9 @@
display: none;
font-size: .9rem;
font-weight: normal;
padding: .3rem;
padding: .6rem .3rem;
background: transparent;
vertical-align: top;
}
&-name {

102
src/Squidex/app/components/internal/app/settings/client.component.ts

@ -10,15 +10,9 @@ import * as Ng2Forms from '@angular/forms';
import {
AccessTokenDto,
AppsStoreService,
AppClientDto,
AppClientCreateDto,
AppClientsService,
fadeAnimation,
ModalView,
Notification,
NotificationService,
TitleService
ModalView
} from 'shared';
@Ng2.Component({
@ -29,99 +23,59 @@ import {
fadeAnimation
]
})
export class ClientComponent {
private oldName: string;
export class ClientComponent implements Ng2.OnChanges {
public isRenaming = false;
public appClientToken: AccessTokenDto;
@Ng2.Input('appName')
public appName: string;
@Ng2.Output()
public renamed = new Ng2.EventEmitter<string>();
@Ng2.Input('client')
@Ng2.Input()
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 get clientName() {
return this.client.name || this.client.id;
}
public cancelRename() {
this.client.name = this.oldName;
this.isRenaming = false;
public get clientId() {
return this.client.id + ':' + this.client.secret;
}
public stopRename() {
this.client.name = this.client.name || this.client.id;
this.isRenaming = false;
public get clientSecret() {
return this.client.secret;
}
public startRename() {
this.oldName = this.client.name;
public renameForm =
this.formBuilder.group({
name: ['']
});
this.isRenaming = true;
constructor(
private readonly formBuilder: Ng2Forms.FormBuilder
) {
}
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 ngOnChanges() {
this.renameForm.controls['name'].setValue(this.clientName);
}
public copyId() {
this.copyToClipbord(this.inputId.nativeElement);
public cancelRename() {
this.isRenaming = false;
}
public copySecret() {
this.copyToClipbord(this.inputSecret.nativeElement);
public startRename() {
this.isRenaming = true;
}
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);
public rename() {
try {
document.execCommand('copy');
} catch (e) {
console.log('Copy failed');
}
if (currentFocus && typeof currentFocus.focus === 'function') {
currentFocus.focus();
this.renamed.emit(this.renameForm.controls['name'].value);
} finally {
this.isRenaming = false;
}
element.setSelectionRange(prevSelectionStart, prevSelectionEnd);
}
}

4
src/Squidex/app/components/internal/app/settings/clients-page.component.html

@ -1,3 +1,5 @@
<sqx-title message="{app} | Clients | Settings" parameter="app" value="{{appName() | async}}"></sqx-title>
<div class="layout">
<div class="layout-left">
<sqx-left-menu></sqx-left-menu>
@ -14,7 +16,7 @@
</div>
<div *ngFor="let client of appClients">
<sqx-client [client]="client" [appName]="appName"></sqx-client>
<sqx-client [client]="client" (renamed)="renameClient(client, $event)"></sqx-client>
</div>
<div class="table-items-footer">

77
src/Squidex/app/components/internal/app/settings/clients-page.component.ts

@ -9,24 +9,27 @@ import * as Ng2 from '@angular/core';
import * as Ng2Forms from '@angular/forms';
import {
AppsStoreService,
AppClientDto,
AppClientCreateDto,
AppClientsService,
Notification,
AppComponentBase,
AppsStoreService,
ArrayHelper,
CreateAppClientDto,
NotificationService,
TitleService
UpdateAppClientDto,
UsersProviderService
} from 'shared';
function rename(client: AppClientDto, name: string) {
return new AppClientDto(client.id, client.secret, name, client.expiresUtc);
};
@Ng2.Component({
selector: 'sqx-clients-page',
styles,
template
})
export class ClientsPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string | null = null;
export class ClientsPageComponent extends AppComponentBase implements Ng2.OnInit {
public appClients: AppClientDto[];
public createForm =
@ -39,47 +42,44 @@ export class ClientsPageComponent implements Ng2.OnInit {
]]
});
constructor(
private readonly titles: TitleService,
private readonly appsStore: AppsStoreService,
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly appClientsService: AppClientsService,
private readonly formBuilder: Ng2Forms.FormBuilder,
private readonly notifications: NotificationService
private readonly formBuilder: Ng2Forms.FormBuilder
) {
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
super(apps, notifications, users);
}
public ngOnInit() {
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Clients', { appName: app.name });
this.load();
}
});
}
public load() {
this.appClientsService.getClients(this.appName)
.subscribe(clients => {
this.appClients = clients;
this.appName()
.switchMap(app => this.appClientsService.getClients(app).retry(2))
.subscribe(dtos => {
this.appClients = dtos;
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.appClients = [];
this.notifyError(error);
});
}
public revokeClient(client: AppClientDto) {
this.appClientsService.deleteClient(this.appName, client.id)
this.appName()
.switchMap(app => this.appClientsService.deleteClient(app, client.id))
.subscribe(() => {
this.appClients = ArrayHelper.remove(this.appClients, client);
}, error => {
this.notifyError(error);
});
}
public renameClient(client: AppClientDto, name: string) {
this.appName()
.switchMap(app => this.appClientsService.updateClient(app, client.id, new UpdateAppClientDto(name)))
.subscribe(() => {
this.appClients.splice(this.appClients.indexOf(client), 1);
this.appClients = ArrayHelper.replace(this.appClients, client, rename(client, name));
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
}
@ -89,14 +89,15 @@ export class ClientsPageComponent implements Ng2.OnInit {
if (this.createForm.valid) {
this.createForm.disable();
const dto = new AppClientCreateDto(this.createForm.controls['name'].value);
const dto = new CreateAppClientDto(this.createForm.controls['name'].value);
this.appClientsService.postClient(this.appName, dto)
.subscribe(client => {
this.appClients.push(client);
this.appName()
.switchMap(app => this.appClientsService.postClient(app, dto))
.subscribe(dto => {
this.appClients = ArrayHelper.push(this.appClients, dto);
this.reset();
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
this.reset();
});
}

10
src/Squidex/app/components/internal/app/settings/contributors-page.component.html

@ -1,3 +1,5 @@
<sqx-title message="{app} | Contributors | Settings" parameter="app" value="{{appName() | async}}"></sqx-title>
<div class="layout">
<div class="layout-left">
<sqx-left-menu></sqx-left-menu>
@ -22,16 +24,16 @@
<template ngFor let-contributor [ngForOf]="appContributors">
<tr>
<td>
<img class="user-picture" [attr.src]="pictureUrl(contributor) | async" />
<img class="user-picture" [attr.src]="userPicture(contributor.contributorId) | async" />
</td>
<td>
<span class="user-name">{{displayName(contributor) | async}}</span>
<span class="user-name">{{userName(contributor.contributorId) | async}}</span>
</td>
<td>
<span class="user-email">{{email(contributor) | async}}</span>
<span class="user-email">{{userEmail(contributor.contributorId) | async}}</span>
</td>
<td>
<select class="form-control" [(ngModel)]="contributor.permission" (ngModelChange)="changePermission(contributor)" [disabled]="currrentUserId === contributor.contributorId">
<select class="form-control" [(ngModel)]="contributor.permission" (ngModelChange)="changePermission(contributor, $event)" [disabled]="currentUserId === contributor.contributorId">
<option *ngFor="let permission of usersPermissions">{{permission}}</option>
</select>
</td>

6
src/Squidex/app/components/internal/app/settings/contributors-page.component.scss

@ -17,9 +17,15 @@
.user {
&-picture {
& {
@include circle(2.2rem);
}
&:not([src]) {
@include opacity(0);
}
}
&-name,
&-email {
@include truncate;

101
src/Squidex/app/components/internal/app/settings/contributors-page.component.ts

@ -7,21 +7,21 @@
import * as Ng2 from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { Observable } from 'rxjs';
import {
AppComponentBase,
AppContributorDto,
AppContributorsService,
AppsStoreService,
ArrayHelper,
AuthService,
AutocompleteItem,
AutocompleteSource,
Notification,
NotificationService,
TitleService,
UserDto,
UsersService,
UsersProviderService
UsersProviderService,
UsersService
} from 'shared';
class UsersDataSource implements AutocompleteSource {
@ -51,22 +51,23 @@ class UsersDataSource implements AutocompleteSource {
}
}
function changePermission(contributor: AppContributorDto, permission: string): AppContributorDto {
return new AppContributorDto(contributor.contributorId, permission);
}
@Ng2.Component({
selector: 'sqx-contributor-page',
styles,
template
})
export class ContributorsPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string;
export class ContributorsPageComponent extends AppComponentBase implements Ng2.OnInit {
public appContributors: AppContributorDto[] = [];
public currentUserId: string;
public selectedUserName: string | null = null;
public selectedUser: UserDto | null = null;
public currrentUserId: string;
public usersDataSource: UsersDataSource;
public usersPermissions = [
'Owner',
@ -74,42 +75,29 @@ export class ContributorsPageComponent implements Ng2.OnInit {
'Editor'
];
constructor(
private readonly titles: TitleService,
private readonly authService: AuthService,
private readonly appsStore: AppsStoreService,
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly appContributorsService: AppContributorsService,
private readonly usersProvider: UsersProviderService,
private readonly usersService: UsersService,
private readonly notifications: NotificationService
private readonly authService: AuthService
) {
this.usersDataSource = new UsersDataSource(usersService, this);
}
super(apps, notifications, users);
public ngOnDestroy() {
this.appSubscription.unsubscribe();
this.usersDataSource = new UsersDataSource(usersService, this);
}
public ngOnInit() {
this.currrentUserId = this.authService.user.id;
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.appName = app.name;
this.currentUserId = this.authService.user.id;
this.titles.setTitle('{appName} | Settings | Contributors', { appName: app.name });
this.load();
}
});
}
public load() {
this.appContributorsService.getContributors(this.appName).retry(2)
.subscribe(contributors => {
this.appContributors = contributors;
this.appName()
.switchMap(app => this.appContributorsService.getContributors(app).retry(2))
.subscribe(dtos => {
this.appContributors = dtos;
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
}
@ -123,46 +111,39 @@ export class ContributorsPageComponent implements Ng2.OnInit {
this.selectedUser = null;
this.selectedUserName = null;
this.appContributorsService.postContributor(this.appName, contributor)
this.appName()
.switchMap(app => this.appContributorsService.postContributor(app, contributor))
.subscribe(() => {
this.appContributors.push(contributor);
this.appContributors = ArrayHelper.push(this.appContributors, contributor);
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
}
public removeContributor(contributor: AppContributorDto) {
this.appContributorsService.deleteContributor(this.appName, contributor.contributorId)
public changePermission(contributor: AppContributorDto, permission: string) {
const newContributor = changePermission(contributor, permission);
this.appName()
.switchMap(app => this.appContributorsService.postContributor(app, newContributor))
.subscribe(() => {
this.appContributors.splice(this.appContributors.indexOf(contributor), 1);
this.appContributors = ArrayHelper.replace(this.appContributors, contributor, newContributor);
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
}
public changePermission(contributor: AppContributorDto) {
this.appContributorsService.postContributor(this.appName, contributor)
.catch(error => {
this.notifications.notify(Notification.error(error.displayMessage));
return Observable.of(true);
}).subscribe();
public removeContributor(contributor: AppContributorDto) {
this.appName()
.switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId))
.subscribe(() => {
this.appContributors = ArrayHelper.push(this.appContributors, contributor);
}, error => {
this.notifyError(error);
});
}
public selectUser(selection: UserDto) {
this.selectedUser = selection;
}
public email(contributor: AppContributorDto): Observable<string> {
return this.usersProvider.getUser(contributor.contributorId).map(u => u.email);
}
public displayName(contributor: AppContributorDto): Observable<string> {
return this.usersProvider.getUser(contributor.contributorId).map(u => u.displayName);
}
public pictureUrl(contributor: AppContributorDto): Observable<string> {
return this.usersProvider.getUser(contributor.contributorId).map(u => u.pictureUrl);
}
}

2
src/Squidex/app/components/internal/app/settings/languages-page.component.html

@ -1,3 +1,5 @@
<sqx-title message="{app} | Languages | Settings" parameter="app" value="{{appName() | async}}"></sqx-title>
<div class="layout">
<div class="layout-left">
<sqx-left-menu></sqx-left-menu>

92
src/Squidex/app/components/internal/app/settings/languages-page.component.ts

@ -8,14 +8,17 @@
import * as Ng2 from '@angular/core';
import {
AddAppLanguageDto,
AppComponentBase,
AppLanguageDto,
AppLanguagesService,
AppsStoreService,
ArrayHelper,
LanguageDto,
LanguageService,
Notification,
NotificationService,
TitleService
UpdateAppLanguageDto,
UsersProviderService
} from 'shared';
@Ng2.Component({
@ -23,10 +26,7 @@ import {
styles,
template
})
export class LanguagesPageComponent implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string;
export class LanguagesPageComponent extends AppComponentBase implements Ng2.OnInit {
public allLanguages: LanguageDto[] = [];
public appLanguages: AppLanguageDto[] = [];
@ -36,17 +36,11 @@ export class LanguagesPageComponent implements Ng2.OnInit {
return this.allLanguages.filter(x => !this.appLanguages.find(l => l.iso2Code === x.iso2Code));
}
constructor(
private readonly titles: TitleService,
private readonly appsStore: AppsStoreService,
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly appLanguagesService: AppLanguagesService,
private readonly languagesService: LanguageService,
private readonly notifications: NotificationService
private readonly languagesService: LanguageService
) {
}
public ngOnDestroy() {
this.appSubscription.unsubscribe();
super(apps, notifications, users);
}
public ngOnInit() {
@ -54,59 +48,59 @@ export class LanguagesPageComponent implements Ng2.OnInit {
.subscribe(languages => {
this.allLanguages = languages;
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Languages', { appName: app.name });
this.load();
}
});
}
public load() {
this.appLanguagesService.getLanguages(this.appName).retry(2)
.subscribe(appLanguages => {
this.appLanguages = appLanguages;
this.appName()
.switchMap(app => this.appLanguagesService.getLanguages(app).retry(2))
.subscribe(dtos => {
this.appLanguages = dtos;
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
}
public setMasterLanguage(selectedLanguage: AppLanguageDto) {
for (let language of this.appLanguages) {
language.isMasterLanguage = false;
}
this.appLanguagesService.makeMasterLanguage(this.appName, selectedLanguage.iso2Code)
.subscribe(() => {
selectedLanguage.isMasterLanguage = true;
public addLanguage() {
this.appName()
.switchMap(app => this.appLanguagesService.postLanguages(app, new AddAppLanguageDto(this.selectedLanguage.iso2Code)))
.subscribe(dto => {
this.appLanguages = ArrayHelper.push(this.appLanguages, dto);
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
this.selectedLanguage = null;
}
public addLanguage() {
this.appLanguagesService.postLanguages(this.appName, this.selectedLanguage.iso2Code)
.subscribe(appLanguage => {
this.appLanguages.push(appLanguage);
public removeLanguage(language: AppLanguageDto) {
this.appName()
.switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code))
.subscribe(dto => {
this.appLanguages = ArrayHelper.push(this.appLanguages, language);
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
this.selectedLanguage = null;
}
public removeLanguage(selectedLanguage: AppLanguageDto) {
this.appLanguagesService.deleteLanguage(this.appName, selectedLanguage.iso2Code)
.subscribe(appLanguage => {
this.appLanguages.splice(this.appLanguages.indexOf(appLanguage), 1);
public setMasterLanguage(language: AppLanguageDto) {
this.appName()
.switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, new UpdateAppLanguageDto(true)))
.subscribe(() => {
this.appLanguages = this.appLanguages.map(l => {
const isMasterLanguage = l === language;
if (isMasterLanguage !== l.isMasterLanguage) {
return new AppLanguageDto(l.iso2Code, l.englishName, isMasterLanguage);
} else {
return l;
}
});
}, error => {
this.notifications.notify(Notification.error(error.displayMessage));
this.notifyError(error);
});
}
}

4
src/Squidex/app/components/internal/module.ts

@ -7,8 +7,8 @@
import * as Ng2 from '@angular/core';
import { SqxFrameworkModule } from 'shared';
import { SqxLayoutModule } from 'components/layout';
import { SqxFrameworkModule } from 'shared';
import {
AppAreaComponent,
@ -18,8 +18,8 @@ import {
ContributorsPageComponent,
DashboardPageComponent,
InternalAreaComponent,
LeftMenuComponent,
LanguagesPageComponent,
LeftMenuComponent,
SchemasPageComponent
} from './declarations';

4
src/Squidex/app/components/layout/app-form.component.ts

@ -10,8 +10,8 @@ import * as Ng2Forms from '@angular/forms';
import {
AppDto,
AppCreateDto,
AppsStoreService,
CreateAppDto,
fadeAnimation
} from 'shared';
@ -65,7 +65,7 @@ export class AppFormComponent implements Ng2.OnInit {
if (this.createForm.valid) {
this.createForm.disable();
const dto = new AppCreateDto(this.createForm.controls['name'].value);
const dto = new CreateAppDto(this.createForm.controls['name'].value);
this.appsStore.createApp(dto)
.subscribe(app => {

6
src/Squidex/app/components/layout/profile-menu.component.scss

@ -5,8 +5,14 @@ $size-avatar: 2.2rem;
.user {
&-picture {
& {
@include circle(2.2rem);
}
&:not([src]) {
@include opacity(0);
}
}
}
.navbar-nav {

45
src/Squidex/app/framework/angular/copy.directive.ts

@ -0,0 +1,45 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
@Ng2.Directive({
selector: '[sqxCopy]'
})
export class CopyDirective {
@Ng2.Input('sqxCopy')
public inputElement: Ng2.ElementRef;
@Ng2.HostListener('click')
public onClick() {
if (this.inputElement) {
this.copyToClipbord(this.inputElement.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);
}
}

44
src/Squidex/app/framework/angular/title.component.ts

@ -0,0 +1,44 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import { TitleService } from './../services/title.service';
@Ng2.Component({
selector: 'sqx-title',
template: ''
})
export class TitleComponent implements Ng2.OnChanges {
@Ng2.Input()
public message: any;
@Ng2.Input()
public parameter: string;
@Ng2.Input()
public value: any;
constructor(
private readonly titleService: TitleService
) {
}
public ngOnChanges() {
const parameters = {};
if (this.parameter) {
if (!this.value) {
return;
}
parameters[this.parameter] = this.value;
}
this.titleService.setTitle(this.message, parameters);
}
}

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

@ -11,6 +11,7 @@ export * from './angular/autocomplete.component';
export * from './angular/validators';
export * from './angular/cloak.directive';
export * from './angular/color-picker.component';
export * from './angular/copy.directive';
export * from './angular/date-time.pipes';
export * from './angular/drag-model.directive';
export * from './angular/focus-on-change.directive';
@ -22,6 +23,7 @@ export * from './angular/scroll-active.directive';
export * from './angular/shortcut.component';
export * from './angular/slider.component';
export * from './angular/spinner.component';
export * from './angular/title.component';
export * from './angular/user-report.component';
export * from './configurations';
@ -34,6 +36,7 @@ export * from './services/title.service';
export * from './plattform';
export * from './utils/array-helper';
export * from './utils/color';
export * from './utils/color-palette';
export * from './utils/date-helper';

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

@ -5,16 +5,17 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2Common from '@angular/common';
import * as Ng2 from '@angular/core';
import * as Ng2Http from '@angular/http';
import * as Ng2Forms from '@angular/forms';
import * as Ng2Common from '@angular/common';
import * as Ng2Http from '@angular/http';
import * as Ng2Router from '@angular/router';
import {
AutocompleteComponent,
CloakDirective,
ColorPickerComponent,
CopyDirective,
DayOfWeekPipe,
DayPipe,
DragModelDirective,
@ -31,6 +32,7 @@ import {
ShortTimePipe,
SliderComponent,
SpinnerComponent,
TitleComponent,
UserReportComponent
} from './declarations';
@ -46,6 +48,7 @@ import {
AutocompleteComponent,
CloakDirective,
ColorPickerComponent,
CopyDirective,
DayOfWeekPipe,
DayPipe,
DragModelDirective,
@ -62,12 +65,14 @@ import {
ShortTimePipe,
SliderComponent,
SpinnerComponent,
TitleComponent,
UserReportComponent,
],
exports: [
AutocompleteComponent,
CloakDirective,
ColorPickerComponent,
CopyDirective,
DayOfWeekPipe,
DayPipe,
DragModelDirective,
@ -84,6 +89,7 @@ import {
ShortTimePipe,
SliderComponent,
SpinnerComponent,
TitleComponent,
UserReportComponent,
Ng2Http.HttpModule,
Ng2Forms.FormsModule,

2
src/Squidex/app/framework/services/drag.service.ts

@ -7,7 +7,7 @@
import * as Ng2 from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { Observable, Subject } from 'rxjs';
import { Vec2 } from './../utils/vec2';

74
src/Squidex/app/framework/utils/array-helper.spec.ts

@ -0,0 +1,74 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { ArrayHelper } from './../';
describe('ArrayHelper', () => {
it('should push item', () => {
const oldArray = [1, 2, 3];
const newArray = ArrayHelper.push(oldArray, 4);
expect(newArray).toEqual([1, 2, 3, 4]);
});
it('should remove item if found', () => {
const oldArray = [1, 2, 3];
const newArray = ArrayHelper.remove(oldArray, 2);
expect(newArray).toEqual([1, 3]);
});
it('should not remove item if not found', () => {
const oldArray = [1, 2, 3];
const newArray = ArrayHelper.remove(oldArray, 5);
expect(newArray).toEqual([1, 2, 3]);
});
it('should remove all by predicate', () => {
const oldArray: number[] = [1, 2, 3, 4];
const newArray = ArrayHelper.removeAll(oldArray, (i: number) => i % 2 === 0);
expect(newArray).toEqual([1, 3]);
});
it('should return original if nothing has been removed', () => {
const oldArray: number[] = [1, 2, 3, 4];
const newArray = ArrayHelper.removeAll(oldArray, (i: number) => i % 200 === 0);
expect(newArray).toEqual(oldArray);
});
it('should replace item if found', () => {
const oldArray = [1, 2, 3];
const newArray = ArrayHelper.replace(oldArray, 2, 4);
expect(newArray).toEqual([1, 4, 3]);
});
it('should not replace item if not found', () => {
const oldArray = [1, 2, 3];
const newArray = ArrayHelper.replace(oldArray, 5, 5);
expect(newArray).toEqual([1, 2, 3]);
});
it('should replace all by predicate', () => {
const oldArray: number[] = [1, 2, 3, 4];
const newArray = ArrayHelper.replaceAll(oldArray, (i: number) => i % 2 === 0, i => i * 2);
expect(newArray).toEqual([1, 4, 3, 8]);
});
it('should return original if nothing has been replace', () => {
const oldArray: number[] = [1, 2, 3, 4];
const newArray = ArrayHelper.replaceAll(oldArray, (i: number) => i % 200 === 0, i => i);
expect(newArray).toEqual(oldArray);
});
});

74
src/Squidex/app/framework/utils/array-helper.ts

@ -0,0 +1,74 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export module ArrayHelper {
export function push<T>(array: T[], item: T): T[] {
return [...array, item];
}
export function remove<T>(array: T[], item: T): T[] {
const index = array.indexOf(item);
if (index >= 0) {
return [...array.slice(0, index), ...array.slice(index + 1)];
} else {
return array;
}
}
export function removeAll<T>(array: T[], predicate: (item: T, index: number) => boolean): T[] {
const copy = array.slice();
let hasChange = false;
for (let i = 0; i < copy.length; ) {
if (predicate(copy[i], i)) {
copy.splice(i, 1);
hasChange = true;
} else {
++i;
}
}
return hasChange ? copy : array;
}
export function replace<T>(array: T[], oldItem: T, newItem: T): T[] {
const index = array.indexOf(oldItem);
if (index >= 0) {
const copy = array.slice();
copy[index] = newItem;
return copy;
} else {
return array;
}
}
export function replaceAll<T>(array: T[], predicate: (item: T, index: number) => boolean, replacer: (item: T) => T): T[] {
const copy = array.slice();
let hasChange = false;
for (let i = 0; i < copy.length; i++) {
if (predicate(copy[i], i)) {
const newItem = replacer(copy[i]);
if (copy[i] !== newItem) {
copy[i] = newItem;
hasChange = true;
}
}
}
return hasChange ? copy : array;
}
}

2
src/Squidex/app/main.ts

@ -7,8 +7,8 @@
import './theme/theme.scss';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

54
src/Squidex/app/shared/app-component-base.ts

@ -0,0 +1,54 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Observable } from 'rxjs';
import {
AppsStoreService,
ErrorDto,
Notification,
NotificationService,
UsersProviderService
} from 'shared';
export abstract class AppComponentBase {
constructor(
private readonly appsStore: AppsStoreService,
private readonly notifications: NotificationService,
private readonly usersProvider: UsersProviderService
) {
}
public appName(): Observable<string> {
return this.appsStore.selectedApp.map(a => a.name);
}
public userEmail(userId: string): Observable<string> {
return this.usersProvider.getUser(userId).map(u => u.email);
}
public userName(userId: string): Observable<string> {
return this.usersProvider.getUser(userId).map(u => u.displayName);
}
public userPicture(userId: string): Observable<string> {
return this.usersProvider.getUser(userId).map(u => u.pictureUrl);
}
public notifyError(error: string | ErrorDto) {
if (error instanceof ErrorDto) {
this.notifications.notify(Notification.error(error.displayMessage));
} else {
this.notifications.notify(Notification.error(error));
}
}
public notifyInfo(error: string) {
this.notifications.notify(Notification.error(error));
}
}

2
src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts

@ -7,8 +7,8 @@
import * as TypeMoq from 'typemoq';
import { AppMustExistGuard } from './app-must-exist.guard';
import { AppsStoreService } from 'shared';
import { AppMustExistGuard } from './app-must-exist.guard';
import { RouterMockup } from './router-mockup';
describe('AppMustExistGuard', () => {

2
src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts

@ -7,9 +7,9 @@
import * as TypeMoq from 'typemoq';
import { AuthService } from 'shared';
import { MustBeAuthenticatedGuard } from './must-be-authenticated.guard';
import { RouterMockup } from './router-mockup';
import { AuthService } from 'shared';
describe('MustBeAuthenticatedGuard', () => {
let authService: TypeMoq.Mock<AuthService>;

2
src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts

@ -7,9 +7,9 @@
import * as TypeMoq from 'typemoq';
import { AuthService } from 'shared';
import { MustBeNotAuthenticatedGuard } from './must-be-not-authenticated.guard';
import { RouterMockup } from './router-mockup';
import { AuthService } from 'shared';
describe('MustBeNotAuthenticatedGuard', () => {
let authService: TypeMoq.Mock<AuthService>;

4
src/Squidex/app/shared/index.ts

@ -8,14 +8,18 @@
export * from './guards/app-must-exist.guard';
export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard';
export * from './services/app-contributors.service';
export * from './services/app-clients.service';
export * from './services/app-languages.service';
export * from './services/apps-store.service';
export * from './services/apps.service';
export * from './services/auth.service';
export * from './services/common';
export * from './services/languages.service';
export * from './services/users-provider.service';
export * from './services/users.service';
export * from './app-component-base';
export * from 'framework';

19
src/Squidex/app/shared/services/app-clients.service.spec.ts

@ -5,8 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs';
@ -15,8 +15,9 @@ import {
AppClientDto,
AppClientsService,
AuthService,
AppClientCreateDto,
DateTime
CreateAppClientDto,
DateTime,
UpdateAppClientDto
} from './../';
describe('AppClientsService', () => {
@ -65,9 +66,9 @@ describe('AppClientsService', () => {
});
it('should make post request to create client', () => {
const createClient = new AppClientCreateDto('client1');
const dto = new CreateAppClientDto('client1');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', TypeMoq.It.isValue(createClient)))
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', TypeMoq.It.isValue(dto)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
@ -84,7 +85,7 @@ describe('AppClientsService', () => {
let client: AppClientDto = null;
appClientsService.postClient('my-app', createClient).subscribe(result => {
appClientsService.postClient('my-app', dto).subscribe(result => {
client = result;
});
@ -95,7 +96,9 @@ describe('AppClientsService', () => {
});
it('should make put request to rename client', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', TypeMoq.It.isValue({ name: 'Client 1' })))
const dto = new UpdateAppClientDto('Client 1 New');
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', TypeMoq.It.isValue(dto)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions()
@ -103,7 +106,7 @@ describe('AppClientsService', () => {
))
.verifiable(TypeMoq.Times.once());
appClientsService.renameClient('my-app', 'client1', 'Client 1');
appClientsService.updateClient('my-app', 'client1', dto);
authService.verifyAll();
});

22
src/Squidex/app/shared/services/app-clients.service.ts

@ -13,25 +13,32 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
import { handleError } from './common';
export class AppClientDto {
constructor(
public readonly id: string,
public readonly secret: string,
public name: string,
public readonly name: string,
public readonly expiresUtc: DateTime
) {
}
}
export class AppClientCreateDto {
export class CreateAppClientDto {
constructor(
public readonly id: string
) {
}
}
export class UpdateAppClientDto {
constructor(
public readonly name: string
) {
}
}
export class AccessTokenDto {
constructor(
public readonly accessToken: string,
@ -68,10 +75,10 @@ export class AppClientsService {
.catch(response => handleError('Failed to load clients. Please reload.', response));
}
public postClient(appName: string, client: AppClientCreateDto): Observable<AppClientDto> {
public postClient(appName: string, dto: CreateAppClientDto): Observable<AppClientDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return this.authService.authPost(url, client)
return this.authService.authPost(url, dto)
.map(response => response.json())
.map(response => {
return new AppClientDto(
@ -83,10 +90,10 @@ export class AppClientsService {
.catch(response => handleError('Failed to add client. Please reload.', response));
}
public renameClient(appName: string, id: string, name: string): Observable<any> {
public updateClient(appName: string, id: string, dto: UpdateAppClientDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`);
return this.authService.authPut(url, { name: name })
return this.authService.authPut(url, dto)
.catch(response => handleError('Failed to revoke client. Please reload.', response));
}
@ -105,6 +112,7 @@ export class AppClientsService {
});
const body = `grant_type=client_credentials&scope=squidex-api&client_id=${appName}:${client.id}&client_secret=${client.secret}`;
const url = this.apiUrl.buildUrl('identity-server/connect/token');
return this.http.post(url, body, options)

2
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -5,8 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs';

6
src/Squidex/app/shared/services/app-contributors.service.ts

@ -12,7 +12,7 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
import { handleError } from './common';
export class AppContributorDto {
constructor(
@ -47,10 +47,10 @@ export class AppContributorsService {
.catch(response => handleError('Failed to load contributors. Please reload.', response));
}
public postContributor(appName: string, contributor: AppContributorDto): Observable<any> {
public postContributor(appName: string, dto: AppContributorDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return this.authService.authPost(url, contributor)
return this.authService.authPost(url, dto)
.catch(response => handleError('Failed to add contributors. Please reload.', response));
}

16
src/Squidex/app/shared/services/app-languages.service.spec.ts

@ -5,16 +5,18 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs';
import {
AddAppLanguageDto,
ApiUrlConfig,
AppLanguageDto,
AppLanguagesService,
AuthService,
UpdateAppLanguageDto
} from './../';
describe('AppLanguagesService', () => {
@ -60,9 +62,9 @@ describe('AppLanguagesService', () => {
});
it('should make post request to add language', () => {
const newLanguage = 'de';
const dto = new AddAppLanguageDto('de');
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', TypeMoq.It.is(y => y['language'] === newLanguage)))
authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', TypeMoq.It.isValue(dto)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions({
@ -77,7 +79,7 @@ describe('AppLanguagesService', () => {
let language: AppLanguageDto;
appLanguagesService.postLanguages('my-app', newLanguage).subscribe(result => {
appLanguagesService.postLanguages('my-app', dto).subscribe(result => {
language = result;
});
@ -88,7 +90,9 @@ describe('AppLanguagesService', () => {
});
it('should make put request to make master language', () => {
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', TypeMoq.It.isValue({ isMasterLanguage: true })))
const dto = new UpdateAppLanguageDto(true);
authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', TypeMoq.It.isValue(dto)))
.returns(() => Observable.of(
new Ng2Http.Response(
new Ng2Http.ResponseOptions()
@ -96,7 +100,7 @@ describe('AppLanguagesService', () => {
))
.verifiable(TypeMoq.Times.once());
appLanguagesService.makeMasterLanguage('my-app', 'de');
appLanguagesService.updateLanguage('my-app', 'de', dto);
authService.verifyAll();
});

28
src/Squidex/app/shared/services/app-languages.service.ts

@ -12,13 +12,27 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
import { handleError } from './common';
export class AppLanguageDto {
constructor(
public readonly iso2Code: string,
public readonly englishName: string,
public isMasterLanguage: boolean
public readonly isMasterLanguage: boolean
) {
}
}
export class AddAppLanguageDto {
constructor(
public readonly name: string,
) {
}
}
export class UpdateAppLanguageDto {
constructor(
public readonly isMasterLanguage: boolean
) {
}
}
@ -49,10 +63,10 @@ export class AppLanguagesService {
.catch(response => handleError('Failed to load languages. Please reload', response));
}
public postLanguages(appName: string, languageCode: string): Observable<AppLanguageDto> {
public postLanguages(appName: string, dto: AddAppLanguageDto): Observable<AppLanguageDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return this.authService.authPost(url, { language: languageCode })
return this.authService.authPost(url, dto)
.map(response => response.json())
.map(response => {
return new AppLanguageDto(
@ -63,14 +77,14 @@ export class AppLanguagesService {
.catch(response => handleError('Failed to add language. Please reload.', response));
}
public makeMasterLanguage(appName: string, languageCode: string): Observable<any> {
public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return this.authService.authPut(url, { isMasterLanguage: true })
return this.authService.authPut(url, dto)
.catch(response => handleError('Failed to change language. Please reload.', response));
}
public deleteLanguage(appName: string, languageCode: string): Observable<AppLanguageDto> {
public deleteLanguage(appName: string, languageCode: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return this.authService.authDelete(url)

17
src/Squidex/app/shared/services/apps-store.service.spec.ts

@ -10,16 +10,19 @@ import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs';
import {
AppCreateDto,
AppDto,
AppsStoreService,
AppsService,
AuthService
AppsStoreService,
AuthService,
CreateAppDto,
DateTime
} from './../';
describe('AppsStoreService', () => {
const oldApps = [new AppDto('id', 'old-name', null, null, 'Owner')];
const newApp = new AppDto('id', 'new-name', null, null, 'Owner');
const now = DateTime.now();
const oldApps = [new AppDto('id', 'old-name', now, now, 'Owner')];
const newApp = new AppDto('id', 'new-name', now, now, 'Owner');
let appsService: TypeMoq.Mock<AppsService>;
let authService: TypeMoq.Mock<AuthService>;
@ -109,7 +112,7 @@ describe('AppsStoreService', () => {
result1 = x;
}).unsubscribe();
store.createApp(new AppCreateDto('new-name')).subscribe(x => { });
store.createApp(new CreateAppDto('new-name'), now).subscribe(x => { });
store.apps.subscribe(x => {
result2 = x;
@ -134,7 +137,7 @@ describe('AppsStoreService', () => {
let result: AppDto[] = null;
store.createApp(new AppCreateDto('new-name')).subscribe(x => { });
store.createApp(new CreateAppDto('new-name'), now).subscribe(x => { });
store.apps.subscribe(x => {
result = x;

22
src/Squidex/app/shared/services/apps-store.service.ts

@ -14,19 +14,21 @@ import {
} from 'rxjs';
import {
AppCreateDto,
AppDto,
AppsService
AppsService,
CreateAppDto
} from './apps.service';
import { DateTime } from 'framework';
import { AuthService } from './auth.service';
@Ng2.Injectable()
export class AppsStoreService {
private lastApps: AppDto[] | null = null;
private isAuthenticated = false;
private readonly apps$ = new Subject<AppDto[]>();
private readonly appName$ = new BehaviorSubject<string | null>(null);
private lastApps: AppDto[] | null = null;
private isAuthenticated = false;
private readonly appsPublished$ =
this.apps$
@ -88,8 +90,16 @@ export class AppsStoreService {
return this.selectedApp.take(1).map(app => app !== null).toPromise();
}
public createApp(appToCreate: AppCreateDto): Observable<AppDto> {
return this.appService.postApp(appToCreate).do(app => {
public createApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> {
return this.appService.postApp(dto)
.map(created => {
now = now || DateTime.now();
const app = new AppDto(created.id, dto.name, now, now, 'Owner');
return app;
})
.do(app => {
if (this.lastApps && app) {
this.apps$.next(this.lastApps.concat([app]));
}

18
src/Squidex/app/shared/services/apps.service.spec.ts

@ -5,18 +5,19 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs';
import {
ApiUrlConfig,
AppCreateDto,
AppDto,
AppsService,
AuthService,
DateTime
CreateAppDto,
DateTime,
EntityCreatedDto
} from './../';
describe('AppsService', () => {
@ -66,8 +67,7 @@ describe('AppsService', () => {
});
it('should make post request to create app', () => {
const createApp = new AppCreateDto('new-app');
const now = DateTime.now();
const createApp = new CreateAppDto('new-app');
authService.setup(x => x.authPost('http://service/p/api/apps', TypeMoq.It.isValue(createApp)))
.returns(() => Observable.of(
@ -81,13 +81,13 @@ describe('AppsService', () => {
))
.verifiable(TypeMoq.Times.once());
let newApp: AppDto = null;
let newCreated: EntityCreatedDto = null;
appsService.postApp(createApp, now).subscribe(result => {
newApp = result;
appsService.postApp(createApp).subscribe(result => {
newCreated = result;
}).unsubscribe();
expect(newApp).toEqual(new AppDto('123', 'new-app', now, now, 'Owner'));
expect(newCreated).toEqual(new EntityCreatedDto('123'));
authService.verifyAll();
});

17
src/Squidex/app/shared/services/apps.service.ts

@ -11,7 +11,7 @@ import { Observable } from 'rxjs';
import { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
import { EntityCreatedDto, handleError } from './common';
export class AppDto {
constructor(
@ -24,7 +24,7 @@ export class AppDto {
}
}
export class AppCreateDto {
export class CreateAppDto {
constructor(
public readonly name: string
) {
@ -60,20 +60,13 @@ export class AppsService {
.catch(response => handleError('Failed to load apps. Please reload.', response));
}
public postApp(app: AppCreateDto, now?: DateTime): Observable<AppDto> {
now = now || DateTime.now();
public postApp(dto: CreateAppDto): Observable<EntityCreatedDto> {
const url = this.apiUrl.buildUrl('api/apps');
return this.authService.authPost(url, app)
return this.authService.authPost(url, dto)
.map(response => response.json())
.map(response => {
return new AppDto(
response.id,
app.name,
now,
now,
'Owner');
return new EntityCreatedDto(response.id);
})
.catch(response => handleError('Failed to create app. Please reload.', response));
}

2
src/Squidex/app/shared/services/auth.service.ts

@ -67,6 +67,8 @@ export class AuthService {
return;
}
Log.logger = console;
this.userManager = new UserManager({
client_id: 'squidex-frontend',
scope: 'squidex-api openid profile squidex-profile',

7
src/Squidex/app/shared/services/errors.ts → src/Squidex/app/shared/services/common.ts

@ -9,6 +9,13 @@ import * as Ng2Http from '@angular/http';
import { Observable } from 'rxjs';
export class EntityCreatedDto {
constructor(
public readonly id: string
) {
}
}
export class ErrorDto {
public get displayMessage(): string {
let result = this.message;

2
src/Squidex/app/shared/services/languages.service.spec.ts

@ -5,8 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs';

3
src/Squidex/app/shared/services/languages.service.ts

@ -6,12 +6,13 @@
*/
import * as Ng2 from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './errors';
import { handleError } from './common';
export class LanguageDto {
constructor(

187
src/Squidex/app/shared/services/schemas.service.ts

@ -0,0 +1,187 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as Ng2 from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service';
import { handleError } from './common';
export class SchemaDto {
constructor(
public readonly id: string,
public readonly name: string,
public readonly created: DateTime,
public readonly lastModified: DateTime
) {
}
}
export class SchemaDetailsDto {
constructor(
public readonly id: string,
public readonly name: string,
public readonly created: DateTime,
public readonly lastModified: DateTime,
public readonly fields: FieldDto[]
) {
}
}
export class FieldDto {
constructor(
public readonly name: string,
public readonly isHidden: boolean,
public readonly isDisabled: boolean,
public readonly properties: FieldPropertiesDto
) {
}
}
export abstract class FieldPropertiesDto {
constructor(
public readonly label: string,
public readonly hints: string,
public readonly placeholder: string,
public readonly isRequired: boolean,
) {
}
}
export class NumberFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string, hints: string, placeholder: string, isRequired: boolean,
public readonly defaultValue: number | null,
public readonly maxValue: number | null,
public readonly minValue: number | null,
public readonly allowedValues: number[]
) {
super(label, hints, placeholder, isRequired);
this['$type'] = 'Number';
}
}
export class StringFieldPropertiesDto extends FieldPropertiesDto {
constructor(label: string, hints: string, placeholder: string, isRequired: boolean,
public readonly defaultValue: string,
public readonly pattern: string,
public readonly patternMessage: string,
public readonly minLength: number | null,
public readonly maxLength: number | null,
public readonly allowedValues: number[]
) {
super(label, hints, placeholder, isRequired);
this['$type'] = 'String';
}
}
export class UpdateSchemaDto {
constructor(
public readonly label: string,
public readonly hints: string
) {
}
}
export class UpdateFieldDto {
constructor(
public readonly properties: FieldPropertiesDto
) {
}
}
export class CreateSchemaDto {
constructor(
public readonly name: string
) {
}
}
@Ng2.Injectable()
export class SchemasService {
constructor(
private readonly authService: AuthService,
private readonly apiUrl: ApiUrlConfig
) {
}
public getSchemas(appName: string): Observable<SchemaDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const items: any[] = response;
return items.map(item => {
return new SchemaDto(
item.id,
item.name,
DateTime.parseISO_UTC(item.created),
DateTime.parseISO_UTC(item.lastModified));
});
})
.catch(response => handleError('Failed to load schemas. Please reload.', response));
}
public getSchema(appName: string, id: string): Observable<SchemaDetailsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/{id}`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
const fields = response.fields.map((item: any) => {
const typeName = item['$type'];
let properties = item.properties;
let propertiesDto: FieldPropertiesDto;
if (typeName === 'String') {
propertiesDto = new StringFieldPropertiesDto(
properties.label,
properties.hints,
properties.placeholder,
properties.isRequired,
properties.defaultValue,
properties.pattern,
properties.patternMessage,
properties.minLength,
properties.maxLength,
properties.allowedValues);
} else {
propertiesDto = new NumberFieldPropertiesDto(
properties.label,
properties.hints,
properties.placeholder,
properties.isRequired,
properties.defaultValue,
properties.minValue,
properties.maxValue,
properties.allowedValues);
}
return new FieldDto(
item.name,
item.isHidden,
item.isDisabled,
propertiesDto);
});
return new SchemaDetailsDto(
response.id,
response.name,
DateTime.parseISO_UTC(response.created),
DateTime.parseISO_UTC(response.lastModified),
fields);
})
.catch(response => handleError('Failed to load schema. Please reload.', response));
}
}

3
src/Squidex/app/shared/services/users-provider.service.ts

@ -29,6 +29,9 @@ export class UsersProviderService {
if (!result) {
const request =
this.usersService.getUser(id).retry(2)
.catch(err => {
return Observable.of(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', ''));
})
.map(u => {
if (this.authService.user && u.id === this.authService.user.id) {
return new UserDto(u.id, u.email, 'Me', u.pictureUrl);

2
src/Squidex/app/shared/services/users.service.spec.ts

@ -5,8 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs';

2
src/Squidex/app/vendor.ts

@ -5,6 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
// tslint:disable ordered-imports
// Angular 2
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';

4
src/Squidex/tslint.json

@ -49,6 +49,7 @@
"no-switch-case-fall-through": true,
"no-trailing-whitespace": false,
"no-unused-expression": true,
"no-unused-variable": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
@ -62,6 +63,9 @@
"only-arrow-functions": [
true,
"allow-declarations"],
"ordered-imports": [
true
],
"quotemark": [
true,
"single"

Loading…
Cancel
Save