Browse Source

A lot of settings migrated.

pull/363/head
Sebastian 7 years ago
parent
commit
48beff2420
  1. 41
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  2. 15
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs
  3. 38
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternsDto.cs
  4. 4
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  5. 2
      src/Squidex/app/framework/utils/version.ts
  6. 11
      src/Squidex/app/shared/services/clients.service.spec.ts
  7. 2
      src/Squidex/app/shared/services/clients.service.ts
  8. 2
      src/Squidex/app/shared/services/contributors.service.spec.ts
  9. 2
      src/Squidex/app/shared/services/contributors.service.ts
  10. 118
      src/Squidex/app/shared/services/patterns.service.spec.ts
  11. 75
      src/Squidex/app/shared/services/patterns.service.ts
  12. 115
      src/Squidex/app/shared/services/roles.service.spec.ts
  13. 72
      src/Squidex/app/shared/services/roles.service.ts
  14. 31
      src/Squidex/app/shared/state/clients.state.spec.ts
  15. 2
      src/Squidex/app/shared/state/clients.state.ts
  16. 19
      src/Squidex/app/shared/state/contributors.state.spec.ts
  17. 2
      src/Squidex/app/shared/state/contributors.state.ts
  18. 8
      src/Squidex/app/shared/state/languages.state.spec.ts
  19. 2
      src/Squidex/app/shared/state/languages.state.ts
  20. 57
      src/Squidex/app/shared/state/patterns.state.spec.ts
  21. 60
      src/Squidex/app/shared/state/patterns.state.ts
  22. 2
      src/Squidex/app/shared/state/plans.state.ts
  23. 59
      src/Squidex/app/shared/state/roles.state.spec.ts
  24. 61
      src/Squidex/app/shared/state/roles.state.ts

41
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -6,11 +6,11 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Linq;
using System.Threading.Tasks; 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;
@ -42,12 +42,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks> /// </remarks>
[HttpGet] [HttpGet]
[Route("apps/{app}/patterns/")] [Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto[]), 200)] [ProducesResponseType(typeof(AppPatternsDto), 200)]
[ApiPermission(Permissions.AppCommon)] [ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetPatterns(string app) public IActionResult GetPatterns(string app)
{ {
var response = App.Patterns.Select(AppPatternDto.FromKvp).OrderBy(x => x.Name).ToArray(); var response = AppPatternsDto.FromApp(App, this);
Response.Headers[HeaderNames.ETag] = App.Version.ToString(); Response.Headers[HeaderNames.ETag] = App.Version.ToString();
@ -66,16 +66,15 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </returns> /// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/patterns/")] [Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto), 201)] [ProducesResponseType(typeof(AppPatternsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppPatternsCreate)] [ApiPermission(Permissions.AppPatternsCreate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request) public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request)
{ {
var command = request.ToAddCommand(); var command = request.ToAddCommand();
await CommandBus.PublishAsync(command); var response = await InvokeCommandAsync(command);
var response = AppPatternDto.FromCommand(command);
return CreatedAtAction(nameof(GetPatterns), new { app }, response); return CreatedAtAction(nameof(GetPatterns), new { app }, response);
} }
@ -93,14 +92,17 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/patterns/{id}/")] [Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(AppPatternDto), 201)] [ProducesResponseType(typeof(AppPatternsDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppPatternsUpdate)] [ApiPermission(Permissions.AppPatternsUpdate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request) public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request)
{ {
await CommandBus.PublishAsync(request.ToUpdateCommand(id)); var command = request.ToUpdateCommand(id);
var response = await InvokeCommandAsync(command);
return NoContent(); return Ok(response);
} }
/// <summary> /// <summary>
@ -109,7 +111,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="id">The id of the pattern to be deleted.</param> /// <param name="id">The id of the pattern to be deleted.</param>
/// <returns> /// <returns>
/// 204 => Pattern removed. /// 200 => Pattern removed.
/// 404 => Pattern or app not found. /// 404 => Pattern or app not found.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
@ -117,13 +119,26 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks> /// </remarks>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/patterns/{id}/")] [Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(AppPatternsDto), 200)]
[ApiPermission(Permissions.AppPatternsDelete)] [ApiPermission(Permissions.AppPatternsDelete)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeletePattern(string app, Guid id) public async Task<IActionResult> DeletePattern(string app, Guid id)
{ {
await CommandBus.PublishAsync(new DeletePattern { PatternId = id }); var command = new DeletePattern { PatternId = id };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<AppPatternsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = AppPatternsDto.FromApp(result, this);
return NoContent(); return response;
} }
} }
} }

15
src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs

@ -6,15 +6,14 @@
// ========================================================================== // ==========================================================================
using System; using System;
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 Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
public sealed class AppPatternDto public sealed class AppPatternDto : Resource
{ {
/// <summary> /// <summary>
/// Unique id of the pattern. /// Unique id of the pattern.
@ -38,14 +37,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string Message { get; set; } public string Message { get; set; }
public static AppPatternDto FromKvp(KeyValuePair<Guid, AppPattern> kvp) public static AppPatternDto FromPattern(Guid id, AppPattern pattern, ApiController controller, string app)
{ {
return SimpleMapper.Map(kvp.Value, new AppPatternDto { PatternId = kvp.Key }); var result = SimpleMapper.Map(pattern, new AppPatternDto { PatternId = id });
return result.CreateLinks(controller, app);
} }
public static AppPatternDto FromCommand(AddPattern command) private AppPatternDto CreateLinks(ApiController controller, string app)
{ {
return SimpleMapper.Map(command, new AppPatternDto()); return this;
} }
} }
} }

38
src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternsDto.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 AppPatternsDto : Resource
{
/// <summary>
/// The patterns.
/// </summary>
[Required]
public AppPatternDto[] Items { get; set; }
public static AppPatternsDto FromApp(IAppEntity app, ApiController controller)
{
var result = new AppPatternsDto
{
Items = app.Patterns.Select(x => AppPatternDto.FromPattern(x.Key, x.Value, controller, app.Name)).ToArray()
};
return result.CreateLinks(controller, app.Name);
}
private AppPatternsDto CreateLinks(ApiController controller, string app)
{
return this;
}
}
}

