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 IStreamPositionStorage positions;
private readonly List<EventStoreCatchUpSubscription> catchSubscriptions = new List<EventStoreCatchUpSubscription>(); private readonly List<EventStoreCatchUpSubscription> catchSubscriptions = new List<EventStoreCatchUpSubscription>();
private EventStoreSubscription liveSubscription; private EventStoreSubscription liveSubscription;
private string streamName;
private bool isSubscribed; private bool isSubscribed;
public EventStoreBus( 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) if (isSubscribed)
{ {
return; return;
} }
SubscribeLive(streamName); this.streamName = streamToConnect;
SubscribeCatch(streamName);
SubscribeLive();
SubscribeCatch();
isSubscribed = true; isSubscribed = true;
} }
private void SubscribeLive(string streamName) private void SubscribeLive()
{ {
Task.Run(async () => Task.Run(async () =>
{ {
@ -94,20 +97,37 @@ namespace Squidex.Infrastructure.CQRS.EventStore
await connection.SubscribeToStreamAsync(streamName, true, await connection.SubscribeToStreamAsync(streamName, true,
(subscription, resolvedEvent) => (subscription, resolvedEvent) =>
{ {
OnLiveEvent(streamName, resolvedEvent); OnLiveEvent(resolvedEvent);
}, userCredentials: credentials); }, (subscription, dropped, ex) =>
{
OnConnectionDropped();
}, credentials);
}).Wait(); }).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) 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; 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; 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"; Func<string> message = () => "Cannot assign contributor";
ThrowIfFound(contributorId, message); ThrowIfFound(contributorId, permission, message);
ThrowIfNoOwner(c => c[contributorId] = permission, message); ThrowIfNoOwner(c => c[contributorId] = permission, message);
contributors[contributorId] = permission; 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; 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"); 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] [HttpPost]
[Route("apps/{app}/clients/")] [Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 201)] [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 context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new AttachClient()));
var result = context.Result<AppClient>(); var result = context.Result<AppClient>();
@ -105,7 +105,7 @@ namespace Squidex.Controllers.Api.Apps
[HttpPut] [HttpPut]
[Route("apps/{app}/clients/{clientId}/")] [Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientDto[]), 201)] [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 })); 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] [HttpPost]
[Route("apps/{app}/contributors/")] [Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ErrorDto), 400)] [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())); 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/")] [Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguageDto), 201)] [ProducesResponseType(typeof(AppLanguageDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)] [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())); await CommandBus.PublishAsync(SimpleMapper.Map(request, new AddLanguage()));
@ -104,7 +104,7 @@ namespace Squidex.Controllers.Api.Apps
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/languages/{language}")] [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) 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 // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Controllers.Api.Apps.Models namespace Squidex.Controllers.Api.Apps.Models
{ {
public class AddLanguageDto public class AddAppLanguageDto
{ {
/// <summary> /// <summary>
/// The language to add. /// 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 // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Squidex.Core.Apps; using Squidex.Core.Apps;
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models namespace Squidex.Controllers.Api.Apps.Models
{ {
public sealed class AssignContributorDto public sealed class AssignAppContributorDto
{ {
/// <summary> /// <summary>
/// The id of the user to add to the app (GUID). /// 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 // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models namespace Squidex.Controllers.Api.Apps.Models
{ {
public sealed class AttachClientDto public sealed class CreateAppClientDto
{ {
/// <summary> /// <summary>
/// The id of the client. /// 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 // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models namespace Squidex.Controllers.Api.Apps.Models
{ {
public class RenameClientDto public class UpdateAppClientDto
{ {
/// <summary> /// <summary>
/// The new display name of the client. /// 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 // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
namespace Squidex.Controllers.Api.Apps.Models namespace Squidex.Controllers.Api.Apps.Models
{ {
public class SetMasterLanguageDto public class UpdateAppLanguageDto
{ {
/// <summary> /// <summary>
/// Set the value to true to make the language to the master language. /// 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, AppContributorsService,
AppLanguagesService, AppLanguagesService,
AppMustExistGuard, AppMustExistGuard,
AppsStoreService,
AppsService, AppsService,
AppsStoreService,
AuthService, AuthService,
CurrencyConfig, CurrencyConfig,
DragService,
DragServiceFactory,
DecimalSeparatorConfig, DecimalSeparatorConfig,
DragService,
LanguageService,
LocalStoreService,
MustBeAuthenticatedGuard, MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard, MustBeNotAuthenticatedGuard,
NotificationService, NotificationService,
LanguageService,
LocalStoreService,
SqxFrameworkModule, SqxFrameworkModule,
TitlesConfig, TitlesConfig,
TitleService, 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 * as Ng2Router from '@angular/router';
import { import {
AppsPageComponent,
AppAreaComponent, AppAreaComponent,
AppsPageComponent,
ClientsPageComponent, ClientsPageComponent,
ContributorsPageComponent, ContributorsPageComponent,
DashboardPageComponent, DashboardPageComponent,
InternalAreaComponent,
HomePageComponent, HomePageComponent,
InternalAreaComponent,
LanguagesPageComponent, LanguagesPageComponent,
LogoutPageComponent, LogoutPageComponent,
NotFoundPageComponent, NotFoundPageComponent,

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

@ -13,9 +13,9 @@
</div> </div>
<div class="client-name"> <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"> <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> </div>
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
@ -39,10 +39,10 @@
<tr> <tr>
<td>Client Id:</td> <td>Client Id:</td>
<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>
<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> <i class="icon-copy"></i>
</button> </button>
</td> </td>
@ -50,10 +50,10 @@
<tr> <tr>
<td>Client Secret:</td> <td>Client Secret:</td>
<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>
<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> <i class="icon-copy"></i>
</button> </button>
</td> </td>

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

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

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

@ -10,15 +10,9 @@ import * as Ng2Forms from '@angular/forms';
import { import {
AccessTokenDto, AccessTokenDto,
AppsStoreService,
AppClientDto, AppClientDto,
AppClientCreateDto,
AppClientsService,
fadeAnimation, fadeAnimation,
ModalView, ModalView
Notification,
NotificationService,
TitleService
} from 'shared'; } from 'shared';
@Ng2.Component({ @Ng2.Component({
@ -29,99 +23,59 @@ import {
fadeAnimation fadeAnimation
] ]
}) })
export class ClientComponent { export class ClientComponent implements Ng2.OnChanges {
private oldName: string;
public isRenaming = false; public isRenaming = false;
public appClientToken: AccessTokenDto; public appClientToken: AccessTokenDto;
@Ng2.Input('appName') @Ng2.Output()
public appName: string; public renamed = new Ng2.EventEmitter<string>();
@Ng2.Input('client') @Ng2.Input()
public client: AppClientDto; public client: AppClientDto;
@Ng2.ViewChild('inputId')
public inputId: Ng2.ElementRef;
@Ng2.ViewChild('inputSecret')
public inputSecret: Ng2.ElementRef;
public modalDialog = new ModalView(); public modalDialog = new ModalView();
constructor( public get clientName() {
private readonly appClientsService: AppClientsService, return this.client.name || this.client.id;
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() { public get clientId() {
this.client.name = this.oldName; return this.client.id + ':' + this.client.secret;
this.isRenaming = false;
} }
public stopRename() { public get clientSecret() {
this.client.name = this.client.name || this.client.id; return this.client.secret;
this.isRenaming = false;
} }
public startRename() { public renameForm =
this.oldName = this.client.name; this.formBuilder.group({
name: ['']
});
this.isRenaming = true; constructor(
private readonly formBuilder: Ng2Forms.FormBuilder
) {
} }
public createToken(client: AppClientDto) { public ngOnChanges() {
this.appClientsService.createToken(this.appName, client) this.renameForm.controls['name'].setValue(this.clientName);
.subscribe(token => {
this.appClientToken = token;
this.modalDialog.show();
}, error => {
this.notifications.notify(Notification.error('Failed to retrieve access token. Please retry.'));
});
} }
public copyId() { public cancelRename() {
this.copyToClipbord(this.inputId.nativeElement); this.isRenaming = false;
} }
public copySecret() { public startRename() {
this.copyToClipbord(this.inputSecret.nativeElement); this.isRenaming = true;
} }
private copyToClipbord(element: HTMLInputElement | HTMLTextAreaElement) { public rename() {
const currentFocus: any = document.activeElement;
const prevSelectionStart = element.selectionStart;
const prevSelectionEnd = element.selectionEnd;
element.focus();
element.setSelectionRange(0, element.value.length);
try { try {
document.execCommand('copy'); this.renamed.emit(this.renameForm.controls['name'].value);
} catch (e) { } finally {
console.log('Copy failed'); this.isRenaming = false;
}
if (currentFocus && typeof currentFocus.focus === 'function') {
currentFocus.focus();
} }
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">
<div class="layout-left"> <div class="layout-left">
<sqx-left-menu></sqx-left-menu> <sqx-left-menu></sqx-left-menu>
@ -14,7 +16,7 @@
</div> </div>
<div *ngFor="let client of appClients"> <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>
<div class="table-items-footer"> <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 * as Ng2Forms from '@angular/forms';
import { import {
AppsStoreService,
AppClientDto, AppClientDto,
AppClientCreateDto,
AppClientsService, AppClientsService,
Notification, AppComponentBase,
AppsStoreService,
ArrayHelper,
CreateAppClientDto,
NotificationService, NotificationService,
TitleService UpdateAppClientDto,
UsersProviderService
} from 'shared'; } from 'shared';
function rename(client: AppClientDto, name: string) {
return new AppClientDto(client.id, client.secret, name, client.expiresUtc);
};
@Ng2.Component({ @Ng2.Component({
selector: 'sqx-clients-page', selector: 'sqx-clients-page',
styles, styles,
template template
}) })
export class ClientsPageComponent implements Ng2.OnInit { export class ClientsPageComponent extends AppComponentBase implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string | null = null;
public appClients: AppClientDto[]; public appClients: AppClientDto[];
public createForm = public createForm =
@ -39,47 +42,44 @@ export class ClientsPageComponent implements Ng2.OnInit {
]] ]]
}); });
constructor( constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly titles: TitleService,
private readonly appsStore: AppsStoreService,
private readonly appClientsService: AppClientsService, private readonly appClientsService: AppClientsService,
private readonly formBuilder: Ng2Forms.FormBuilder, private readonly formBuilder: Ng2Forms.FormBuilder
private readonly notifications: NotificationService
) { ) {
} super(apps, notifications, users);
public ngOnDestroy() {
this.appSubscription.unsubscribe();
} }
public ngOnInit() { 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(); this.load();
} }
});
}
public load() { public load() {
this.appClientsService.getClients(this.appName) this.appName()
.subscribe(clients => { .switchMap(app => this.appClientsService.getClients(app).retry(2))
this.appClients = clients; .subscribe(dtos => {
this.appClients = dtos;
}, error => { }, error => {
this.notifications.notify(Notification.error(error.displayMessage)); this.notifyError(error);
this.appClients = [];
}); });
} }
public revokeClient(client: AppClientDto) { 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(() => { .subscribe(() => {
this.appClients.splice(this.appClients.indexOf(client), 1); this.appClients = ArrayHelper.replace(this.appClients, client, rename(client, name));
}, error => { }, 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) { if (this.createForm.valid) {
this.createForm.disable(); 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) this.appName()
.subscribe(client => { .switchMap(app => this.appClientsService.postClient(app, dto))
this.appClients.push(client); .subscribe(dto => {
this.appClients = ArrayHelper.push(this.appClients, dto);
this.reset(); this.reset();
}, error => { }, error => {
this.notifications.notify(Notification.error(error.displayMessage)); this.notifyError(error);
this.reset(); 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">
<div class="layout-left"> <div class="layout-left">
<sqx-left-menu></sqx-left-menu> <sqx-left-menu></sqx-left-menu>
@ -22,16 +24,16 @@
<template ngFor let-contributor [ngForOf]="appContributors"> <template ngFor let-contributor [ngForOf]="appContributors">
<tr> <tr>
<td> <td>
<img class="user-picture" [attr.src]="pictureUrl(contributor) | async" /> <img class="user-picture" [attr.src]="userPicture(contributor.contributorId) | async" />
</td> </td>
<td> <td>
<span class="user-name">{{displayName(contributor) | async}}</span> <span class="user-name">{{userName(contributor.contributorId) | async}}</span>
</td> </td>
<td> <td>
<span class="user-email">{{email(contributor) | async}}</span> <span class="user-email">{{userEmail(contributor.contributorId) | async}}</span>
</td> </td>
<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> <option *ngFor="let permission of usersPermissions">{{permission}}</option>
</select> </select>
</td> </td>

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

@ -17,9 +17,15 @@
.user { .user {
&-picture { &-picture {
& {
@include circle(2.2rem); @include circle(2.2rem);
} }
&:not([src]) {
@include opacity(0);
}
}
&-name, &-name,
&-email { &-email {
@include truncate; @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 * as Ng2 from '@angular/core';
import { Observable, Subscription } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
AppComponentBase,
AppContributorDto, AppContributorDto,
AppContributorsService, AppContributorsService,
AppsStoreService, AppsStoreService,
ArrayHelper,
AuthService, AuthService,
AutocompleteItem, AutocompleteItem,
AutocompleteSource, AutocompleteSource,
Notification,
NotificationService, NotificationService,
TitleService,
UserDto, UserDto,
UsersService, UsersProviderService,
UsersProviderService UsersService
} from 'shared'; } from 'shared';
class UsersDataSource implements AutocompleteSource { 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({ @Ng2.Component({
selector: 'sqx-contributor-page', selector: 'sqx-contributor-page',
styles, styles,
template template
}) })
export class ContributorsPageComponent implements Ng2.OnInit { export class ContributorsPageComponent extends AppComponentBase implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string;
public appContributors: AppContributorDto[] = []; public appContributors: AppContributorDto[] = [];
public currentUserId: string;
public selectedUserName: string | null = null; public selectedUserName: string | null = null;
public selectedUser: UserDto | null = null; public selectedUser: UserDto | null = null;
public currrentUserId: string;
public usersDataSource: UsersDataSource; public usersDataSource: UsersDataSource;
public usersPermissions = [ public usersPermissions = [
'Owner', 'Owner',
@ -74,42 +75,29 @@ export class ContributorsPageComponent implements Ng2.OnInit {
'Editor' 'Editor'
]; ];
constructor( constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly titles: TitleService,
private readonly authService: AuthService,
private readonly appsStore: AppsStoreService,
private readonly appContributorsService: AppContributorsService, private readonly appContributorsService: AppContributorsService,
private readonly usersProvider: UsersProviderService,
private readonly usersService: UsersService, 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.usersDataSource = new UsersDataSource(usersService, this);
this.appSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.currrentUserId = this.authService.user.id; this.currentUserId = this.authService.user.id;
this.appSubscription =
this.appsStore.selectedApp.subscribe(app => {
if (app) {
this.appName = app.name;
this.titles.setTitle('{appName} | Settings | Contributors', { appName: app.name });
this.load(); this.load();
} }
});
}
public load() { public load() {
this.appContributorsService.getContributors(this.appName).retry(2) this.appName()
.subscribe(contributors => { .switchMap(app => this.appContributorsService.getContributors(app).retry(2))
this.appContributors = contributors; .subscribe(dtos => {
this.appContributors = dtos;
}, error => { }, 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.selectedUser = null;
this.selectedUserName = null; this.selectedUserName = null;
this.appContributorsService.postContributor(this.appName, contributor) this.appName()
.switchMap(app => this.appContributorsService.postContributor(app, contributor))
.subscribe(() => { .subscribe(() => {
this.appContributors.push(contributor); this.appContributors = ArrayHelper.push(this.appContributors, contributor);
}, error => { }, error => {
this.notifications.notify(Notification.error(error.displayMessage)); this.notifyError(error);
}); });
} }
public removeContributor(contributor: AppContributorDto) { public changePermission(contributor: AppContributorDto, permission: string) {
this.appContributorsService.deleteContributor(this.appName, contributor.contributorId) const newContributor = changePermission(contributor, permission);
this.appName()
.switchMap(app => this.appContributorsService.postContributor(app, newContributor))
.subscribe(() => { .subscribe(() => {
this.appContributors.splice(this.appContributors.indexOf(contributor), 1); this.appContributors = ArrayHelper.replace(this.appContributors, contributor, newContributor);
}, error => { }, error => {
this.notifications.notify(Notification.error(error.displayMessage)); this.notifyError(error);
}); });
} }
public changePermission(contributor: AppContributorDto) { public removeContributor(contributor: AppContributorDto) {
this.appContributorsService.postContributor(this.appName, contributor) this.appName()
.catch(error => { .switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId))
this.notifications.notify(Notification.error(error.displayMessage)); .subscribe(() => {
this.appContributors = ArrayHelper.push(this.appContributors, contributor);
return Observable.of(true); }, error => {
}).subscribe(); this.notifyError(error);
});
} }
public selectUser(selection: UserDto) { public selectUser(selection: UserDto) {
this.selectedUser = selection; 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">
<div class="layout-left"> <div class="layout-left">
<sqx-left-menu></sqx-left-menu> <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 * as Ng2 from '@angular/core';
import { import {
AddAppLanguageDto,
AppComponentBase,
AppLanguageDto, AppLanguageDto,
AppLanguagesService, AppLanguagesService,
AppsStoreService, AppsStoreService,
ArrayHelper,
LanguageDto, LanguageDto,
LanguageService, LanguageService,
Notification,
NotificationService, NotificationService,
TitleService UpdateAppLanguageDto,
UsersProviderService
} from 'shared'; } from 'shared';
@Ng2.Component({ @Ng2.Component({
@ -23,10 +26,7 @@ import {
styles, styles,
template template
}) })
export class LanguagesPageComponent implements Ng2.OnInit { export class LanguagesPageComponent extends AppComponentBase implements Ng2.OnInit {
private appSubscription: any | null = null;
private appName: string;
public allLanguages: LanguageDto[] = []; public allLanguages: LanguageDto[] = [];
public appLanguages: AppLanguageDto[] = []; 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)); return this.allLanguages.filter(x => !this.appLanguages.find(l => l.iso2Code === x.iso2Code));
} }
constructor( constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly titles: TitleService,
private readonly appsStore: AppsStoreService,
private readonly appLanguagesService: AppLanguagesService, private readonly appLanguagesService: AppLanguagesService,
private readonly languagesService: LanguageService, private readonly languagesService: LanguageService
private readonly notifications: NotificationService
) { ) {
} super(apps, notifications, users);
public ngOnDestroy() {
this.appSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
@ -54,59 +48,59 @@ export class LanguagesPageComponent implements Ng2.OnInit {
.subscribe(languages => { .subscribe(languages => {
this.allLanguages = languages; this.allLanguages = languages;
}, error => { }, 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(); this.load();
} }
});
}
public load() { public load() {
this.appLanguagesService.getLanguages(this.appName).retry(2) this.appName()
.subscribe(appLanguages => { .switchMap(app => this.appLanguagesService.getLanguages(app).retry(2))
this.appLanguages = appLanguages; .subscribe(dtos => {
this.appLanguages = dtos;
}, error => { }, error => {
this.notifications.notify(Notification.error(error.displayMessage)); this.notifyError(error);
}); });
} }
public setMasterLanguage(selectedLanguage: AppLanguageDto) { public addLanguage() {
for (let language of this.appLanguages) { this.appName()
language.isMasterLanguage = false; .switchMap(app => this.appLanguagesService.postLanguages(app, new AddAppLanguageDto(this.selectedLanguage.iso2Code)))
} .subscribe(dto => {
this.appLanguages = ArrayHelper.push(this.appLanguages, dto);
this.appLanguagesService.makeMasterLanguage(this.appName, selectedLanguage.iso2Code)
.subscribe(() => {
selectedLanguage.isMasterLanguage = true;
}, error => { }, error => {
this.notifications.notify(Notification.error(error.displayMessage)); this.notifyError(error);
}); });
this.selectedLanguage = null;
} }
public addLanguage() { public removeLanguage(language: AppLanguageDto) {
this.appLanguagesService.postLanguages(this.appName, this.selectedLanguage.iso2Code) this.appName()
.subscribe(appLanguage => { .switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code))
this.appLanguages.push(appLanguage); .subscribe(dto => {
this.appLanguages = ArrayHelper.push(this.appLanguages, language);
}, error => { }, error => {
this.notifications.notify(Notification.error(error.displayMessage)); this.notifyError(error);
}); });
this.selectedLanguage = null;
} }
public removeLanguage(selectedLanguage: AppLanguageDto) { public setMasterLanguage(language: AppLanguageDto) {
this.appLanguagesService.deleteLanguage(this.appName, selectedLanguage.iso2Code) this.appName()
.subscribe(appLanguage => { .switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, new UpdateAppLanguageDto(true)))
this.appLanguages.splice(this.appLanguages.indexOf(appLanguage), 1); .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 => { }, 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 * as Ng2 from '@angular/core';
import { SqxFrameworkModule } from 'shared';
import { SqxLayoutModule } from 'components/layout'; import { SqxLayoutModule } from 'components/layout';
import { SqxFrameworkModule } from 'shared';
import { import {
AppAreaComponent, AppAreaComponent,
@ -18,8 +18,8 @@ import {
ContributorsPageComponent, ContributorsPageComponent,
DashboardPageComponent, DashboardPageComponent,
InternalAreaComponent, InternalAreaComponent,
LeftMenuComponent,
LanguagesPageComponent, LanguagesPageComponent,
LeftMenuComponent,
SchemasPageComponent SchemasPageComponent
} from './declarations'; } from './declarations';

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

@ -10,8 +10,8 @@ import * as Ng2Forms from '@angular/forms';
import { import {
AppDto, AppDto,
AppCreateDto,
AppsStoreService, AppsStoreService,
CreateAppDto,
fadeAnimation fadeAnimation
} from 'shared'; } from 'shared';
@ -65,7 +65,7 @@ export class AppFormComponent implements Ng2.OnInit {
if (this.createForm.valid) { if (this.createForm.valid) {
this.createForm.disable(); 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) this.appsStore.createApp(dto)
.subscribe(app => { .subscribe(app => {

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

@ -5,8 +5,14 @@ $size-avatar: 2.2rem;
.user { .user {
&-picture { &-picture {
& {
@include circle(2.2rem); @include circle(2.2rem);
} }
&:not([src]) {
@include opacity(0);
}
}
} }
.navbar-nav { .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/validators';
export * from './angular/cloak.directive'; export * from './angular/cloak.directive';
export * from './angular/color-picker.component'; export * from './angular/color-picker.component';
export * from './angular/copy.directive';
export * from './angular/date-time.pipes'; export * from './angular/date-time.pipes';
export * from './angular/drag-model.directive'; export * from './angular/drag-model.directive';
export * from './angular/focus-on-change.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/shortcut.component';
export * from './angular/slider.component'; export * from './angular/slider.component';
export * from './angular/spinner.component'; export * from './angular/spinner.component';
export * from './angular/title.component';
export * from './angular/user-report.component'; export * from './angular/user-report.component';
export * from './configurations'; export * from './configurations';
@ -34,6 +36,7 @@ export * from './services/title.service';
export * from './plattform'; export * from './plattform';
export * from './utils/array-helper';
export * from './utils/color'; export * from './utils/color';
export * from './utils/color-palette'; export * from './utils/color-palette';
export * from './utils/date-helper'; export * from './utils/date-helper';

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

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

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

@ -7,7 +7,7 @@
import * as Ng2 from '@angular/core'; import * as Ng2 from '@angular/core';
import { Subject, Observable } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { Vec2 } from './../utils/vec2'; 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 './theme/theme.scss';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module'; 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 * as TypeMoq from 'typemoq';
import { AppMustExistGuard } from './app-must-exist.guard';
import { AppsStoreService } from 'shared'; import { AppsStoreService } from 'shared';
import { AppMustExistGuard } from './app-must-exist.guard';
import { RouterMockup } from './router-mockup'; import { RouterMockup } from './router-mockup';
describe('AppMustExistGuard', () => { describe('AppMustExistGuard', () => {

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

@ -7,9 +7,9 @@
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { AuthService } from 'shared';
import { MustBeAuthenticatedGuard } from './must-be-authenticated.guard'; import { MustBeAuthenticatedGuard } from './must-be-authenticated.guard';
import { RouterMockup } from './router-mockup'; import { RouterMockup } from './router-mockup';
import { AuthService } from 'shared';
describe('MustBeAuthenticatedGuard', () => { describe('MustBeAuthenticatedGuard', () => {
let authService: TypeMoq.Mock<AuthService>; 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 * as TypeMoq from 'typemoq';
import { AuthService } from 'shared';
import { MustBeNotAuthenticatedGuard } from './must-be-not-authenticated.guard'; import { MustBeNotAuthenticatedGuard } from './must-be-not-authenticated.guard';
import { RouterMockup } from './router-mockup'; import { RouterMockup } from './router-mockup';
import { AuthService } from 'shared';
describe('MustBeNotAuthenticatedGuard', () => { describe('MustBeNotAuthenticatedGuard', () => {
let authService: TypeMoq.Mock<AuthService>; 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/app-must-exist.guard';
export * from './guards/must-be-authenticated.guard'; export * from './guards/must-be-authenticated.guard';
export * from './guards/must-be-not-authenticated.guard'; export * from './guards/must-be-not-authenticated.guard';
export * from './services/app-contributors.service'; export * from './services/app-contributors.service';
export * from './services/app-clients.service'; export * from './services/app-clients.service';
export * from './services/app-languages.service'; export * from './services/app-languages.service';
export * from './services/apps-store.service'; export * from './services/apps-store.service';
export * from './services/apps.service'; export * from './services/apps.service';
export * from './services/auth.service'; export * from './services/auth.service';
export * from './services/common';
export * from './services/languages.service'; export * from './services/languages.service';
export * from './services/users-provider.service'; export * from './services/users-provider.service';
export * from './services/users.service'; export * from './services/users.service';
export * from './app-component-base';
export * from 'framework'; 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 * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http'; import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -15,8 +15,9 @@ import {
AppClientDto, AppClientDto,
AppClientsService, AppClientsService,
AuthService, AuthService,
AppClientCreateDto, CreateAppClientDto,
DateTime DateTime,
UpdateAppClientDto
} from './../'; } from './../';
describe('AppClientsService', () => { describe('AppClientsService', () => {
@ -65,9 +66,9 @@ describe('AppClientsService', () => {
}); });
it('should make post request to create client', () => { 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( .returns(() => Observable.of(
new Ng2Http.Response( new Ng2Http.Response(
new Ng2Http.ResponseOptions({ new Ng2Http.ResponseOptions({
@ -84,7 +85,7 @@ describe('AppClientsService', () => {
let client: AppClientDto = null; let client: AppClientDto = null;
appClientsService.postClient('my-app', createClient).subscribe(result => { appClientsService.postClient('my-app', dto).subscribe(result => {
client = result; client = result;
}); });
@ -95,7 +96,9 @@ describe('AppClientsService', () => {
}); });
it('should make put request to rename client', () => { 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( .returns(() => Observable.of(
new Ng2Http.Response( new Ng2Http.Response(
new Ng2Http.ResponseOptions() new Ng2Http.ResponseOptions()
@ -103,7 +106,7 @@ describe('AppClientsService', () => {
)) ))
.verifiable(TypeMoq.Times.once()); .verifiable(TypeMoq.Times.once());
appClientsService.renameClient('my-app', 'client1', 'Client 1'); appClientsService.updateClient('my-app', 'client1', dto);
authService.verifyAll(); 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 { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { handleError } from './errors'; import { handleError } from './common';
export class AppClientDto { export class AppClientDto {
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly secret: string, public readonly secret: string,
public name: string, public readonly name: string,
public readonly expiresUtc: DateTime public readonly expiresUtc: DateTime
) { ) {
} }
} }
export class AppClientCreateDto { export class CreateAppClientDto {
constructor( constructor(
public readonly id: string public readonly id: string
) { ) {
} }
} }
export class UpdateAppClientDto {
constructor(
public readonly name: string
) {
}
}
export class AccessTokenDto { export class AccessTokenDto {
constructor( constructor(
public readonly accessToken: string, public readonly accessToken: string,
@ -68,10 +75,10 @@ export class AppClientsService {
.catch(response => handleError('Failed to load clients. Please reload.', response)); .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`); 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 => response.json())
.map(response => { .map(response => {
return new AppClientDto( return new AppClientDto(
@ -83,10 +90,10 @@ export class AppClientsService {
.catch(response => handleError('Failed to add client. Please reload.', response)); .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}`); 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)); .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 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'); const url = this.apiUrl.buildUrl('identity-server/connect/token');
return this.http.post(url, body, options) 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 * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http'; import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs'; 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 { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { handleError } from './errors'; import { handleError } from './common';
export class AppContributorDto { export class AppContributorDto {
constructor( constructor(
@ -47,10 +47,10 @@ export class AppContributorsService {
.catch(response => handleError('Failed to load contributors. Please reload.', response)); .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`); 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)); .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 * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http'; import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
AddAppLanguageDto,
ApiUrlConfig, ApiUrlConfig,
AppLanguageDto, AppLanguageDto,
AppLanguagesService, AppLanguagesService,
AuthService, AuthService,
UpdateAppLanguageDto
} from './../'; } from './../';
describe('AppLanguagesService', () => { describe('AppLanguagesService', () => {
@ -60,9 +62,9 @@ describe('AppLanguagesService', () => {
}); });
it('should make post request to add language', () => { 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( .returns(() => Observable.of(
new Ng2Http.Response( new Ng2Http.Response(
new Ng2Http.ResponseOptions({ new Ng2Http.ResponseOptions({
@ -77,7 +79,7 @@ describe('AppLanguagesService', () => {
let language: AppLanguageDto; let language: AppLanguageDto;
appLanguagesService.postLanguages('my-app', newLanguage).subscribe(result => { appLanguagesService.postLanguages('my-app', dto).subscribe(result => {
language = result; language = result;
}); });
@ -88,7 +90,9 @@ describe('AppLanguagesService', () => {
}); });
it('should make put request to make master language', () => { 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( .returns(() => Observable.of(
new Ng2Http.Response( new Ng2Http.Response(
new Ng2Http.ResponseOptions() new Ng2Http.ResponseOptions()
@ -96,7 +100,7 @@ describe('AppLanguagesService', () => {
)) ))
.verifiable(TypeMoq.Times.once()); .verifiable(TypeMoq.Times.once());
appLanguagesService.makeMasterLanguage('my-app', 'de'); appLanguagesService.updateLanguage('my-app', 'de', dto);
authService.verifyAll(); 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 { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { handleError } from './errors'; import { handleError } from './common';
export class AppLanguageDto { export class AppLanguageDto {
constructor( constructor(
public readonly iso2Code: string, public readonly iso2Code: string,
public readonly englishName: 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)); .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`); 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 => response.json())
.map(response => { .map(response => {
return new AppLanguageDto( return new AppLanguageDto(
@ -63,14 +77,14 @@ export class AppLanguagesService {
.catch(response => handleError('Failed to add language. Please reload.', response)); .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}`); 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)); .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}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return this.authService.authDelete(url) 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 { Observable } from 'rxjs';
import { import {
AppCreateDto,
AppDto, AppDto,
AppsStoreService,
AppsService, AppsService,
AuthService AppsStoreService,
AuthService,
CreateAppDto,
DateTime
} from './../'; } from './../';
describe('AppsStoreService', () => { describe('AppsStoreService', () => {
const oldApps = [new AppDto('id', 'old-name', null, null, 'Owner')]; const now = DateTime.now();
const newApp = new AppDto('id', 'new-name', null, null, 'Owner');
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 appsService: TypeMoq.Mock<AppsService>;
let authService: TypeMoq.Mock<AuthService>; let authService: TypeMoq.Mock<AuthService>;
@ -109,7 +112,7 @@ describe('AppsStoreService', () => {
result1 = x; result1 = x;
}).unsubscribe(); }).unsubscribe();
store.createApp(new AppCreateDto('new-name')).subscribe(x => { }); store.createApp(new CreateAppDto('new-name'), now).subscribe(x => { });
store.apps.subscribe(x => { store.apps.subscribe(x => {
result2 = x; result2 = x;
@ -134,7 +137,7 @@ describe('AppsStoreService', () => {
let result: AppDto[] = null; 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 => { store.apps.subscribe(x => {
result = x; result = x;

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

@ -14,19 +14,21 @@ import {
} from 'rxjs'; } from 'rxjs';
import { import {
AppCreateDto,
AppDto, AppDto,
AppsService AppsService,
CreateAppDto
} from './apps.service'; } from './apps.service';
import { DateTime } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@Ng2.Injectable() @Ng2.Injectable()
export class AppsStoreService { export class AppsStoreService {
private lastApps: AppDto[] | null = null;
private isAuthenticated = false;
private readonly apps$ = new Subject<AppDto[]>(); private readonly apps$ = new Subject<AppDto[]>();
private readonly appName$ = new BehaviorSubject<string | null>(null); private readonly appName$ = new BehaviorSubject<string | null>(null);
private lastApps: AppDto[] | null = null;
private isAuthenticated = false;
private readonly appsPublished$ = private readonly appsPublished$ =
this.apps$ this.apps$
@ -88,8 +90,16 @@ export class AppsStoreService {
return this.selectedApp.take(1).map(app => app !== null).toPromise(); return this.selectedApp.take(1).map(app => app !== null).toPromise();
} }
public createApp(appToCreate: AppCreateDto): Observable<AppDto> { public createApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> {
return this.appService.postApp(appToCreate).do(app => { 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) { if (this.lastApps && app) {
this.apps$.next(this.lastApps.concat([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 * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http'; import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
ApiUrlConfig, ApiUrlConfig,
AppCreateDto,
AppDto, AppDto,
AppsService, AppsService,
AuthService, AuthService,
DateTime CreateAppDto,
DateTime,
EntityCreatedDto
} from './../'; } from './../';
describe('AppsService', () => { describe('AppsService', () => {
@ -66,8 +67,7 @@ describe('AppsService', () => {
}); });
it('should make post request to create app', () => { it('should make post request to create app', () => {
const createApp = new AppCreateDto('new-app'); const createApp = new CreateAppDto('new-app');
const now = DateTime.now();
authService.setup(x => x.authPost('http://service/p/api/apps', TypeMoq.It.isValue(createApp))) authService.setup(x => x.authPost('http://service/p/api/apps', TypeMoq.It.isValue(createApp)))
.returns(() => Observable.of( .returns(() => Observable.of(
@ -81,13 +81,13 @@ describe('AppsService', () => {
)) ))
.verifiable(TypeMoq.Times.once()); .verifiable(TypeMoq.Times.once());
let newApp: AppDto = null; let newCreated: EntityCreatedDto = null;
appsService.postApp(createApp, now).subscribe(result => { appsService.postApp(createApp).subscribe(result => {
newApp = result; newCreated = result;
}).unsubscribe(); }).unsubscribe();
expect(newApp).toEqual(new AppDto('123', 'new-app', now, now, 'Owner')); expect(newCreated).toEqual(new EntityCreatedDto('123'));
authService.verifyAll(); 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 { ApiUrlConfig, DateTime } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { handleError } from './errors'; import { EntityCreatedDto, handleError } from './common';
export class AppDto { export class AppDto {
constructor( constructor(
@ -24,7 +24,7 @@ export class AppDto {
} }
} }
export class AppCreateDto { export class CreateAppDto {
constructor( constructor(
public readonly name: string public readonly name: string
) { ) {
@ -60,20 +60,13 @@ export class AppsService {
.catch(response => handleError('Failed to load apps. Please reload.', response)); .catch(response => handleError('Failed to load apps. Please reload.', response));
} }
public postApp(app: AppCreateDto, now?: DateTime): Observable<AppDto> { public postApp(dto: CreateAppDto): Observable<EntityCreatedDto> {
now = now || DateTime.now();
const url = this.apiUrl.buildUrl('api/apps'); 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 => response.json())
.map(response => { .map(response => {
return new AppDto( return new EntityCreatedDto(response.id);
response.id,
app.name,
now,
now,
'Owner');
}) })
.catch(response => handleError('Failed to create app. Please reload.', response)); .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; return;
} }
Log.logger = console;
this.userManager = new UserManager({ this.userManager = new UserManager({
client_id: 'squidex-frontend', client_id: 'squidex-frontend',
scope: 'squidex-api openid profile squidex-profile', 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'; import { Observable } from 'rxjs';
export class EntityCreatedDto {
constructor(
public readonly id: string
) {
}
}
export class ErrorDto { export class ErrorDto {
public get displayMessage(): string { public get displayMessage(): string {
let result = this.message; 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 * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http'; import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';

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

@ -6,12 +6,13 @@
*/ */
import * as Ng2 from '@angular/core'; import * as Ng2 from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework'; import { ApiUrlConfig } from 'framework';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { handleError } from './errors'; import { handleError } from './common';
export class LanguageDto { export class LanguageDto {
constructor( 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) { if (!result) {
const request = const request =
this.usersService.getUser(id).retry(2) this.usersService.getUser(id).retry(2)
.catch(err => {
return Observable.of(new UserDto('NOT FOUND', 'NOT FOUND', 'NOT FOUND', ''));
})
.map(u => { .map(u => {
if (this.authService.user && u.id === this.authService.user.id) { if (this.authService.user && u.id === this.authService.user.id) {
return new UserDto(u.id, u.email, 'Me', u.pictureUrl); 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 * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import * as TypeMoq from 'typemoq';
import * as Ng2Http from '@angular/http'; import * as Ng2Http from '@angular/http';
import * as TypeMoq from 'typemoq';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';

2
src/Squidex/app/vendor.ts

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

4
src/Squidex/tslint.json

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

Loading…
Cancel
Save