Browse Source

Tests improved.

pull/363/head
Sebastian 7 years ago
parent
commit
45b0f61a7b
  1. 42
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  2. 13
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  3. 8
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  4. 10
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  5. 59
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs
  6. 46
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  7. 16
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  8. 38
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs
  9. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs
  10. 2
      src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  11. 4
      src/Squidex/app/features/rules/pages/rules/rules-page.component.scss
  12. 2
      src/Squidex/app/features/settings/pages/more/more-page.component.ts
  13. 8
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  14. 93
      src/Squidex/app/shared/services/apps.service.spec.ts
  15. 29
      src/Squidex/app/shared/services/apps.service.ts
  16. 2
      src/Squidex/app/shared/services/users.service.spec.ts
  17. 2
      src/Squidex/app/shared/services/users.service.ts
  18. 51
      src/Squidex/app/shared/state/apps.state.spec.ts
  19. 36
      src/Squidex/app/shared/state/apps.state.ts
  20. 45
      src/Squidex/app/shared/state/languages.state.spec.ts
  21. 2
      src/Squidex/app/shared/state/languages.state.ts
  22. 4
      src/Squidex/app/shell/pages/app/left-menu.component.html

42
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Shared; using Squidex.Shared;
@ -41,12 +42,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("apps/{app}/clients/")] [Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 200)] [ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsRead)] [ApiPermission(Permissions.AppClientsRead)]
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetClients(string app) public IActionResult GetClients(string app)
{ {
var response = App.Clients.Select(ClientDto.FromKvp).ToArray(); var response = ClientsDto.FromApp(App, this);
Response.Headers[HeaderNames.ETag] = App.Version.ToString(); Response.Headers[HeaderNames.ETag] = App.Version.ToString();
@ -60,6 +61,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="request">Client object that needs to be added to the app.</param> /// <param name="request">Client object that needs to be added to the app.</param>
/// <returns> /// <returns>
/// 201 => Client generated. /// 201 => Client generated.
/// 400 => Client request not valid.
/// 404 => App not found. /// 404 => App not found.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
@ -68,16 +70,15 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("apps/{app}/clients/")] [Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto), 201)] [ProducesResponseType(typeof(ClientsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppClientsCreate)] [ApiPermission(Permissions.AppClientsCreate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostClient(string app, [FromBody] CreateClientDto request) public async Task<IActionResult> PostClient(string app, [FromBody] CreateClientDto request)
{ {
var command = request.ToCommand(); var command = request.ToCommand();
await CommandBus.PublishAsync(command); var response = await InvokeCommandAsync(command);
var response = ClientDto.FromCommand(command);
return CreatedAtAction(nameof(GetClients), new { app }, response); return CreatedAtAction(nameof(GetClients), new { app }, response);
} }
@ -89,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="clientId">The id of the client that must be updated.</param> /// <param name="clientId">The id of the client that must be updated.</param>
/// <param name="request">Client object that needs to be updated.</param> /// <param name="request">Client object that needs to be updated.</param>
/// <returns> /// <returns>
/// 204 => Client updated. /// 200 => Client updated.
/// 400 => Client request not valid. /// 400 => Client request not valid.
/// 404 => Client or app not found. /// 404 => Client or app not found.
/// </returns> /// </returns>
@ -98,13 +99,17 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks> /// </remarks>
[HttpPut] [HttpPut]
[Route("apps/{app}/clients/{clientId}/")] [Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppClientsUpdate)] [ApiPermission(Permissions.AppClientsUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateClientDto request) public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateClientDto request)
{ {
await CommandBus.PublishAsync(request.ToCommand(clientId)); var command = request.ToCommand(clientId);
var response = await InvokeCommandAsync(command);
return NoContent(); return Ok(response);
} }
/// <summary> /// <summary>
@ -113,7 +118,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="clientId">The id of the client that must be deleted.</param> /// <param name="clientId">The id of the client that must be deleted.</param>
/// <returns> /// <returns>
/// 204 => Client revoked. /// 200 => Client revoked.
/// 404 => Client or app not found. /// 404 => Client or app not found.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
@ -121,13 +126,26 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks> /// </remarks>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/clients/{clientId}/")] [Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsDelete)] [ApiPermission(Permissions.AppClientsDelete)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteClient(string app, string clientId) public async Task<IActionResult> DeleteClient(string app, string clientId)
{ {
await CommandBus.PublishAsync(new RevokeClient { Id = clientId }); var command = new RevokeClient { Id = clientId };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<ClientsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = ClientsDto.FromApp(result, this);
return NoContent(); return response;
} }
} }
} }