4
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -21,7 +21,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The contributors. /// The contributors.
/// </summary> /// </summary>
[Required] [Required]
public ContributorDto[] Contributors { get; set; } public ContributorDto[] Items { get; set; }
/// <summary> /// <summary>
/// The maximum number of allowed contributors. /// The maximum number of allowed contributors.
@ -40,7 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
var result = new ContributorsDto var result = new ContributorsDto
{ {
Contributors = contributors, Items = contributors,
}; };
if (isInvited) if (isInvited)

2
src/Squidex/app/framework/utils/version.ts

@ -6,6 +6,8 @@
*/ */
export class Version { export class Version {
public static readonly EMPTY = new Version('');
constructor( constructor(
public readonly value: string public readonly value: string
) { ) {

11
src/Squidex/app/shared/services/clients.service.spec.ts

@ -172,7 +172,7 @@ describe('ClientsService', () => {
function clientsResponse(...ids: number[]) { function clientsResponse(...ids: number[]) {
return { return {
contributors: ids.map(id => ({ items: ids.map(id => ({
id: `id${id}`, id: `id${id}`,
name: `Client ${id}`, name: `Client ${id}`,
role: `Role${id}`, role: `Role${id}`,
@ -181,12 +181,8 @@ describe('ClientsService', () => {
update: { method: 'PUT', href: `/clients/id${id}` } update: { method: 'PUT', href: `/clients/id${id}` }
} }
})), })),
maxContributors: ids.length * 13,
_links: { _links: {
create: { method: 'POST', href: '/contributors' } create: { method: 'POST', href: '/clients' }
},
_meta: {
isInvited: 'true'
} }
}; };
} }
@ -205,9 +201,6 @@ export function createClients(...ids: number[]): ClientsPayload {
)), )),
_links: { _links: {
create: { method: 'POST', href: '/clients' } create: { method: 'POST', href: '/clients' }
},
_meta: {
isInvited: 'true'
} }
}; };
} }

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

