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. 89
      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.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
@ -41,12 +42,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpGet]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 200)]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsRead)]
[ApiCosts(0)]
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();
@ -60,6 +61,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="request">Client object that needs to be added to the app.</param>
/// <returns>
/// 201 => Client generated.
/// 400 => Client request not valid.
/// 404 => App not found.
/// </returns>
/// <remarks>
@ -68,16 +70,15 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpPost]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto), 201)]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppClientsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostClient(string app, [FromBody] CreateClientDto request)
{
var command = request.ToCommand();
await CommandBus.PublishAsync(command);
var response = ClientDto.FromCommand(command);
var response = await InvokeCommandAsync(command);
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="request">Client object that needs to be updated.</param>
/// <returns>
/// 204 => Client updated.
/// 200 => Client updated.
/// 400 => Client request not valid.
/// 404 => Client or app not found.
/// </returns>
@ -98,13 +99,17 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpPut]
[Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppClientsUpdate)]
[ApiCosts(1)]
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>
@ -113,7 +118,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param>
/// <param name="clientId">The id of the client that must be deleted.</param>
/// <returns>
/// 204 => Client revoked.
/// 200 => Client revoked.
/// 404 => Client or app not found.
/// </returns>
/// <remarks>
@ -121,13 +126,26 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpDelete]
[Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientsDto), 200)]
[ApiPermission(Permissions.AppClientsDelete)]
[ApiCosts(1)]
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)
{
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);
}
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 response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetLanguages), new { app }, response);
}
@ -98,7 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var command = request.ToCommand(ParseLanguage(language));
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
@ -121,12 +121,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var command = new RemoveLanguage { Language = ParseLanguage(language) };
var response = await InvokeCommandAsync(app, command);
var response = await InvokeCommandAsync(command);
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);

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

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure;
@ -84,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpPost]
[Route("apps/")]
[ProducesResponseType(typeof(AppCreatedDto), 201)]
[ProducesResponseType(typeof(AppDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ApiPermission]
@ -93,8 +94,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var context = await CommandBus.PublishAsync(request.ToCommand());
var result = context.Result<EntityCreatedResult<Guid>>();
var response = AppCreatedDto.FromResult(request.Name, result, appPlansProvider);
var userOrClientId = HttpContext.User.UserOrClientId();
var userPermissions = HttpContext.Permissions();
var result = context.Result<IAppEntity>();
var response = AppDto.FromApp(result, userOrClientId, userPermissions, appPlansProvider, this);
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.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using NodaTime;
using Squidex.Areas.Api.Controllers.Assets;
using Squidex.Areas.Api.Controllers.Backups;
using Squidex.Areas.Api.Controllers.Ping;
using Squidex.Areas.Api.Controllers.Plans;
using Squidex.Areas.Api.Controllers.Rules;
using Squidex.Areas.Api.Controllers.Schemas;
@ -59,6 +61,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
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>
/// Gets the current plan name.
/// </summary>
@ -70,6 +82,26 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string PlanUpgrade { get; set; }
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>();
@ -83,23 +115,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
permissions.AddRange(userPermissions.ToAppPermissions(app.Name));
}
var result = SimpleMapper.Map(app, new AppDto());
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));
return new PermissionSet(permissions);
}
private static AppDto CreateLinks(AppDto result, ApiController controller, PermissionSet permissions)
{
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))
{
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.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
using Roles = Squidex.Domain.Apps.Core.Apps.Role;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ClientDto
public sealed class ClientDto : Resource
{
/// <summary>
/// The client id.
@ -39,14 +37,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
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.EventConsumers;
using Squidex.Areas.Api.Controllers.Languages;
using Squidex.Areas.Api.Controllers.Ping;
using Squidex.Shared;
using Squidex.Web;
@ -18,6 +20,10 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
{
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))
{
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.
/// </returns>
[HttpGet]
[Route("user/resources/")]
[Route("/")]
[ProducesResponseType(typeof(ResourcesDto), 200)]
[ApiPermission]
public IActionResult GetUserResources()

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

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

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

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

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

@ -101,7 +101,9 @@ describe('AppLanguagesService', () => {
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');
@ -122,7 +124,7 @@ describe('AppLanguagesService', () => {
const resource: Resource = {
_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[]) {
return {
items: codes.map((code, i) => ({
code: code,
iso2Code: code,
englishName: code,
isMaster: i === 0,
isOptional: i % 2 === 1,

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

@ -11,10 +11,10 @@ import { inject, TestBed } from '@angular/core/testing';
import {
AnalyticsService,
ApiUrlConfig,
AppCreatedDto,
AppDto,
AppsService,
DateTime
DateTime,
Resource
} from '@app/shared/internal';
describe('AppsService', () => {
@ -50,31 +50,11 @@ describe('AppsService', () => {
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([
{
id: '123',
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'
}
appResponse(12),
appResponse(13)
]);
expect(apps!).toEqual(
[
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')
]);
expect(apps!).toEqual([createApp(12), createApp(13)]);
}));
it('should make post request to create app',
@ -82,7 +62,7 @@ describe('AppsService', () => {
const dto = { name: 'new-app' };
let app: AppCreatedDto;
let app: AppDto;
appsService.postApp(dto).subscribe(result => {
app = result;
@ -93,25 +73,21 @@ describe('AppsService', () => {
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
id: '123',
permissions: ['Reader'],
planName: 'Basic',
planUpgrade: 'Enterprise'
});
req.flush(appResponse(12));
expect(app!).toEqual({
id: '123',
permissions: ['Reader'],
planName: 'Basic',
planUpgrade: 'Enterprise'
});
expect(app!).toEqual(createApp(12));
}));
it('should make delete request to archive app',
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');
@ -120,4 +96,39 @@ describe('AppsService', () => {
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,
DateTime,
pretifyError,
Resource,
ResourceLinks,
withLinks
} from '@app/framework';
@ -28,6 +29,8 @@ export class AppDto {
public readonly permissions: string[],
public readonly created: DateTime,
public readonly lastModified: DateTime,
public readonly canAccessApi: boolean,
public readonly canAccessContent: boolean,
public readonly planName?: string,
public readonly planUpgrade?: string
) {
@ -39,13 +42,6 @@ export interface CreateAppDto {
readonly template?: string;
}
export interface AppCreatedDto {
readonly id: string;
readonly permissions: string[];
readonly planName?: string;
readonly planUpgrade?: string;
}
@Injectable()
export class AppsService {
constructor(
@ -67,22 +63,27 @@ export class AppsService {
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');
return this.http.post<AppCreatedDto>(url, dto).pipe(
return this.http.post<any>(url, dto).pipe(
map(body => {
return parseApp(body);
}),
tap(() => {
this.analytics.trackEvent('App', 'Created', dto.name);
}),
pretifyError('Failed to create app. Please reload.'));
}
public deleteApp(appName: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}`);
public deleteApp(resource: Resource): Observable<any> {
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(() => {
this.analytics.trackEvent('App', 'Archived', appName);
this.analytics.trackEvent('App', 'Archived');
}),
pretifyError('Failed to archive app. Please reload.'));
}
@ -96,6 +97,8 @@ function parseApp(response: any) {
response.permissions,
DateTime.parseISO(response.created),
DateTime.parseISO(response.lastModified),
response.canAccessApi,
response.canAccessContent,
response.planName,
response.planUpgrade),
response);

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

@ -124,7 +124,7 @@ describe('UsersService', () => {
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.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> {
const url = this.apiUrl.buildUrl(`api/user/resources`);
const url = this.apiUrl.buildUrl(`api`);
return this.http.get<any>(url).pipe(
map(body => {

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

@ -12,19 +12,16 @@ import {
AppDto,
AppsService,
AppsState,
DateTime,
DialogService
} from '@app/shared/internal';
describe('AppsState', () => {
const now = DateTime.now();
import { createApp } from '../services/apps.service.spec';
const oldApps = [
new AppDto('id1', 'old-name1', ['Owner'], now, now),
new AppDto('id2', 'old-name2', ['Owner'], now, now)
];
describe('AppsState', () => {
const app1 = createApp(1);
const app2 = createApp(2);
const newApp = new AppDto('id3', 'new-name', ['Owner'], now, now);
const newApp = createApp(3);
let dialogs: IMock<DialogService>;
let appsService: IMock<AppsService>;
@ -36,7 +33,7 @@ describe('AppsState', () => {
appsService = Mock.ofType<AppsService>();
appsService.setup(x => x.getApps())
.returns(() => of(oldApps)).verifiable();
.returns(() => of([app1, app2])).verifiable();
appsState = new AppsState(appsService.object, dialogs.object);
appsState.load().subscribe();
@ -47,18 +44,18 @@ describe('AppsState', () => {
});
it('should load apps', () => {
expect(appsState.snapshot.apps.values).toEqual(oldApps);
expect(appsState.snapshot.apps.values).toEqual([app1, app2]);
});
it('should select app', () => {
let selectedApp: AppDto;
appsState.select(oldApps[0].name).subscribe(x => {
appsState.select(app1.name).subscribe(x => {
selectedApp = x!;
});
expect(selectedApp!).toBe(oldApps[0]);
expect(appsState.snapshot.selectedApp).toBe(oldApps[0]);
expect(selectedApp!).toBe(app1);
expect(appsState.snapshot.selectedApp).toBe(app1);
});
it('should return null on select when unselecting user', () => {
@ -87,46 +84,46 @@ describe('AppsState', () => {
const request = { ...newApp };
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', () => {
const request = { ...newApp };
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();
appsState.create(request, now).subscribe();
appsState.create(request).subscribe();
const appsAfterCreate = appsState.snapshot.apps.values;
appsState.delete(newApp.name).subscribe();
appsState.delete(newApp).subscribe();
const appsAfterDelete = appsState.snapshot.apps.values;
expect(appsAfterCreate).toEqual([newApp, ...oldApps]);
expect(appsAfterDelete).toEqual(oldApps);
expect(appsAfterCreate).toEqual([app1, app2, newApp]);
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 };
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();
appsState.create(request, now).subscribe();
appsState.create(request).subscribe();
appsState.select(newApp.name).subscribe();
appsState.delete(newApp.name).subscribe();
appsState.delete(newApp).subscribe();
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 {
DateTime,
DialogService,
ImmutableArray,
shareSubscribed,
@ -18,7 +17,6 @@ import {
} from '@app/framework';
import {
AppCreatedDto,
AppDto,
AppsService,
CreateAppDto
@ -42,6 +40,10 @@ export class AppsState extends State<Snapshot> {
return this.snapshot.selectedApp ? this.snapshot.selectedApp.name : '';
}
public get selectedAppState() {
return this.snapshot.selectedApp;
}
public selectedApp =
this.changes.pipe(map(s => s.selectedApp),
distinctUntilChanged(sameApp));
@ -85,9 +87,8 @@ export class AppsState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public create(request: CreateAppDto, now?: DateTime): Observable<AppDto> {
public create(request: CreateAppDto): Observable<AppDto> {
return this.appsService.postApp(request).pipe(
map(payload => createApp(request, payload, now)),
tap(created => {
this.next(s => {
const apps = s.apps.push(created).sortByStringAsc(x => x.name);
@ -98,13 +99,17 @@ export class AppsState extends State<Snapshot> {
shareSubscribed(this.dialogs, { silent: true }));
}
public delete(name: string): Observable<any> {
return this.appsService.deleteApp(name).pipe(
public delete(app: AppDto): Observable<any> {
return this.appsService.deleteApp(app).pipe(
tap(() => {
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 };
});
@ -112,18 +117,3 @@ export class AppsState extends State<Snapshot> {
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 {
AppLanguagesPayload,
AppLanguagesService,
DialogService,
ImmutableArray,
@ -71,11 +72,11 @@ describe('LanguagesState', () => {
expect(languagesState.snapshot.languages.values).toEqual([
{
language: oldLanguages.items[0],
fallbackLanguages: ImmutableArray.empty(),
fallbackLanguagesNew: ImmutableArray.of([oldLanguages[1]])
fallbackLanguages: ImmutableArray.of([oldLanguages.items[1]]),
fallbackLanguagesNew: ImmutableArray.empty()
}, {
language: oldLanguages.items[1],
fallbackLanguages: ImmutableArray.of([oldLanguages[0]]),
fallbackLanguages: ImmutableArray.of([oldLanguages.items[0]]),
fallbackLanguagesNew: ImmutableArray.empty()
}
]);
@ -108,15 +109,7 @@ describe('LanguagesState', () => {
languagesState.add(languageIT).subscribe();
expect(languagesState.snapshot.languages.values).toEqual([
{
language: oldLanguages[0],
fallbackLanguages: ImmutableArray.empty(),
fallbackLanguagesNew: ImmutableArray.empty()
}
]);
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]);
expect(languagesState.snapshot.version).toEqual(newVersion);
expectUpdated(updated);
});
it('should update language in snapshot when updated', () => {
@ -124,39 +117,35 @@ describe('LanguagesState', () => {
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();
languagesState.update(oldLanguages[1], request).subscribe();
languagesState.update(oldLanguages.items[1], request).subscribe();
expect(languagesState.snapshot.languages.values).toEqual([
{
language: oldLanguages[0],
fallbackLanguages: ImmutableArray.empty(),
fallbackLanguagesNew: ImmutableArray.empty()
}
]);
expect(languagesState.snapshot.allLanguagesNew.values).toEqual([languageDE, languageIT, languageES]);
expect(languagesState.snapshot.version).toEqual(newVersion);
expectUpdated(updated);
});
it('should remove language from snapshot when deleted', () => {
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();
languagesState.remove(oldLanguages[1]).subscribe();
languagesState.remove(oldLanguages.items[1]).subscribe();
expectUpdated(updated);
});
function expectUpdated(updated: AppLanguagesPayload) {
expect(languagesState.snapshot.languages.values).toEqual([
{
language: oldLanguages[0],
language: updated.items[0],
fallbackLanguages: 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);
});
}
});
});

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

@ -182,7 +182,7 @@ export class LanguagesState extends State<Snapshot> {
ImmutableArray.of(
language.fallback
.map(l => languages.find(x => x.iso2Code === l)).filter(x => !!x)
.map(x => <AppLanguageDto>x)),
.map(l => l!)),
fallbackLanguagesNew:
languages
.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>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'contents'">
<li class="nav-item" *ngIf="selectedApp.canAccessContent">
<a class="nav-link" routerLink="content" routerLinkActive="active">
<i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div>
</a>
@ -24,7 +24,7 @@
<i class="nav-icon icon-settings"></i> <div class="nav-text">Settings</div>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'api'">
<li class="nav-item" *ngIf="selectedApp.canAccessApi">
<a class="nav-link" routerLink="api" routerLinkActive="active">
<i class="nav-icon icon-api"></i> <div class="nav-text">API</div>
</a>

Loading…
Cancel
Save