13
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -109,11 +109,20 @@ namespace Squidex.Areas.Api.Controllers.Apps
public async Task<IActionResult> DeleteContributor(string app, string id) public async Task<IActionResult> DeleteContributor(string app, string id)
{ {
var command = new RemoveContributor { ContributorId = id }; var command = new RemoveContributor { ContributorId = id };
var context = await CommandBus.PublishAsync(command);
var response = ContributorsDto.FromApp(context.Result<IAppEntity>(), appPlansProvider, this, false); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
private async Task<ContributorsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = ContributorsDto.FromApp(result, appPlansProvider, this, false);
return response;
}
} }
} }

8
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{ {
var command = request.ToCommand(); var command = request.ToCommand();
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetLanguages), new { app }, response); return CreatedAtAction(nameof(GetLanguages), new { app }, response);
} }
@ -98,7 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{ {
var command = request.ToCommand(ParseLanguage(language)); var command = request.ToCommand(ParseLanguage(language));
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
@ -121,12 +121,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
{ {
var command = new RemoveLanguage { Language = ParseLanguage(language) }; var command = new RemoveLanguage { Language = ParseLanguage(language) };
var response = await InvokeCommandAsync(app, command); var response = await InvokeCommandAsync(command);
return Ok(response); return Ok(response);
} }
private async Task<AppLanguagesDto> InvokeCommandAsync(string app, ICommand command) private async Task<AppLanguagesDto> InvokeCommandAsync(ICommand command)
{ {
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);

10
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -84,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("apps/")] [Route("apps/")]
[ProducesResponseType(typeof(AppCreatedDto), 201)] [ProducesResponseType(typeof(AppDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)] [ProducesResponseType(typeof(ErrorDto), 409)]
[ApiPermission] [ApiPermission]
@ -93,8 +94,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
{ {
var context = await CommandBus.PublishAsync(request.ToCommand()); var context = await CommandBus.PublishAsync(request.ToCommand());
var result = context.Result<EntityCreatedResult<Guid>>(); var userOrClientId = HttpContext.User.UserOrClientId();
var response = AppCreatedDto.FromResult(request.Name, result, appPlansProvider); var userPermissions = HttpContext.Permissions();
var result = context.Result<IAppEntity>();
var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this);
return CreatedAtAction(nameof(GetApps), response); return CreatedAtAction(nameof(GetApps), response);
} }

59
src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs

@ -1,59 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppCreatedDto
{
/// <summary>
/// Id of the created entity.
/// </summary>
[Required]
public string Id { get; set; }
/// <summary>
/// The permission level of the user.
/// </summary>
public string[] Permissions { get; set; }
/// <summary>
/// The new version of the entity.
/// </summary>
public long Version { get; set; }
/// <summary>
/// Gets the current plan name.
/// </summary>
public string PlanName { get; set; }
/// <summary>
/// Gets the next plan name.
/// </summary>
public string PlanUpgrade { get; set; }
public static AppCreatedDto FromResult(string name, EntityCreatedResult<Guid> result, IAppPlansProvider apps)
{
var response = new AppCreatedDto
{
Id = result.IdOrValue.ToString(),
Permissions = Role.CreateOwner(name).Permissions.ToIds().ToArray(),
PlanName = apps.GetPlan(null)?.Name,
PlanUpgrade = apps.GetPlanUpgrade(null)?.Name,
Version = result.Version
};
return response;
}
}
}

46
src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -8,9 +8,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using NodaTime; using NodaTime;
using Squidex.Areas.Api.Controllers.Assets; using Squidex.Areas.Api.Controllers.Assets;
using Squidex.Areas.Api.Controllers.Backups; using Squidex.Areas.Api.Controllers.Backups;
using Squidex.Areas.Api.Controllers.Ping;
using Squidex.Areas.Api.Controllers.Plans; using Squidex.Areas.Api.Controllers.Plans;
using Squidex.Areas.Api.Controllers.Rules; using Squidex.Areas.Api.Controllers.Rules;
using Squidex.Areas.Api.Controllers.Schemas; using Squidex.Areas.Api.Controllers.Schemas;
@ -59,6 +61,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string[] Permissions { get; set; } public string[] Permissions { get; set; }
/// <summary>
/// Indicates if the user can access the api.
/// </summary>
public bool CanAccessApi { get; set; }
/// <summary>
/// Indicates if the user can access at least one content.
/// </summary>
public bool CanAccessContent { get; set; }
/// <summary> /// <summary>
/// Gets the current plan name. /// Gets the current plan name.
/// </summary> /// </summary>
@ -70,6 +82,26 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string PlanUpgrade { get; set; } public string PlanUpgrade { get; set; }
public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller) public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans, ApiController controller)
{
var permissions = GetPermissions(app, userId, userPermissions);
var result = SimpleMapper.Map(app, new AppDto());
result.Permissions = permissions.ToIds().ToArray();
result.PlanName = plans.GetPlanForApp(app)?.Name;
result.CanAccessApi = controller.HasPermission(AllPermissions.AppApi, app.Name, "*", permissions);
result.CanAccessContent = controller.HasPermission(AllPermissions.AppContentsRead, app.Name, "*", permissions);
if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name))
{
result.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
}
return CreateLinks(result, controller, permissions);
}
private static PermissionSet GetPermissions(IAppEntity app, string userId, PermissionSet userPermissions)
{ {
var permissions = new List<Permission>(); var permissions = new List<Permission>();
@ -83,23 +115,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
permissions.AddRange(userPermissions.ToAppPermissions(app.Name)); permissions.AddRange(userPermissions.ToAppPermissions(app.Name));
} }
var result = SimpleMapper.Map(app, new AppDto()); return new PermissionSet(permissions);
result.Permissions = permissions.ToArray(x => x.Id);
result.PlanName = plans.GetPlanForApp(app)?.Name;
if (controller.HasPermission(AllPermissions.AppPlansChange, app.Name))
{
result.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
}
return CreateLinks(result, controller, new PermissionSet(permissions));
} }
private static AppDto CreateLinks(AppDto result, ApiController controller, PermissionSet permissions) private static AppDto CreateLinks(AppDto result, ApiController controller, PermissionSet permissions)
{ {
var values = new { app = result.Name }; var values = new { app = result.Name };
result.AddGetLink("ping", controller.Url<PingController>(x => nameof(x.GetAppPing), values));
if (controller.HasPermission(AllPermissions.AppDelete, result.Name, permissions: permissions)) if (controller.HasPermission(AllPermissions.AppDelete, result.Name, permissions: permissions))
{ {
result.AddDeleteLink("delete", controller.Url<AppsController>(x => nameof(x.DeleteApp), values)); result.AddDeleteLink("delete", controller.Url<AppsController>(x => nameof(x.DeleteApp), values));

16
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -5,16 +5,14 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Roles = Squidex.Domain.Apps.Core.Apps.Role; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
public sealed class ClientDto public sealed class ClientDto : Resource
{ {
/// <summary> /// <summary>
/// The client id. /// The client id.
@ -39,14 +37,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string Role { get; set; } public string Role { get; set; }
public static ClientDto FromKvp(KeyValuePair<string, AppClient> kvp) public static ClientDto FromClient(string id, AppClient client, ApiController controller, string app)
{ {
return SimpleMapper.Map(kvp.Value, new ClientDto { Id = kvp.Key }); var result = SimpleMapper.Map(client, new ClientDto { Id = id });
return CreateLinks(result, controller, app);
} }
public static ClientDto FromCommand(AttachClient command) private static ClientDto CreateLinks(ClientDto result, ApiController controller, string app)
{ {
return SimpleMapper.Map(command, new ClientDto { Name = command.Id, Role = Roles.Editor }); return result;
} }
} }
} }

38
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientsDto.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ClientsDto : Resource
{
/// <summary>
/// The clients.
/// </summary>
[Required]
public ClientDto[] Items { get; set; }
public static ClientsDto FromApp(IAppEntity app, ApiController controller)
{
var result = new ClientsDto
{
Items = app.Clients.Select(x => ClientDto.FromClient(x.Key, x.Value, controller, app.Name)).ToArray()
};
return CreateLinks(result, controller, app.Name);
}
private static ClientsDto CreateLinks(ClientsDto result, ApiController controller, string app)
{
return result;
}
}
}

6
src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs

@ -7,6 +7,8 @@
using Squidex.Areas.Api.Controllers.Backups; using Squidex.Areas.Api.Controllers.Backups;
using Squidex.Areas.Api.Controllers.EventConsumers; using Squidex.Areas.Api.Controllers.EventConsumers;
using Squidex.Areas.Api.Controllers.Languages;
using Squidex.Areas.Api.Controllers.Ping;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
@ -18,6 +20,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{ {
var result = new ResourcesDto(); var result = new ResourcesDto();
result.AddGetLink("ping", controller.Url<PingController>(x => nameof(x.GetPing)));
result.AddGetLink("languages", controller.Url<LanguagesController>(x => nameof(x.GetLanguages)));
if (controller.HasPermission(Permissions.AdminEventsRead)) if (controller.HasPermission(Permissions.AdminEventsRead))
{ {
result.AddGetLink("admin/eventConsumers", controller.Url<EventConsumersController>(x => nameof(x.GetEventConsumers))); result.AddGetLink("admin/eventConsumers", controller.Url<EventConsumersController>(x => nameof(x.GetEventConsumers)));

2
src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -63,7 +63,7 @@ namespace Squidex.Areas.Api.Controllers.Users
/// 200 => User resources returned. /// 200 => User resources returned.
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("user/resources/")] [Route("/")]
[ProducesResponseType(typeof(ResourcesDto), 200)] [ProducesResponseType(typeof(ResourcesDto), 200)]
[ApiPermission] [ApiPermission]
public IActionResult GetUserResources() public IActionResult GetUserResources()

4
src/Squidex/app/features/rules/pages/rules/rules-page.component.scss

@ -1,10 +1,6 @@
@import '_vars'; @import '_vars';
@import '_mixins'; @import '_mixins';
sqx-toggle {
display: inline-block;
}
.rule-element { .rule-element {
display: block; display: block;
} }

2
src/Squidex/app/features/settings/pages/more/more-page.component.ts

@ -23,7 +23,7 @@ export class MorePageComponent {
} }
public archiveApp() { public archiveApp() {
this.appsState.delete(this.appsState.appName) this.appsState.delete(this.appsState.selectedAppState!)
.subscribe(() => { .subscribe(() => {
this.router.navigate(['/app']); this.router.navigate(['/app']);
}); });

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

@ -101,7 +101,9 @@ describe('AppLanguagesService', () => {
let languages: AppLanguagesDto; let languages: AppLanguagesDto;
appLanguagesService.putLanguage('my-app', resource, dto, version).subscribe(); appLanguagesService.putLanguage('my-app', resource, dto, version).subscribe(result => {
languages = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/languages/de'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/languages/de');
@ -122,7 +124,7 @@ describe('AppLanguagesService', () => {
const resource: Resource = { const resource: Resource = {
_links: { _links: {
update: { method: 'PUT', href: 'api/apps/my-app/languages/de' } update: { method: 'DELETE', href: 'api/apps/my-app/languages/de' }
} }
}; };
@ -149,7 +151,7 @@ describe('AppLanguagesService', () => {
function languagesResponse(...codes: string[]) { function languagesResponse(...codes: string[]) {
return { return {
items: codes.map((code, i) => ({ items: codes.map((code, i) => ({
code: code, iso2Code: code,
englishName: code, englishName: code,
isMaster: i === 0, isMaster: i === 0,
isOptional: i % 2 === 1, isOptional: i % 2 === 1,

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

@ -11,10 +11,10 @@ import { inject, TestBed } from '@angular/core/testing';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
AppCreatedDto,
AppDto, AppDto,
AppsService, AppsService,
DateTime DateTime,
Resource
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('AppsService', () => { describe('AppsService', () => {
@ -50,31 +50,11 @@ describe('AppsService', () => {
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([ req.flush([
{ appResponse(12),
id: '123', appResponse(13)
name: 'name1',
permissions: ['Owner'],
created: '2016-01-01',
lastModified: '2016-02-02',
planName: 'Free',
planUpgrade: 'Basic'
},
{
id: '456',
name: 'name2',
permissions: ['Owner'],
created: '2017-01-01',
lastModified: '2017-02-02',
planName: 'Basic',
planUpgrade: 'Enterprise'
}
]); ]);
expect(apps!).toEqual( expect(apps!).toEqual([createApp(12), createApp(13)]);
[
new AppDto('123', 'name1', ['Owner'], DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'),
new AppDto('456', 'name2', ['Owner'], DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise')
]);
})); }));
it('should make post request to create app', it('should make post request to create app',
@ -82,7 +62,7 @@ describe('AppsService', () => {
const dto = { name: 'new-app' }; const dto = { name: 'new-app' };
let app: AppCreatedDto; let app: AppDto;
appsService.postApp(dto).subscribe(result => { appsService.postApp(dto).subscribe(result => {
app = result; app = result;
@ -93,25 +73,21 @@ describe('AppsService', () => {
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ req.flush(appResponse(12));
id: '123',
permissions: ['Reader'],
planName: 'Basic',
planUpgrade: 'Enterprise'
});
expect(app!).toEqual({ expect(app!).toEqual(createApp(12));
id: '123',
permissions: ['Reader'],
planName: 'Basic',
planUpgrade: 'Enterprise'
});
})); }));
it('should make delete request to archive app', it('should make delete request to archive app',
inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => {
appsService.deleteApp('my-app').subscribe(); const resource: Resource = {
_links: {
delete: { method: 'DELETE', href: '/api/apps/my-app' }
}
};
appsService.deleteApp(resource).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app'); const req = httpMock.expectOne('http://service/p/api/apps/my-app');
@ -120,4 +96,39 @@ describe('AppsService', () => {
req.flush({}); req.flush({});
})); }));
});
function appResponse(id: number, suffix = '') {
return {
id: `id${id}`,
name: `name${id}${suffix}`,
permissions: ['Owner'],
created: `${id % 1000 + 2000}-12-12T10:10`,
lastModified: `${id % 1000 + 2000}-11-11T10:10`,
canAccessApi: id % 2 === 0,
canAccessContent: id % 2 === 0,
planName: 'Free',
planUpgrade: 'Basic',
_links: {
schemas: { method: 'GET', href: '/schemas' }
}
};
}
});
export function createApp(id: number, suffix = '') {
const result = new AppDto(
`id${id}`,
`name${id}${suffix}`,
['Owner'],
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10`),
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10`),
id % 2 === 0,
id % 2 === 0,
'Free',
'Basic'
);
result._links['schemas'] = { method: 'GET', href: '/schemas' };
return result;
}

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

@ -15,6 +15,7 @@ import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
pretifyError, pretifyError,
Resource,
ResourceLinks, ResourceLinks,
withLinks withLinks
} from '@app/framework'; } from '@app/framework';
@ -28,6 +29,8 @@ export class AppDto {
public readonly permissions: string[], public readonly permissions: string[],
public readonly created: DateTime, public readonly created: DateTime,
public readonly lastModified: DateTime, public readonly lastModified: DateTime,
public readonly canAccessApi: boolean,
public readonly canAccessContent: boolean,
public readonly planName?: string, public readonly planName?: string,
public readonly planUpgrade?: string public readonly planUpgrade?: string
) { ) {
@ -39,13 +42,6 @@ export interface CreateAppDto {
readonly template?: string; readonly template?: string;
} }
export interface AppCreatedDto {
readonly id: string;
readonly permissions: string[];
readonly planName?: string;
readonly planUpgrade?: string;
}
@Injectable() @Injectable()
export class AppsService { export class AppsService {
constructor( constructor(
@ -67,22 +63,27 @@ export class AppsService {
pretifyError('Failed to load apps. Please reload.')); pretifyError('Failed to load apps. Please reload.'));
} }
public postApp(dto: CreateAppDto): Observable<AppCreatedDto> { public postApp(dto: CreateAppDto): Observable<AppDto> {
const url = this.apiUrl.buildUrl('api/apps'); const url = this.apiUrl.buildUrl('api/apps');
return this.http.post<AppCreatedDto>(url, dto).pipe( return this.http.post<any>(url, dto).pipe(
map(body => {
return parseApp(body);
}),
tap(() => { tap(() => {
this.analytics.trackEvent('App', 'Created', dto.name); this.analytics.trackEvent('App', 'Created', dto.name);
}), }),
pretifyError('Failed to create app. Please reload.')); pretifyError('Failed to create app. Please reload.'));
} }
public deleteApp(appName: string): Observable<any> { public deleteApp(resource: Resource): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}`); const link = resource._links['delete'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.delete(url).pipe( return this.http.request(link.method, url).pipe(
tap(() => { tap(() => {
this.analytics.trackEvent('App', 'Archived', appName); this.analytics.trackEvent('App', 'Archived');
}), }),
pretifyError('Failed to archive app. Please reload.')); pretifyError('Failed to archive app. Please reload.'));
} }
@ -96,6 +97,8 @@ function parseApp(response: any) {
response.permissions, response.permissions,
DateTime.parseISO(response.created), DateTime.parseISO(response.created),
DateTime.parseISO(response.lastModified), DateTime.parseISO(response.lastModified),
response.canAccessApi,
response.canAccessContent,
response.planName, response.planName,
response.planUpgrade), response.planUpgrade),
response); response);

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

@ -124,7 +124,7 @@ describe('UsersService', () => {
resources = result; resources = result;
}); });
const req = httpMock.expectOne('http://service/p/api/user/resources'); const req = httpMock.expectOne('http://service/p/api');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();

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

@ -67,7 +67,7 @@ export class UsersService {
} }
public getResources(): Observable<ResourcesDto> { public getResources(): Observable<ResourcesDto> {
const url = this.apiUrl.buildUrl(`api/user/resources`); const url = this.apiUrl.buildUrl(`api`);
return this.http.get<any>(url).pipe( return this.http.get<any>(url).pipe(
map(body => { map(body => {

51
src/Squidex/app/shared/state/apps.state.spec.ts

@ -12,19 +12,16 @@ import {
AppDto, AppDto,
AppsService, AppsService,
AppsState, AppsState,
DateTime,
DialogService DialogService
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('AppsState', () => { import { createApp } from '../services/apps.service.spec';
const now = DateTime.now();
const oldApps = [ describe('AppsState', () => {
new AppDto('id1', 'old-name1', ['Owner'], now, now), const app1 = createApp(1);
new AppDto('id2', 'old-name2', ['Owner'], now, now) const app2 = createApp(2);
];
const newApp = new AppDto('id3', 'new-name', ['Owner'], now, now); const newApp = createApp(3);
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
let appsService: IMock<AppsService>; let appsService: IMock<AppsService>;
@ -36,7 +33,7 @@ describe('AppsState', () => {
appsService = Mock.ofType<AppsService>(); appsService = Mock.ofType<AppsService>();
appsService.setup(x => x.getApps()) appsService.setup(x => x.getApps())
.returns(() => of(oldApps)).verifiable(); .returns(() => of([app1, app2])).verifiable();
appsState = new AppsState(appsService.object, dialogs.object); appsState = new AppsState(appsService.object, dialogs.object);
appsState.load().subscribe(); appsState.load().subscribe();
@ -47,18 +44,18 @@ describe('AppsState', () => {
}); });
it('should load apps', () => { it('should load apps', () => {
expect(appsState.snapshot.apps.values).toEqual(oldApps); expect(appsState.snapshot.apps.values).toEqual([app1, app2]);
}); });
it('should select app', () => { it('should select app', () => {
let selectedApp: AppDto; let selectedApp: AppDto;
appsState.select(oldApps[0].name).subscribe(x => { appsState.select(app1.name).subscribe(x => {
selectedApp = x!; selectedApp = x!;
}); });
expect(selectedApp!).toBe(oldApps[0]); expect(selectedApp!).toBe(app1);
expect(appsState.snapshot.selectedApp).toBe(oldApps[0]); expect(appsState.snapshot.selectedApp).toBe(app1);
}); });
it('should return null on select when unselecting user', () => { it('should return null on select when unselecting user', () => {
@ -87,46 +84,46 @@ describe('AppsState', () => {
const request = { ...newApp }; const request = { ...newApp };
appsService.setup(x => x.postApp(request)) appsService.setup(x => x.postApp(request))
.returns(() => of({ ...newApp, permissions: ['Owner'] })).verifiable(); .returns(() => of(newApp)).verifiable();
appsState.create(request, now).subscribe(); appsState.create(request).subscribe();
expect(appsState.snapshot.apps.values).toEqual([newApp, ...oldApps]); expect(appsState.snapshot.apps.values).toEqual([app1, app2, newApp]);
}); });
it('should remove app from snapshot when archived', () => { it('should remove app from snapshot when archived', () => {
const request = { ...newApp }; const request = { ...newApp };
appsService.setup(x => x.postApp(request)) appsService.setup(x => x.postApp(request))
.returns(() => of({ ...newApp, permissions: ['Owner'] })).verifiable(); .returns(() => of(newApp)).verifiable();
appsService.setup(x => x.deleteApp(newApp.name)) appsService.setup(x => x.deleteApp(newApp))
.returns(() => of({})).verifiable(); .returns(() => of({})).verifiable();
appsState.create(request, now).subscribe(); appsState.create(request).subscribe();
const appsAfterCreate = appsState.snapshot.apps.values; const appsAfterCreate = appsState.snapshot.apps.values;
appsState.delete(newApp.name).subscribe(); appsState.delete(newApp).subscribe();
const appsAfterDelete = appsState.snapshot.apps.values; const appsAfterDelete = appsState.snapshot.apps.values;
expect(appsAfterCreate).toEqual([newApp, ...oldApps]); expect(appsAfterCreate).toEqual([app1, app2, newApp]);
expect(appsAfterDelete).toEqual(oldApps); expect(appsAfterDelete).toEqual([app1, app2]);
}); });
it('should selected app from snapshot when archived', () => { it('should remove selected app from snapshot when archived', () => {
const request = { ...newApp }; const request = { ...newApp };
appsService.setup(x => x.postApp(request)) appsService.setup(x => x.postApp(request))
.returns(() => of({ ...newApp, permissions: ['Owner'] })).verifiable(); .returns(() => of(newApp)).verifiable();
appsService.setup(x => x.deleteApp(newApp.name)) appsService.setup(x => x.deleteApp(newApp))
.returns(() => of({})).verifiable(); .returns(() => of({})).verifiable();
appsState.create(request, now).subscribe(); appsState.create(request).subscribe();
appsState.select(newApp.name).subscribe(); appsState.select(newApp.name).subscribe();
appsState.delete(newApp.name).subscribe(); appsState.delete(newApp).subscribe();
expect(appsState.snapshot.selectedApp).toBeNull(); expect(appsState.snapshot.selectedApp).toBeNull();
}); });

36
src/Squidex/app/shared/state/apps.state.ts

@ -10,7 +10,6 @@ import { Observable, of } from 'rxjs';
import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators';
import { import {
DateTime,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
shareSubscribed, shareSubscribed,
@ -18,7 +17,6 @@ import {
} from '@app/framework'; } from '@app/framework';
import { import {
AppCreatedDto,
AppDto, AppDto,
AppsService, AppsService,
CreateAppDto CreateAppDto
@ -42,6 +40,10 @@ export class AppsState extends State<Snapshot> {
return this.snapshot.selectedApp ? this.snapshot.selectedApp.name : ''; return this.snapshot.selectedApp ? this.snapshot.selectedApp.name : '';
} }
public get selectedAppState() {
return this.snapshot.selectedApp;
}
public selectedApp = public selectedApp =
this.changes.pipe(map(s => s.selectedApp), this.changes.pipe(map(s => s.selectedApp),
distinctUntilChanged(sameApp)); distinctUntilChanged(sameApp));
@ -85,9 +87,8 @@ export class AppsState extends State<Snapshot> {
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public create(request: CreateAppDto, now?: DateTime): Observable<AppDto> { public create(request: CreateAppDto): Observable<AppDto> {
return this.appsService.postApp(request).pipe( return this.appsService.postApp(request).pipe(
map(payload => createApp(request, payload, now)),
tap(created => { tap(created => {
this.next(s => { this.next(s => {
const apps = s.apps.push(created).sortByStringAsc(x => x.name); const apps = s.apps.push(created).sortByStringAsc(x => x.name);
@ -98,32 +99,21 @@ export class AppsState extends State<Snapshot> {
shareSubscribed(this.dialogs, { silent: true })); shareSubscribed(this.dialogs, { silent: true }));
} }
public delete(name: string): Observable<any> { public delete(app: AppDto): Observable<any> {
return this.appsService.deleteApp(name).pipe( return this.appsService.deleteApp(app).pipe(
tap(() => { tap(() => {
this.next(s => { this.next(s => {
const apps = s.apps.filter(x => x.name !== name); const apps = s.apps.filter(x => x.name !== app.name);
const selectedApp = s.selectedApp && s.selectedApp.name === name ? null : s.selectedApp; const selectedApp =
s.selectedApp &&
s.selectedApp.name === app.name ?
null :
s.selectedApp;
return { ...s, apps, selectedApp }; return { ...s, apps, selectedApp };
}); });
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
}
function createApp(request: CreateAppDto, response: AppCreatedDto, now?: DateTime) {
now = now || DateTime.now();
const app = new AppDto(
response.id,
request.name,
response.permissions,
now,
now,
response.planName,
response.planUpgrade);
return app;
} }

45
src/Squidex/app/shared/state/languages.state.spec.ts

@ -9,6 +9,7 @@ import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { import {
AppLanguagesPayload,
AppLanguagesService, AppLanguagesService,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
@ -71,11 +72,11 @@ describe('LanguagesState', () => {
expect(languagesState.snapshot.languages.values).toEqual([ expect(languagesState.snapshot.languages.values).toEqual([
{ {
language: oldLanguages.items[0], language: oldLanguages.items[0],
fallbackLanguages: ImmutableArray.empty(), fallbackLanguages: ImmutableArray.of([oldLanguages.items[1]]),
fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1]]) fallbackLanguagesNew: ImmutableArray.empty()
}, { }, {
language: oldLanguages.items[1], language: oldLanguages.items[1],
fallbackLanguages: ImmutableArray.of([oldLanguages[0]]), fallbackLanguages: ImmutableArray.of([oldLanguages.items[0]]),
fallbackLanguagesNew: ImmutableArray.empty() fallbackLanguagesNew: ImmutableArray.empty()
} }
]); ]);
@ -108,15 +109,7 @@ describe('LanguagesState', () => {
languagesState.add(languageIT).subscribe(); languagesState.add(languageIT).subscribe();
expect(languagesState.snapshot.languages.values).toEqual([ expectUpdated(updated);
{
language: oldLanguages[0],
fallbackLanguages: ImmutableArray.empty(),
fallbackLanguagesNew: ImmutableArray.empty()
}
]);
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]);
expect(languagesState.snapshot.version).toEqual(newVersion);
}); });
it('should update language in snapshot when updated', () => { it('should update language in snapshot when updated', () => {
@ -124,39 +117,35 @@ describe('LanguagesState', () => {
const request = { isMaster: true, isOptional: false, fallback: [] }; const request = { isMaster: true, isOptional: false, fallback: [] };
languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version)) languagesService.setup(x => x.putLanguage(app, oldLanguages.items[1], request, version))
.returns(() => of(versioned(newVersion, updated))).verifiable(); .returns(() => of(versioned(newVersion, updated))).verifiable();
languagesState.update(oldLanguages[1], request).subscribe(); languagesState.update(oldLanguages.items[1], request).subscribe();
expect(languagesState.snapshot.languages.values).toEqual([ expectUpdated(updated);
{
language: oldLanguages[0],
fallbackLanguages: ImmutableArray.empty(),
fallbackLanguagesNew: ImmutableArray.empty()
}
]);
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]);
expect(languagesState.snapshot.version).toEqual(newVersion);
}); });
it('should remove language from snapshot when deleted', () => { it('should remove language from snapshot when deleted', () => {
const updated = createLanguages('de'); const updated = createLanguages('de');
languagesService.setup(x => x.deleteLanguage(app, oldLanguages[1].iso2Code, version)) languagesService.setup(x => x.deleteLanguage(app, oldLanguages.items[1], version))
.returns(() => of(versioned(newVersion, updated))).verifiable(); .returns(() => of(versioned(newVersion, updated))).verifiable();
languagesState.remove(oldLanguages[1]).subscribe(); languagesState.remove(oldLanguages.items[1]).subscribe();
expectUpdated(updated);
});
function expectUpdated(updated: AppLanguagesPayload) {
expect(languagesState.snapshot.languages.values).toEqual([ expect(languagesState.snapshot.languages.values).toEqual([
{ {
language: oldLanguages[0], language: updated.items[0],
fallbackLanguages: ImmutableArray.empty(), fallbackLanguages: ImmutableArray.empty(),
fallbackLanguagesNew: ImmutableArray.empty() fallbackLanguagesNew: ImmutableArray.empty()
} }
]); ]);
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]); expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageEN, languageIT, languageES]);
expect(languagesState.snapshot.version).toEqual(newVersion); expect(languagesState.snapshot.version).toEqual(newVersion);
}); }
}); });
}); });

2
src/Squidex/app/shared/state/languages.state.ts

@ -182,7 +182,7 @@ export class LanguagesState extends State<Snapshot> {
ImmutableArray.of( ImmutableArray.of(
language.fallback language.fallback
.map(l => languages.find(x => x.iso2Code === l)).filter(x => !!x) .map(l => languages.find(x => x.iso2Code === l)).filter(x => !!x)
.map(x => <AppLanguageDto>x)), .map(l => l!)),
fallbackLanguagesNew: fallbackLanguagesNew:
languages languages
.filter(l => .filter(l =>

4
src/Squidex/app/shell/pages/app/left-menu.component.html

@ -4,7 +4,7 @@
<i class="nav-icon icon-schemas"></i> <div class="nav-text">Schemas</div> <i class="nav-icon icon-schemas"></i> <div class="nav-text">Schemas</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'contents'"> <li class="nav-item" *ngIf="selectedApp.canAccessContent">
<a class="nav-link" routerLink="content" routerLinkActive="active"> <a class="nav-link" routerLink="content" routerLinkActive="active">
<i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div> <i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div>
</a> </a>
@ -24,7 +24,7 @@
<i class="nav-icon icon-settings"></i> <div class="nav-text">Settings</div> <i class="nav-icon icon-settings"></i> <div class="nav-text">Settings</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'api'"> <li class="nav-item" *ngIf="selectedApp.canAccessApi">
<a class="nav-link" routerLink="api" routerLinkActive="active"> <a class="nav-link" routerLink="api" routerLinkActive="active">
<i class="nav-icon icon-api"></i> <div class="nav-text">API</div> <i class="nav-icon icon-api"></i> <div class="nav-text">API</div>
</a> </a>

Loading…
Cancel
Save