@ -139,7 +139,7 @@ export class ClientsService {
} }
function parseClients(response: any): ClientsPayload { function parseClients(response: any): ClientsPayload {
const items: any[] = response; const items: any[] = response.items;
const clients = items.map(item => const clients = items.map(item =>
withLinks( withLinks(

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

@ -119,7 +119,7 @@ describe('ContributorsService', () => {
function contributorsResponse(...ids: number[]) { function contributorsResponse(...ids: number[]) {
return { return {
contributors: ids.map(id => ({ items: ids.map(id => ({
contributorId: `id${id}`, role: id % 2 === 0 ? 'Owner' : 'Developer', contributorId: `id${id}`, role: id % 2 === 0 ? 'Owner' : 'Developer',
_links: { _links: {
update: { method: 'PUT', href: `/contributors/id${id}` } update: { method: 'PUT', href: `/contributors/id${id}` }

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

@ -96,7 +96,7 @@ export class ContributorsService {
} }
function parseContributors(response: any) { function parseContributors(response: any) {
const items: any[] = response.contributors; const items: any[] = response.items;
const contributors = items.map(item => const contributors = items.map(item =>
withLinks( withLinks(

118
src/Squidex/app/shared/services/patterns.service.spec.ts

@ -13,8 +13,11 @@ import {
ApiUrlConfig, ApiUrlConfig,
PatternDto, PatternDto,
PatternsDto, PatternsDto,
PatternsPayload,
PatternsService, PatternsService,
Version Resource,
Version,
withLinks
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('PatternsService', () => { describe('PatternsService', () => {
@ -51,31 +54,13 @@ describe('PatternsService', () => {
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();
req.flush([ req.flush(patternsResponse(1, 2, 3), {
{
patternId: '1',
pattern: '[0-9]',
name: 'Number',
message: 'Message1'
}, {
patternId: '2',
pattern: '[0-9]*',
name: 'Numbers',
message: 'Message2'
}
], {
headers: { headers: {
etag: '2' etag: '2'
} }
}); });
expect(patterns!).toEqual({ expect(patterns!).toEqual({payload: createPatterns(1, 2, 3), version: new Version('2') });
payload: [
new PatternDto('1', 'Number', '[0-9]', 'Message1'),
new PatternDto('2', 'Numbers', '[0-9]*', 'Message2')
],
version: new Version('2')
});
})); }));
it('should make post request to add pattern', it('should make post request to add pattern',
@ -83,10 +68,10 @@ describe('PatternsService', () => {
const dto = { name: 'Number', pattern: '[0-9]' }; const dto = { name: 'Number', pattern: '[0-9]' };
let pattern: PatternDto; let patterns: PatternsDto;
patternService.postPattern('my-app', dto, version).subscribe(result => { patternService.postPattern('my-app', dto, version).subscribe(result => {
pattern = result.payload; patterns = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns');
@ -94,14 +79,13 @@ describe('PatternsService', () => {
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({ req.flush(patternsResponse(1, 2, 3), {
name: 'Number', headers: {
patternId: '1', etag: '2'
pattern: '[0-9]', }
message: 'Message1'
}); });
expect(pattern!).toEqual(new PatternDto('1', 'Number', '[0-9]', 'Message1')); expect(patterns!).toEqual({payload: createPatterns(1, 2, 3), version: new Version('2') });
})); }));
it('should make put request to update pattern', it('should make put request to update pattern',
@ -109,26 +93,92 @@ describe('PatternsService', () => {
const dto = { name: 'Number', pattern: '[0-9]' }; const dto = { name: 'Number', pattern: '[0-9]' };
patternService.putPattern('my-app', '1', dto, version).subscribe(); const resource: Resource = {
_links: {
update: { method: 'PUT', href: '/api/apps/my-app/patterns/1' }
}
};
let patterns: PatternsDto;
patternService.putPattern('my-app', resource, dto, version).subscribe(result => {
patterns = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns/1'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns/1');
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({}); req.flush(patternsResponse(1, 2, 3), {
headers: {
etag: '2'
}
});
expect(patterns!).toEqual({payload: createPatterns(1, 2, 3), version: new Version('2') });
})); }));
it('should make delete request to remove pattern', it('should make delete request to remove pattern',
inject([PatternsService, HttpTestingController], (patternService: PatternsService, httpMock: HttpTestingController) => { inject([PatternsService, HttpTestingController], (patternService: PatternsService, httpMock: HttpTestingController) => {
patternService.deletePattern('my-app', '1', version).subscribe(); const resource: Resource = {
_links: {
delete: { method: 'DELETE', href: '/api/apps/my-app/patterns/1' }
}
};
let patterns: PatternsDto;
patternService.deletePattern('my-app', resource, version).subscribe(result => {
patterns = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns/1'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/patterns/1');
expect(req.request.method).toEqual('DELETE'); expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({}); req.flush(patternsResponse(1, 2, 3), {
headers: {
etag: '2'
}
});
expect(patterns!).toEqual({payload: createPatterns(1, 2, 3), version: new Version('2') });
})); }));
});
function patternsResponse(...ids: number[]) {
return {
items: ids.map(id => ({
name: `Name${id}`,
patternId: `id${id}`,
pattern: `Pattern${id}`,
message: `Message${id}`,
_links: {
update: { method: 'PUT', href: `/patterns/id${id}` }
}
})),
_links: {
create: { method: 'POST', href: '/patterns' }
}
};
}
});
export function createPatterns(...ids: number[]): PatternsPayload {
return {
items: ids.map(id =>
withLinks(
new PatternDto(`id${id}`, `Name${id}`, `Pattern${id}`, `Message${id}`),
{
_links: {
update: { method: 'PUT', href: `/patterns/id${id}` }
}
}
)),
_links: {
create: { method: 'POST', href: '/patterns' }
}
};
}

75
src/Squidex/app/shared/services/patterns.service.ts

@ -15,22 +15,28 @@ import {
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
mapVersioned, mapVersioned,
Model,
pretifyError, pretifyError,
Resource,
ResourceLinks,
Version, Version,
Versioned Versioned,
withLinks
} from '@app/framework'; } from '@app/framework';
export type PatternsDto = Versioned<PatternDto[]>; export type PatternsDto = Versioned<PatternsPayload>;
export type PatternsPayload = {
items: PatternDto[]
} & Resource;
export class PatternDto {
public readonly _links: ResourceLinks = {};
export class PatternDto extends Model<PatternDto> {
constructor( constructor(
public readonly id: string, public readonly id: string,
public readonly name: string, public readonly name: string,
public readonly pattern: string, public readonly pattern: string,
public readonly message?: string public readonly message?: string
) { ) {
super();
} }
} }
@ -54,33 +60,17 @@ export class PatternsService {
return HTTP.getVersioned(this.http, url).pipe( return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => { mapVersioned(({ body }) => {
const items: any[] = body; return parsePatterns(body);
const patterns =
items.map(item =>
new PatternDto(
item.patternId,
item.name,
item.pattern,
item.message));
return patterns;
}), }),
pretifyError('Failed to add pattern. Please reload.')); pretifyError('Failed to add pattern. Please reload.'));
} }
public postPattern(appName: string, dto: EditPatternDto, version: Version): Observable<Versioned<PatternDto>> { public postPattern(appName: string, dto: EditPatternDto, version: Version): Observable<PatternsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns`);
return HTTP.postVersioned(this.http, url, dto, version).pipe( return HTTP.postVersioned(this.http, url, dto, version).pipe(
mapVersioned(({ body }) => { mapVersioned(({ body }) => {
const pattern = new PatternDto( return parsePatterns(body);
body.patternId,
body.name,
body.pattern,
body.message);
return pattern;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Patterns', 'Created', appName); this.analytics.trackEvent('Patterns', 'Created', appName);
@ -88,23 +78,48 @@ export class PatternsService {
pretifyError('Failed to add pattern. Please reload.')); pretifyError('Failed to add pattern. Please reload.'));
} }
public putPattern(appName: string, id: string, dto: EditPatternDto, version: Version): Observable<Versioned<any>> { public putPattern(appName: string, resource: Resource, dto: EditPatternDto, version: Version): Observable<PatternsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns/${id}`); const link = resource._links['update'];
return HTTP.putVersioned(this.http, url, dto, version).pipe( const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
mapVersioned(({ body }) => {
return parsePatterns(body);
}),
tap(() => { tap(() => {
this.analytics.trackEvent('Patterns', 'Updated', appName); this.analytics.trackEvent('Patterns', 'Updated', appName);
}), }),
pretifyError('Failed to update pattern. Please reload.')); pretifyError('Failed to update pattern. Please reload.'));
} }
public deletePattern(appName: string, id: string, version: Version): Observable<Versioned<any>> { public deletePattern(appName: string, resource: Resource, version: Version): Observable<PatternsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/patterns/${id}`); const link = resource._links['delete'];
return HTTP.deleteVersioned(this.http, url, version).pipe( const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
mapVersioned(({ body }) => {
return parsePatterns(body);
}),
tap(() => { tap(() => {
this.analytics.trackEvent('Patterns', 'Configured', appName); this.analytics.trackEvent('Patterns', 'Configured', appName);
}), }),
pretifyError('Failed to remove pattern. Please reload.')); pretifyError('Failed to remove pattern. Please reload.'));
} }
}
function parsePatterns(response: any) {
const items: any[] = response.items;
const patterns = items.map(item =>
withLinks(
new PatternDto(
item.patternId,
item.name,
item.pattern,
item.message),
item));
return withLinks({ items: patterns, _links: {} }, response);
} }

115
src/Squidex/app/shared/services/roles.service.spec.ts

@ -11,10 +11,13 @@ import { inject, TestBed } from '@angular/core/testing';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
Resource,
RoleDto, RoleDto,
RolesDto, RolesDto,
RolesPayload,
RolesService, RolesService,
Version Version,
withLinks
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('RolesService', () => { describe('RolesService', () => {
@ -70,31 +73,13 @@ describe('RolesService', () => {
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();
req.flush({ req.flush(rolesResponse(2, 4), {
roles: [{
name: 'Role1',
numClients: 3,
numContributors: 5,
permissions: ['P1']
}, {
name: 'Role2',
numClients: 7,
numContributors: 9,
permissions: ['P2']
}]
}, {
headers: { headers: {
etag: '2' etag: '2'
} }
}); });
expect(roles!).toEqual({ expect(roles!).toEqual({ payload: createRoles(2, 4), version: new Version('2') });
payload: [
new RoleDto('Role1', 3, 5, ['P1']),
new RoleDto('Role2', 7, 9, ['P2'])
],
version: new Version('2')
});
})); }));
it('should make post request to add role', it('should make post request to add role',
@ -102,10 +87,10 @@ describe('RolesService', () => {
const dto = { name: 'Role3' }; const dto = { name: 'Role3' };
let role: RoleDto; let roles: RolesDto;
roleService.postRole('my-app', dto, version).subscribe(result => { roleService.postRole('my-app', dto, version).subscribe(result => {
role = result.payload; roles = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles');
@ -113,9 +98,13 @@ describe('RolesService', () => {
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({}); req.flush(rolesResponse(2, 4), {
headers: {
etag: '2'
}
});
expect(role!).toEqual(new RoleDto('Role3', 0, 0, [])); expect(roles!).toEqual({ payload: createRoles(2, 4), version: new Version('2') });
})); }));
it('should make put request to update role', it('should make put request to update role',
@ -123,26 +112,92 @@ describe('RolesService', () => {
const dto = { permissions: ['P4', 'P5'] }; const dto = { permissions: ['P4', 'P5'] };
roleService.putRole('my-app', 'role1', dto, version).subscribe(); const resource: Resource = {
_links: {
update: { method: 'PUT', href: '/api/apps/my-app/roles/role1' }
}
};
let roles: RolesDto;
roleService.putRole('my-app', resource, dto, version).subscribe(result => {
roles = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles/role1'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles/role1');
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({}); req.flush(rolesResponse(2, 4), {
headers: {
etag: '2'
}
});
expect(roles!).toEqual({ payload: createRoles(2, 4), version: new Version('2') });
})); }));
it('should make delete request to remove role', it('should make delete request to remove role',
inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => { inject([RolesService, HttpTestingController], (roleService: RolesService, httpMock: HttpTestingController) => {
roleService.deleteRole('my-app', 'role1', version).subscribe(); const resource: Resource = {
_links: {
delete: { method: 'DELETE', href: '/api/apps/my-app/roles/role1' }
}
};
let roles: RolesDto;
roleService.deleteRole('my-app', resource, version).subscribe(result => {
roles = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles/role1'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/roles/role1');
expect(req.request.method).toEqual('DELETE'); expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({}); req.flush(rolesResponse(2, 4), {
headers: {
etag: '2'
}
});
expect(roles!).toEqual({ payload: createRoles(2, 4), version: new Version('2') });
})); }));
});
function rolesResponse(...ids: number[]) {
return {
items: ids.map(id => ({
name: `name${id}`,
numClients: id * 2,
numContributors: id * 3,
permissions: [`permission${id}`],
_links: {
update: { method: 'PUT', href: `/roles/id${id}` }
}
})),
_links: {
create: { method: 'POST', href: '/roles' }
}
};
}
});
export function createRoles(...ids: number[]): RolesPayload {
return {
items: ids.map(id =>
withLinks(
new RoleDto(`name${id}`, id * 2, id * 3, [`permission${id}`]),
{
_links: {
update: { method: 'PUT', href: `/roles/id${id}` }
}
}
)),
_links: {
create: { method: 'POST', href: '/roles' }
}
};
}

72
src/Squidex/app/shared/services/roles.service.ts

@ -15,22 +15,28 @@ import {
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
mapVersioned, mapVersioned,
Model,
pretifyError, pretifyError,
Resource,
ResourceLinks,
Version, Version,
Versioned Versioned,
withLinks
} from '@app/framework'; } from '@app/framework';
export type RolesDto = Versioned<RoleDto[]>; export type RolesDto = Versioned<RolesPayload>;
export type RolesPayload = {
items: RoleDto[]
} & Resource;
export class RoleDto {
public readonly _links: ResourceLinks = {};
export class RoleDto extends Model<RoleDto> {
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly numClients: number, public readonly numClients: number,
public readonly numContributors: number, public readonly numContributors: number,
public readonly permissions: string[] public readonly permissions: string[]
) { ) {
super();
} }
} }
@ -56,28 +62,17 @@ export class RolesService {
return HTTP.getVersioned(this.http, url).pipe( return HTTP.getVersioned(this.http, url).pipe(
mapVersioned(({ body }) => { mapVersioned(({ body }) => {
const items: any[] = body.roles; return parseRoles(body);
const roles = items.map(item =>
new RoleDto(
item.name,
item.numClients,
item.numContributors,
item.permissions));
return roles;
}), }),
pretifyError('Failed to load roles. Please reload.')); pretifyError('Failed to load roles. Please reload.'));
} }
public postRole(appName: string, dto: CreateRoleDto, version: Version): Observable<Versioned<RoleDto>> { public postRole(appName: string, dto: CreateRoleDto, version: Version): Observable<RolesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles`);
return HTTP.postVersioned(this.http, url, dto, version).pipe( return HTTP.postVersioned(this.http, url, dto, version).pipe(
mapVersioned(() => { mapVersioned(({ body }) => {
const role = new RoleDto(dto.name, 0, 0, []); return parseRoles(body);
return role;
}), }),
tap(() => { tap(() => {
this.analytics.trackEvent('Role', 'Created', appName); this.analytics.trackEvent('Role', 'Created', appName);
@ -85,20 +80,30 @@ export class RolesService {
pretifyError('Failed to add role. Please reload.')); pretifyError('Failed to add role. Please reload.'));
} }
public putRole(appName: string, name: string, dto: UpdateRoleDto, version: Version): Observable<Versioned<any>> { public putRole(appName: string, resource: Resource, dto: UpdateRoleDto, version: Version): Observable<RolesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles/${name}`); const link = resource._links['update'];
const url = this.apiUrl.buildUrl(link.href);
return HTTP.putVersioned(this.http, url, dto, version).pipe( return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe(
mapVersioned(({ body }) => {
return parseRoles(body);
}),
tap(() => { tap(() => {
this.analytics.trackEvent('Role', 'Updated', appName); this.analytics.trackEvent('Role', 'Updated', appName);
}), }),
pretifyError('Failed to revoke role. Please reload.')); pretifyError('Failed to revoke role. Please reload.'));
} }
public deleteRole(appName: string, name: string, version: Version): Observable<Versioned<any>> { public deleteRole(appName: string, resource: Resource, version: Version): Observable<RolesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/roles/${name}`); const link = resource._links['delete'];
return HTTP.deleteVersioned(this.http, url, version).pipe( const url = this.apiUrl.buildUrl(link.href);
return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
mapVersioned(({ body }) => {
return parseRoles(body);
}),
tap(() => { tap(() => {
this.analytics.trackEvent('Role', 'Deleted', appName); this.analytics.trackEvent('Role', 'Deleted', appName);
}), }),
@ -111,4 +116,19 @@ export class RolesService {
return this.http.get<string[]>(url).pipe( return this.http.get<string[]>(url).pipe(
pretifyError('Failed to load permissions. Please reload.')); pretifyError('Failed to load permissions. Please reload.'));
} }
}
export function parseRoles(response: any) {
const items: any[] = response.items;
const roles = items.map(item =>
withLinks(
new RoleDto(
item.name,
item.numClients,
item.numContributors,
item.permissions),
item));
return withLinks({ items: roles, _links: {} }, response);
} }

31
src/Squidex/app/shared/state/clients.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 {
ClientsPayload,
ClientsService, ClientsService,
ClientsState, ClientsState,
DialogService, DialogService,
@ -88,8 +89,7 @@ describe('ClientsState', () => {
clientsState.attach(request).subscribe(); clientsState.attach(request).subscribe();
expect(clientsState.snapshot.clients.values).toEqual(updated.items); expectNewClients(updated);
expect(clientsState.snapshot.version).toEqual(newVersion);
}); });
it('should update clients when role updated', () => { it('should update clients when role updated', () => {
@ -97,13 +97,12 @@ describe('ClientsState', () => {
const request = { role: 'Owner' }; const request = { role: 'Owner' };
clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version)) clientsService.setup(x => x.putClient(app, oldClients.items[0], request, version))
.returns(() => of(versioned(newVersion))).verifiable(); .returns(() => of(versioned(newVersion, updated))).verifiable();
clientsState.update(oldClients[0], request).subscribe(); clientsState.update(oldClients.items[0], request).subscribe();
expect(clientsState.snapshot.clients.values).toEqual(updated.items); expectNewClients(updated);
expect(clientsState.snapshot.version).toEqual(newVersion);
}); });
it('should update clients when name updated', () => { it('should update clients when name updated', () => {
@ -112,24 +111,28 @@ describe('ClientsState', () => {
const request = { name: 'NewName' }; const request = { name: 'NewName' };
clientsService.setup(x => x.putClient(app, oldClients.items[0], request, version)) clientsService.setup(x => x.putClient(app, oldClients.items[0], request, version))
.returns(() => of(versioned(newVersion))).verifiable(); .returns(() => of(versioned(newVersion, updated))).verifiable();
clientsState.update(oldClients[0], request).subscribe(); clientsState.update(oldClients.items[0], request).subscribe();
expect(clientsState.snapshot.clients.values).toEqual(updated.items); expectNewClients(updated);
expect(clientsState.snapshot.version).toEqual(newVersion);
}); });
it('should update clients when client revoked', () => { it('should update clients when client revoked', () => {
const updated = createClients(1, 2, 3); const updated = createClients(1, 2, 3);
clientsService.setup(x => x.deleteClient(app, oldClients.items[0], version)) clientsService.setup(x => x.deleteClient(app, oldClients.items[0], version))
.returns(() => of(versioned(newVersion))).verifiable(); .returns(() => of(versioned(newVersion, updated))).verifiable();
clientsState.revoke(oldClients[0]).subscribe(); clientsState.revoke(oldClients.items[0]).subscribe();
expectNewClients(updated);
});
function expectNewClients(updated: ClientsPayload) {
expect(clientsState.snapshot.clients.values).toEqual(updated.items); expect(clientsState.snapshot.clients.values).toEqual(updated.items);
expect(clientsState.snapshot.version).toEqual(newVersion); expect(clientsState.snapshot.version).toEqual(newVersion);
});
}
}); });
}); });

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

@ -65,7 +65,7 @@ export class ClientsState extends State<Snapshot> {
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ clients: ImmutableArray.empty(), version: new Version(''), links: {} }); super({ clients: ImmutableArray.empty(), version: Version.EMPTY, links: {} });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {

19
src/Squidex/app/shared/state/contributors.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 {
ContributorsPayload,
ContributorsService, ContributorsService,
ContributorsState, ContributorsState,
DialogService, DialogService,
@ -81,7 +82,7 @@ describe('ContributorsState', () => {
}); });
it('should update contributors when user assigned', () => { it('should update contributors when user assigned', () => {
const updated = createContributors(1, 2, 3); const updated = createContributors(5, 6);
const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' };
@ -90,22 +91,24 @@ describe('ContributorsState', () => {
contributorsState.assign(request).subscribe(); contributorsState.assign(request).subscribe();
expect(contributorsState.snapshot.contributors.values).toEqual(oldContributors.items); expectNewContributors(updated);
expect(contributorsState.snapshot.maxContributors).toBe(oldContributors.maxContributors);
expect(contributorsState.snapshot.version).toEqual(newVersion);
}); });
it('should update contributors when contribution revoked', () => { it('should update contributors when contribution revoked', () => {
const updated = createContributors(1, 2, 3); const updated = createContributors(5, 6);
contributorsService.setup(x => x.deleteContributor(app, oldContributors.items[0], version)) contributorsService.setup(x => x.deleteContributor(app, oldContributors.items[0], version))
.returns(() => of(versioned(newVersion, updated))).verifiable(); .returns(() => of(versioned(newVersion, updated))).verifiable();
contributorsState.revoke(oldContributors.items[0]).subscribe(); contributorsState.revoke(oldContributors.items[0]).subscribe();
expect(contributorsState.snapshot.contributors.values).toEqual(oldContributors.items); expectNewContributors(updated);
expect(contributorsState.snapshot.maxContributors).toBe(oldContributors.maxContributors);
expect(contributorsState.snapshot.version).toEqual(newVersion);
}); });
function expectNewContributors(updated: ContributorsPayload) {
expect(contributorsState.snapshot.contributors.values).toEqual(updated.items);
expect(contributorsState.snapshot.maxContributors).toBe(updated.maxContributors);
expect(contributorsState.snapshot.version).toEqual(newVersion);
}
}); });
}); });

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

@ -79,7 +79,7 @@ export class ContributorsState extends State<Snapshot> {
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ contributors: ImmutableArray.empty(), version: new Version(''), maxContributors: -1, links: {} }); super({ contributors: ImmutableArray.empty(), version: Version.EMPTY, maxContributors: -1, links: {} });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {

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

@ -109,7 +109,7 @@ describe('LanguagesState', () => {
languagesState.add(languageIT).subscribe(); languagesState.add(languageIT).subscribe();
expectUpdated(updated); expectNewLanguages(updated);
}); });
it('should update languages when language updated', () => { it('should update languages when language updated', () => {
@ -122,7 +122,7 @@ describe('LanguagesState', () => {
languagesState.update(oldLanguages.items[1], request).subscribe(); languagesState.update(oldLanguages.items[1], request).subscribe();
expectUpdated(updated); expectNewLanguages(updated);
}); });
it('should update languages when language deleted', () => { it('should update languages when language deleted', () => {
@ -133,10 +133,10 @@ describe('LanguagesState', () => {
languagesState.remove(oldLanguages.items[1]).subscribe(); languagesState.remove(oldLanguages.items[1]).subscribe();
expectUpdated(updated); expectNewLanguages(updated);
}); });
function expectUpdated(updated: AppLanguagesPayload) { function expectNewLanguages(updated: AppLanguagesPayload) {
expect(languagesState.snapshot.languages.values).toEqual([ expect(languagesState.snapshot.languages.values).toEqual([
{ {
language: updated.items[0], language: updated.items[0],

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

@ -96,7 +96,7 @@ export class LanguagesState extends State<Snapshot> {
allLanguages: ImmutableArray.empty(), allLanguages: ImmutableArray.empty(),
allLanguagesNew: ImmutableArray.empty(), allLanguagesNew: ImmutableArray.empty(),
languages: ImmutableArray.empty(), languages: ImmutableArray.empty(),
version: new Version(''), version: Version.EMPTY,
links: {} links: {}
}); });
} }

57
src/Squidex/app/shared/state/patterns.state.spec.ts

@ -10,12 +10,14 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
DialogService, DialogService,
PatternDto, PatternsPayload,
PatternsService, PatternsService,
PatternsState, PatternsState,
versioned versioned
} from '@app/shared/internal'; } from '@app/shared/internal';
import { createPatterns } from '../services/patterns.service.spec';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
describe('PatternsState', () => { describe('PatternsState', () => {
@ -26,10 +28,7 @@ describe('PatternsState', () => {
version version
} = TestValues; } = TestValues;
const oldPatterns = [ const oldPatterns = createPatterns(1, 2, 3);
new PatternDto('id1', 'name1', 'pattern1', ''),
new PatternDto('id2', 'name2', 'pattern2', '')
];
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
let patternsService: IMock<PatternsService>; let patternsService: IMock<PatternsService>;
@ -49,11 +48,11 @@ describe('PatternsState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load patterns', () => { it('should load patterns', () => {
patternsService.setup(x => x.getPatterns(app)) patternsService.setup(x => x.getPatterns(app))
.returns(() => of({ payload: oldPatterns, version })).verifiable(); .returns(() => of(versioned(version, oldPatterns))).verifiable();
patternsState.load().subscribe(); patternsState.load().subscribe();
expect(patternsState.snapshot.patterns.values).toEqual(oldPatterns); expect(patternsState.snapshot.patterns.values).toEqual(oldPatterns.items);
expect(patternsState.snapshot.version).toEqual(version); expect(patternsState.snapshot.version).toEqual(version);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never()); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
@ -61,7 +60,7 @@ describe('PatternsState', () => {
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
patternsService.setup(x => x.getPatterns(app)) patternsService.setup(x => x.getPatterns(app))
.returns(() => of({ payload: oldPatterns, version })).verifiable(); .returns(() => of(versioned(version, oldPatterns))).verifiable();
patternsState.load(true).subscribe(); patternsState.load(true).subscribe();
@ -74,49 +73,51 @@ describe('PatternsState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
patternsService.setup(x => x.getPatterns(app)) patternsService.setup(x => x.getPatterns(app))
.returns(() => of({ payload: oldPatterns, version })).verifiable(); .returns(() => of(versioned(version, oldPatterns))).verifiable();
patternsState.load().subscribe(); patternsState.load().subscribe();
}); });
it('should add pattern to snapshot when created', () => { it('should add pattern to snapshot when created', () => {
const newPattern = new PatternDto('id3', 'name3', 'pattern3', ''); const updated = createPatterns(4, 5);
const request = { ...newPattern }; const request = { name: 'new', pattern: 'a-z' };
patternsService.setup(x => x.postPattern(app, request, version)) patternsService.setup(x => x.postPattern(app, request, version))
.returns(() => of(versioned(newVersion, newPattern))).verifiable(); .returns(() => of(versioned(newVersion, updated))).verifiable();
patternsState.create(request).subscribe(); patternsState.create(request).subscribe();
expect(patternsState.snapshot.patterns.values).toEqual([...oldPatterns, newPattern]); expectNewPatterns(updated);
expect(patternsState.snapshot.version).toEqual(newVersion);
}); });
it('should update properties when updated', () => { it('should update properties when updated', () => {
const request = { name: 'name2_1', pattern: 'pattern2_1', message: 'message2_1' }; const updated = createPatterns(4, 5);
patternsService.setup(x => x.putPattern(app, oldPatterns[1].id, request, version)) const request = { name: 'name2_1', pattern: 'pattern2_1', message: 'message2_1' };
.returns(() => of(versioned(newVersion))).verifiable();
patternsState.update(oldPatterns[1], request).subscribe(); patternsService.setup(x => x.putPattern(app, oldPatterns.items[1], request, version))
.returns(() => of(versioned(newVersion, updated))).verifiable();
const pattern_1 = patternsState.snapshot.patterns.at(1); patternsState.update(oldPatterns.items[1], request).subscribe();
expect(pattern_1.name).toBe(request.name); expectNewPatterns(updated);
expect(pattern_1.pattern).toBe(request.pattern);
expect(pattern_1.message).toBe(request.message);
expect(patternsState.snapshot.version).toEqual(newVersion);
}); });
it('should remove pattern from snapshot when deleted', () => { it('should remove pattern from snapshot when deleted', () => {
patternsService.setup(x => x.deletePattern(app, oldPatterns[0].id, version)) const updated = createPatterns(4, 5);
.returns(() => of(versioned(newVersion))).verifiable();
patternsState.delete(oldPatterns[0]).subscribe(); patternsService.setup(x => x.deletePattern(app, oldPatterns.items[0], version))
.returns(() => of(versioned(newVersion, updated))).verifiable();
expect(patternsState.snapshot.patterns.values).toEqual([oldPatterns[1]]); patternsState.delete(oldPatterns.items[0]).subscribe();
expect(patternsState.snapshot.version).toEqual(newVersion);
expectNewPatterns(updated);
}); });
function expectNewPatterns(updated: PatternsPayload) {
expect(patternsState.snapshot.patterns.values).toEqual(updated.items);
expect(patternsState.snapshot.version).toEqual(newVersion);
}
}); });
}); });

60
src/Squidex/app/shared/state/patterns.state.ts

@ -12,7 +12,7 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
mapVersioned, ResourceLinks,
shareMapSubscribed, shareMapSubscribed,
shareSubscribed, shareSubscribed,
State, State,
@ -24,6 +24,7 @@ import { AppsState } from './apps.state';
import { import {
EditPatternDto, EditPatternDto,
PatternDto, PatternDto,
PatternsPayload,
PatternsService PatternsService
} from './../services/patterns.service'; } from './../services/patterns.service';
@ -36,6 +37,9 @@ interface Snapshot {
// Indicates if the patterns are loaded. // Indicates if the patterns are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// The links.
links: ResourceLinks;
} }
type PatternsList = ImmutableArray<PatternDto>; type PatternsList = ImmutableArray<PatternDto>;
@ -55,7 +59,7 @@ export class PatternsState extends State<Snapshot> {
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ patterns: ImmutableArray.empty(), version: new Version('') }); super({ patterns: ImmutableArray.empty(), version: Version.EMPTY, links: {} });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
@ -69,52 +73,43 @@ export class PatternsState extends State<Snapshot> {
this.dialogs.notifyInfo('Patterns reloaded.'); this.dialogs.notifyInfo('Patterns reloaded.');
} }
this.next(s => { this.replacePatterns(payload, version);
const patterns = ImmutableArray.of(payload).sortByStringAsc(x => x.name);
return { ...s, patterns, isLoaded: true, version: version };
});
}), }),
shareMapSubscribed(this.dialogs, x => x.payload)); shareMapSubscribed(this.dialogs, x => x.payload));
} }
public create(request: EditPatternDto): Observable<PatternDto> { public create(request: EditPatternDto): Observable<any> {
return this.patternsService.postPattern(this.appName, request, this.version).pipe( return this.patternsService.postPattern(this.appName, request, this.version).pipe(
tap(({ version, payload }) => { tap(({ version, payload }) => {
this.next(s => { this.replacePatterns(payload, version);
const patterns = s.patterns.push(payload).sortByStringAsc(x => x.name);
return { ...s, patterns, version: version };
});
}), }),
shareMapSubscribed(this.dialogs, x => x.payload)); shareSubscribed(this.dialogs));
} }
public update(pattern: PatternDto, request: EditPatternDto): Observable<PatternDto> { public update(pattern: PatternDto, request: EditPatternDto): Observable<any> {
return this.patternsService.putPattern(this.appName, pattern.id, request, this.version).pipe( return this.patternsService.putPattern(this.appName, pattern, request, this.version).pipe(
mapVersioned(() => update(pattern, request)),
tap(({ version, payload }) => { tap(({ version, payload }) => {
this.next(s => { this.replacePatterns(payload, version);
const patterns = s.patterns.replaceBy('id', payload).sortByStringAsc(x => x.name);
return { ...s, patterns, version: version };
});
}), }),
shareMapSubscribed(this.dialogs, x => x.payload)); shareSubscribed(this.dialogs));
} }
public delete(pattern: PatternDto): Observable<any> { public delete(pattern: PatternDto): Observable<any> {
return this.patternsService.deletePattern(this.appName, pattern.id, this.version).pipe( return this.patternsService.deletePattern(this.appName, pattern, this.version).pipe(
tap(({ version }) => { tap(({ version, payload }) => {
this.next(s => { this.replacePatterns(payload, version);
const patterns = s.patterns.filter(c => c.id !== pattern.id);
return { ...s, patterns, version: version };
});
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
private replacePatterns(payload: PatternsPayload, version: Version) {
const patterns = ImmutableArray.of(payload.items);
this.next(s => {
return { ...s, patterns, isLoaded: true, version, links: payload._links };
});
}
private get appName() { private get appName() {
return this.appsState.appName; return this.appsState.appName;
} }
@ -122,7 +117,4 @@ export class PatternsState extends State<Snapshot> {
private get version() { private get version() {
return this.snapshot.version; return this.snapshot.version;
} }
} }
const update = (pattern: PatternDto, request: EditPatternDto) =>
pattern.with(request);

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

@ -79,7 +79,7 @@ export class PlansState extends State<Snapshot> {
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly plansService: PlansService private readonly plansService: PlansService
) { ) {
super({ plans: ImmutableArray.empty(), version: new Version('') }); super({ plans: ImmutableArray.empty(), version: Version.EMPTY });
} }
public load(isReload = false, overridePlanId?: string): Observable<any> { public load(isReload = false, overridePlanId?: string): Observable<any> {

59
src/Squidex/app/shared/state/roles.state.spec.ts

@ -10,12 +10,14 @@ import { IMock, It, Mock, Times } from 'typemoq';
import { import {
DialogService, DialogService,
RoleDto, RolesPayload,
RolesService, RolesService,
RolesState, RolesState,
versioned versioned
} from '@app/shared/internal'; } from '@app/shared/internal';
import { createRoles } from '../services/roles.service.spec';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
describe('RolesState', () => { describe('RolesState', () => {
@ -26,10 +28,7 @@ describe('RolesState', () => {
version version
} = TestValues; } = TestValues;
const oldRoles = [ const oldRoles = createRoles(1, 2);
new RoleDto('Role1', 3, 5, ['P1']),
new RoleDto('Role2', 7, 9, ['P2'])
];
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
let rolesService: IMock<RolesService>; let rolesService: IMock<RolesService>;
@ -45,11 +44,11 @@ describe('RolesState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load roles', () => { it('should load roles', () => {
rolesService.setup(x => x.getRoles(app)) rolesService.setup(x => x.getRoles(app))
.returns(() => of({ payload: oldRoles, version })).verifiable(); .returns(() => of(versioned(version, oldRoles))).verifiable();
rolesState.load().subscribe(); rolesState.load().subscribe();
expect(rolesState.snapshot.roles.values).toEqual(oldRoles); expect(rolesState.snapshot.roles.values).toEqual(oldRoles.items);
expect(rolesState.snapshot.isLoaded).toBeTruthy(); expect(rolesState.snapshot.isLoaded).toBeTruthy();
expect(rolesState.snapshot.version).toEqual(version); expect(rolesState.snapshot.version).toEqual(version);
@ -76,42 +75,46 @@ describe('RolesState', () => {
rolesState.load().subscribe(); rolesState.load().subscribe();
}); });
it('should add role to snapshot when added', () => { it('should update roles when role added', () => {
const newRole = new RoleDto('Role3', 0, 0, ['P3']); const updated = createRoles(4, 5);
const request = { name: newRole.name }; const request = { name: 'newRole' };
rolesService.setup(x => x.postRole(app, request, version)) rolesService.setup(x => x.postRole(app, request, version))
.returns(() => of(versioned(newVersion, newRole))); .returns(() => of(versioned(newVersion, updated)));
rolesState.add(request).subscribe(); rolesState.add(request).subscribe();
expect(rolesState.snapshot.roles.values).toEqual([oldRoles[0], oldRoles[1], newRole]); expectNewRoles(updated);
expect(rolesState.snapshot.version).toEqual(newVersion);
}); });
it('should update permissions when updated', () => { it('should update roles when role updated', () => {
const request = { permissions: ['P4', 'P5'] }; const updated = createRoles(4, 5);
rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version)) const request = { permissions: ['P4', 'P5'] };
.returns(() => of(versioned(newVersion)));
rolesState.update(oldRoles[1], request).subscribe(); rolesService.setup(x => x.putRole(app, oldRoles.items[1], request, version))
.returns(() => of(versioned(newVersion, updated)));
const role_1 = rolesState.snapshot.roles.at(1); rolesState.update(oldRoles.items[1], request).subscribe();
expect(role_1.permissions).toEqual(request.permissions); expectNewRoles(updated);
expect(rolesState.snapshot.version).toEqual(newVersion);
}); });
it('should remove role from snapshot when deleted', () => { it('should update roles when role deleted', () => {
rolesService.setup(x => x.deleteRole(app, oldRoles[0].name, version)) const updated = createRoles(4, 5);
.returns(() => of(versioned(newVersion)));
rolesState.delete(oldRoles[0]).subscribe(); rolesService.setup(x => x.deleteRole(app, oldRoles.items[1], version))
.returns(() => of(versioned(newVersion, updated)));
expect(rolesState.snapshot.roles.values).toEqual([oldRoles[1]]); rolesState.delete(oldRoles.items[1]).subscribe();
expect(rolesState.snapshot.version).toEqual(newVersion);
expectNewRoles(updated);
}); });
function expectNewRoles(updated: RolesPayload) {
expect(rolesState.snapshot.roles.values).toEqual(updated.items);
expect(rolesState.snapshot.version).toEqual(newVersion);
}
}); });
}); });

61
src/Squidex/app/shared/state/roles.state.ts

@ -12,8 +12,7 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { import {
DialogService, DialogService,
ImmutableArray, ImmutableArray,
mapVersioned, ResourceLinks,
shareMapSubscribed,
shareSubscribed, shareSubscribed,
State, State,
Version Version
@ -24,6 +23,7 @@ import { AppsState } from './apps.state';
import { import {
CreateRoleDto, CreateRoleDto,
RoleDto, RoleDto,
RolesPayload,
RolesService, RolesService,
UpdateRoleDto UpdateRoleDto
} from './../services/roles.service'; } from './../services/roles.service';
@ -37,6 +37,9 @@ interface Snapshot {
// Indicates if the roles are loaded. // Indicates if the roles are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// The links.
links: ResourceLinks;
} }
type RolesList = ImmutableArray<RoleDto>; type RolesList = ImmutableArray<RoleDto>;
@ -56,7 +59,7 @@ export class RolesState extends State<Snapshot> {
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ roles: ImmutableArray.empty(), version: new Version('') }); super({ roles: ImmutableArray.empty(), version: Version.EMPTY, links: {} });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
@ -70,50 +73,41 @@ export class RolesState extends State<Snapshot> {
this.dialogs.notifyInfo('Roles reloaded.'); this.dialogs.notifyInfo('Roles reloaded.');
} }
this.next(s => { this.replaceRoles(payload, version);
const roles = ImmutableArray.of(payload).sortByStringAsc(x => x.name);
return { ...s, roles, isLoaded: true, version };
});
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public add(request: CreateRoleDto): Observable<RoleDto> { public add(request: CreateRoleDto): Observable<any> {
return this.rolesService.postRole(this.appName, request, this.version).pipe( return this.rolesService.postRole(this.appName, request, this.version).pipe(
tap(({ version, payload }) => { tap(({ version, payload }) => {
this.next(s => { this.replaceRoles(payload, version);
const roles = s.roles.push(payload).sortByStringAsc(x => x.name); }),
shareSubscribed(this.dialogs));
}
return { ...s, roles, version }; public update(role: RoleDto, request: UpdateRoleDto): Observable<any> {
}); return this.rolesService.putRole(this.appName, role, request, this.version).pipe(
tap(({ version, payload }) => {
this.replaceRoles(payload, version);
}), }),
shareMapSubscribed(this.dialogs, x => x.payload)); shareSubscribed(this.dialogs));
} }
public delete(role: RoleDto): Observable<any> { public delete(role: RoleDto): Observable<any> {
return this.rolesService.deleteRole(this.appName, role.name, this.version).pipe( return this.rolesService.deleteRole(this.appName, role, this.version).pipe(
tap(({ version }) => { tap(({ version, payload }) => {
this.next(s => { this.replaceRoles(payload, version);
const roles = s.roles.removeBy('name', role);
return { ...s, roles, version };
});
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));
} }
public update(role: RoleDto, request: UpdateRoleDto): Observable<RoleDto> { private replaceRoles(payload: RolesPayload, version: Version) {
return this.rolesService.putRole(this.appName, role.name, request, this.version).pipe( const roles = ImmutableArray.of(payload.items);
mapVersioned(() => update(role, request)),
tap(({ version, payload }) => {
this.next(s => {
const roles = s.roles.replaceBy('name', payload);
return { ...s, roles, version }; this.next(s => {
}); return { ...s, roles, isLoaded: true, version, links: payload._links };
}), });
shareMapSubscribed(this.dialogs, x => x.payload));
} }
private get appName() { private get appName() {
@ -123,7 +117,4 @@ export class RolesState extends State<Snapshot> {
private get version() { private get version() {
return this.snapshot.version; return this.snapshot.version;
} }
} }
const update = (role: RoleDto, request: UpdateRoleDto) =>
role.with({ permissions: request.permissions });
Loading…
Cancel
Save