Browse Source

Test

pull/363/head
Sebastian 7 years ago
parent
commit
6c6494c136
  1. 8
      src/Squidex.Web/Extensions.cs
  2. 77
      src/Squidex.Web/PermissionExtensions.cs
  3. 52
      src/Squidex.Web/Resource.cs
  4. 23
      src/Squidex.Web/ResourceLink.cs
  5. 46
      src/Squidex.Web/UrlHelperExtensions.cs
  6. 53
      src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  7. 34
      src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs
  8. 22
      src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  9. 4
      src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  10. 2
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  11. 6
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  12. 22
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  13. 19
      src/Squidex/app/features/administration/services/users.service.ts
  14. 49
      src/Squidex/app/features/administration/state/users.state.spec.ts
  15. 55
      src/Squidex/app/features/administration/state/users.state.ts
  16. 23
      src/Squidex/app/framework/angular/http/hateos.pipes.ts
  17. 1
      src/Squidex/app/framework/declarations.ts
  18. 1
      src/Squidex/app/framework/internal.ts
  19. 6
      src/Squidex/app/framework/module.ts
  20. 25
      src/Squidex/app/framework/utils/hateos.ts

8
src/Squidex.Web/Extensions.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Infrastructure.Security;
@ -40,5 +41,12 @@ namespace Squidex.Web
return (null, null);
}
public static bool IsUser(this ApiController controller, string userId)
{
var subject = controller.User.OpenIdSubject();
return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase);
}
}
}

77
src/Squidex.Web/PermissionExtensions.cs

@ -0,0 +1,77 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
namespace Squidex.Web
{
public static class PermissionExtensions
{
private sealed class PermissionFeature
{
public PermissionSet Permissions { get; }
public PermissionFeature(PermissionSet permissions)
{
Permissions = permissions;
}
}
public static PermissionSet GetPermissions(this HttpContext httpContext)
{
var feature = httpContext.Features.Get<PermissionFeature>();
if (feature == null)
{
feature = new PermissionFeature(httpContext.User.Permissions());
httpContext.Features.Set(feature);
}
return feature.Permissions;
}
public static bool HasPermission(this HttpContext httpContext, Permission permission)
{
return httpContext.GetPermissions().Includes(permission);
}
public static bool HasPermission(this HttpContext httpContext, string id, string app = "*", string schema = "*")
{
return httpContext.GetPermissions().Includes(Permissions.ForApp(id, app, schema));
}
public static bool HasPermission(this ApiController controller, Permission permission)
{
return controller.HttpContext.GetPermissions().Includes(permission);
}
public static bool HasPermission(this ApiController controller, string id, string app = "*", string schema = "*")
{
if (app == "*")
{
if (controller.RouteData.Values.TryGetValue("app", out var value) && value is string s)
{
app = s;
}
}
if (schema == "*")
{
if (controller.RouteData.Values.TryGetValue("name", out var value) && value is string s)
{
schema = s;
}
}
return controller.HttpContext.GetPermissions().Includes(Permissions.ForApp(id, app, schema));
}
}
}

52
src/Squidex.Web/Resource.cs

@ -0,0 +1,52 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net.Http;
namespace Squidex.Web
{
public abstract class Resource
{
[JsonProperty("_links")]
[Required]
[Display(Description = "The links.")]
public Dictionary<string, ResourceLink> Links { get; } = new Dictionary<string, ResourceLink>();
public void AddSelfLink(string href)
{
AddGetLink("self", href);
}
public void AddGetLink(string rel, string href)
{
AddLink(rel, HttpMethod.Get, href);
}
public void AddPostLink(string rel, string href)
{
AddLink(rel, HttpMethod.Post, href);
}
public void AddPutLink(string rel, string href)
{
AddLink(rel, HttpMethod.Put, href);
}
public void AddDeleteLink(string rel, string href)
{
AddLink(rel, HttpMethod.Delete, href);
}
public void AddLink(string rel, HttpMethod method, string href)
{
Links[rel] = new ResourceLink { Href = href, Method = method };
}
}
}

23
src/Squidex.Web/ResourceLink.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Net.Http;
namespace Squidex.Web
{
public class ResourceLink
{
[Required]
[Display(Description = "The link url.")]
public string Href { get; set; }
[Required]
[Display(Description = "The link method.")]
public HttpMethod Method { get; set; }
}
}

46
src/Squidex.Web/UrlHelperExtensions.cs

@ -0,0 +1,46 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using System;
using System.Linq.Expressions;
using System.Reflection;
namespace Squidex.Web
{
public static class UrlHelperExtensions
{
private static class NameOf<T>
{
public static readonly string Controller;
static NameOf()
{
const string suffix = "Controller";
var name = typeof(T).Name;
if (name.EndsWith(suffix))
{
name = name.Substring(0, name.Length - suffix.Length);
}
Controller = name;
}
}
public static string Url<T>(this IUrlHelper urlHelper, Func<T, string> action, object values = null) where T : Controller
{
return urlHelper.Action(action(null), NameOf<T>.Controller, values);
}
public static string Url<T>(this Controller controller, Func<T, string> action, object values = null) where T : Controller
{
return controller.Url.Url<T>(action, values);
}
}
}

53
src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -8,12 +8,18 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Users;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class UserDto
public sealed class UserDto : Resource
{
private static readonly Permission LockPermission = new Permission(Shared.Permissions.AdminUsersLock);
private static readonly Permission UnlockPermission = new Permission(Shared.Permissions.AdminUsersUnlock);
private static readonly Permission UpdatePermission = new Permission(Shared.Permissions.AdminUsersUpdate);
/// <summary>
/// The id of the user.
/// </summary>
@ -44,11 +50,50 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
[Required]
public IEnumerable<string> Permissions { get; set; }
public static UserDto FromUser(IUser user)
public static UserDto FromUser(IUser user, ApiController controller)
{
var userPermssions = user.Permissions().ToIds();
var userName = user.DisplayName();
var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions });
return CreateLinks(result, controller);
}
private static UserDto CreateLinks(UserDto result, ApiController controller)
{
var permissions = user.Permissions().ToIds();
var values = new { id = result.Id };
if (controller is UserManagementController)
{
result.AddSelfLink(controller.Url<UserManagementController>(c => nameof(c.GetUser), values));
}
else
{
result.AddSelfLink(controller.Url<UsersController>(c => nameof(c.GetUser), values));
}
if (!controller.IsUser(result.Id))
{
if (controller.HasPermission(LockPermission) && !result.IsLocked)
{
result.AddPutLink("lock", controller.Url<UserManagementController>(c => nameof(c.LockUser), values));
}
if (controller.HasPermission(UnlockPermission) && result.IsLocked)
{
result.AddPutLink("unlock", controller.Url<UserManagementController>(c => nameof(c.UnlockUser), values));
}
}
if (controller.HasPermission(UpdatePermission))
{
result.AddPutLink("update", controller.Url<UserManagementController>(c => nameof(c.PutUser), values));
}
result.AddGetLink("picture", controller.Url<UsersController>(c => nameof(c.GetUserPicture), values));
return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName(), Permissions = permissions });
return result;
}
}
}

34
src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs

@ -5,10 +5,19 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Users.Models
{
public sealed class UsersDto
public sealed class UsersDto : Resource
{
private static readonly Permission CreatePermissions = new Permission(Permissions.AdminUsersCreate);
/// <summary>
/// The total number of users.
/// </summary>
@ -18,5 +27,28 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// The users.
/// </summary>
public UserDto[] Items { get; set; }
public static UsersDto FromResults(IEnumerable<UserWithClaims> items, long total, ApiController controller)
{
var result = new UsersDto
{
Total = total,
Items = items.Select(x => UserDto.FromUser(x, controller)).ToArray()
};
return CreateLinks(result, controller);
}
private static UsersDto CreateLinks(UsersDto result, ApiController controller)
{
result.AddSelfLink(controller.Url<UserManagementController>(c => nameof(c.GetUsers)));
if (controller.HasPermission(CreatePermissions))
{
result.AddPostLink("create", controller.Url<UserManagementController>(c => nameof(c.PostUser)));
}
return result;
}
}
}

22
src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -14,7 +12,6 @@ using Squidex.Areas.Api.Controllers.Users.Models;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Web;
@ -43,11 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Users
await Task.WhenAll(taskForItems, taskForCount);
var response = new UsersDto
{
Total = taskForCount.Result,
Items = taskForItems.Result.Select(UserDto.FromUser).ToArray()
};
var response = UsersDto.FromResults(taskForItems.Result, taskForCount.Result, this);
return Ok(response);
}
@ -64,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Users
return NotFound();
}
var response = UserDto.FromUser(entity);
var response = UserDto.FromUser(entity, this);
return Ok(response);
}
@ -96,7 +89,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersLock)]
public async Task<IActionResult> LockUser(string id)
{
if (IsSelf(id))
if (this.IsUser(id))
{
throw new ValidationException("Locking user failed.", new ValidationError("You cannot lock yourself."));
}
@ -111,7 +104,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersUnlock)]
public async Task<IActionResult> UnlockUser(string id)
{
if (IsSelf(id))
if (this.IsUser(id))
{
throw new ValidationException("Unlocking user failed.", new ValidationError("You cannot unlock yourself."));
}
@ -120,12 +113,5 @@ namespace Squidex.Areas.Api.Controllers.Users
return NoContent();
}
private bool IsSelf(string id)
{
var subject = User.OpenIdSubject();
return string.Equals(subject, id, StringComparison.OrdinalIgnoreCase);
}
}
}

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

@ -76,7 +76,7 @@ namespace Squidex.Areas.Api.Controllers.Users
{
var entities = await userResolver.QueryByEmailAsync(query);
var models = entities.Where(x => !x.IsHidden()).Select(UserDto.FromUser).ToArray();
var models = entities.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, this)).ToArray();
return Ok(models);
}
@ -110,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Users
if (entity != null)
{
var response = UserDto.FromUser(entity);
var response = UserDto.FromUser(entity, this);
return Ok(response);
}

2
src/Squidex/app/features/administration/pages/users/user-page.component.html

@ -50,7 +50,7 @@
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" spellcheck="false" />
</div>
<div class="form-group form-group-password" [class.hidden]="user?.isCurrentUser">
<div class="form-group form-group-password">
<div class="form-group">
<label for="password">Password</label>

6
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -26,7 +26,7 @@ import {
export class UserPageComponent extends ResourceOwner implements OnInit {
public canUpdate = false;
public user?: { user: UserDto, isCurrentUser: boolean };
public user?: UserDto;
public userForm = new UserForm(this.formBuilder);
constructor(
@ -45,7 +45,7 @@ export class UserPageComponent extends ResourceOwner implements OnInit {
this.user = selectedUser!;
if (selectedUser) {
this.userForm.load(selectedUser.user);
this.userForm.load(selectedUser);
}
}));
}
@ -55,7 +55,7 @@ export class UserPageComponent extends ResourceOwner implements OnInit {
if (value) {
if (this.user) {
this.usersState.update(this.user.user, value)
this.usersState.update(this.user, value)
.subscribe(() => {
this.userForm.submitCompleted();
}, error => {

22
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -48,32 +48,24 @@
<div class="grid-content">
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="usersState.users | async; let users">
<tbody *ngFor="let userInfo of users; trackBy: trackByUser">
<tr [routerLink]="userInfo.user.id" routerLinkActive="active">
<tbody *ngFor="let user of users; trackBy: trackByUser">
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" title="{{userInfo.user.name}}" [attr.src]="userInfo.user | sqxUserDtoPicture" />
<img class="user-picture" title="{{user.name}}" [attr.src]="user | sqxUserDtoPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{userInfo.user.displayName}}</span>
<span class="user-name table-cell">{{user.displayName}}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{userInfo.user.email}}</span>
<span class="user-email table-cell">{{user.email}}</span>
</td>
<td class="cell-actions">
<ng-container *ngIf="!userInfo.isCurrentUser; else self">
<button type="button" class="btn btn-text" (click)="lock(userInfo.user)" sqxStopClick *ngIf="!userInfo.user.isLocked" title="Lock User">
<button type="button" class="btn btn-text" (click)="lock(user)" sqxStopClick *ngIf="user | sqxHasLink:'lock'" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button type="button" class="btn btn-text" (click)="unlock(userInfo.user)" sqxStopClick *ngIf="userInfo.user.isLocked" title="Unlock User">
<button type="button" class="btn btn-text" (click)="unlock(user)" sqxStopClick *ngIf="user | sqxHasLink:'unlock'" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</ng-container>
<ng-template #self>
<button class="btn btn-text invisible">
&nbsp;
</button>
</ng-template>
</td>
</tr>
<tr class="spacer"></tr>

19
src/Squidex/app/features/administration/services/users.service.ts

@ -14,12 +14,19 @@ import {
ApiUrlConfig,
Model,
pretifyError,
ResultSet
Resource,
ResourceLinks,
ResultSet,
withLinks
} from '@app/shared';
export class UsersDto extends ResultSet<UserDto> {}
export class UsersDto extends ResultSet<UserDto> {
public _links: ResourceLinks;
}
export class UserDto extends Model<UserDto> {
public _links: ResourceLinks;
constructor(
public readonly id: string,
public readonly email: string,
@ -60,17 +67,19 @@ export class UsersService {
public getUsers(take: number, skip: number, query?: string): Observable<UsersDto> {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return this.http.get<{ total: number, items: any[] }>(url).pipe(
return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe(
map(body => {
const users = body.items.map(item =>
withLinks(
new UserDto(
item.id,
item.email,
item.displayName,
item.permissions,
item.isLocked));
item.isLocked),
item));
return new UsersDto(body.total, users);
return withLinks(new UsersDto(body.total, users), body);
}),
pretifyError('Failed to load users. Please reload.'));
}

49
src/Squidex/app/features/administration/state/users.state.spec.ts

@ -16,7 +16,7 @@ import {
UsersService
} from '@app/features/administration/internal';
import { SnapshotUser, UsersState } from './users.state';
import { UsersState } from './users.state';
describe('UsersState', () => {
const oldUsers = [
@ -26,21 +26,15 @@ describe('UsersState', () => {
const newUser = new UserDto('id3', 'mail3@mail.de', 'name3', ['Permission3'], false);
let authService: IMock<AuthService>;
let dialogs: IMock<DialogService>;
let usersService: IMock<UsersService>;
let usersState: UsersState;
beforeEach(() => {
authService = Mock.ofType<AuthService>();
authService.setup(x => x.user)
.returns(() => <any>{ id: 'id2' });
dialogs = Mock.ofType<DialogService>();
usersService = Mock.ofType<UsersService>();
usersState = new UsersState(authService.object, dialogs.object, usersService.object);
usersState = new UsersState(dialogs.object, usersService.object);
});
afterEach(() => {
@ -54,10 +48,7 @@ describe('UsersState', () => {
usersState.load().subscribe();
expect(usersState.snapshot.users.values).toEqual([
{ isCurrentUser: false, user: oldUsers[0] },
{ isCurrentUser: true, user: oldUsers[1] }
]);
expect(usersState.snapshot.users.values).toEqual(oldUsers);
expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200);
expect(usersState.snapshot.isLoaded).toBeTruthy();
@ -91,7 +82,7 @@ describe('UsersState', () => {
usersState.select('id1').subscribe();
usersState.load().subscribe();
expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUsers[0] });
expect(usersState.snapshot.selectedUser).toEqual(newUsers[0]);
});
it('should load next page and prev page when paging', () => {
@ -127,32 +118,32 @@ describe('UsersState', () => {
});
it('should return user on select and not load when already loaded', () => {
let selectedUser: SnapshotUser;
let selectedUser: UserDto;
usersState.select('id1').subscribe(x => {
selectedUser = x!;
});
expect(selectedUser!.user).toEqual(oldUsers[0]);
expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: oldUsers[0] });
expect(selectedUser!).toEqual(oldUsers[0]);
expect(usersState.snapshot.selectedUser).toEqual(oldUsers[0]);
});
it('should return user on select and load when not loaded', () => {
usersService.setup(x => x.getUser('id3'))
.returns(() => of(newUser));
let selectedUser: SnapshotUser;
let selectedUser: UserDto;
usersState.select('id3').subscribe(x => {
selectedUser = x!;
});
expect(selectedUser!.user).toEqual(newUser);
expect(usersState.snapshot.selectedUser).toEqual({ isCurrentUser: false, user: newUser });
expect(selectedUser!).toEqual(newUser);
expect(usersState.snapshot.selectedUser).toEqual(newUser);
});
it('should return null on select when unselecting user', () => {
let selectedUser: SnapshotUser;
let selectedUser: UserDto;
usersState.select(null).subscribe(x => {
selectedUser = x!;
@ -166,7 +157,7 @@ describe('UsersState', () => {
usersService.setup(x => x.getUser('unknown'))
.returns(() => throwError({})).verifiable();
let selectedUser: SnapshotUser;
let selectedUser: UserDto;
usersState.select('unknown').subscribe(x => {
selectedUser = x!;
@ -185,7 +176,7 @@ describe('UsersState', () => {
const user_1 = usersState.snapshot.users.at(0);
expect(user_1.user.isLocked).toBeTruthy();
expect(user_1.isLocked).toBeTruthy();
expect(user_1).toBe(usersState.snapshot.selectedUser!);
});
@ -198,7 +189,7 @@ describe('UsersState', () => {
const user_1 = usersState.snapshot.users.at(1);
expect(user_1.user.isLocked).toBeFalsy();
expect(user_1.isLocked).toBeFalsy();
expect(user_1).toBe(usersState.snapshot.selectedUser!);
});
@ -213,9 +204,9 @@ describe('UsersState', () => {
const user_1 = usersState.snapshot.users.at(0);
expect(user_1.user.email).toEqual(request.email);
expect(user_1.user.displayName).toEqual(request.displayName);
expect(user_1.user.permissions).toEqual(request.permissions);
expect(user_1.email).toEqual(request.email);
expect(user_1.displayName).toEqual(request.displayName);
expect(user_1.permissions).toEqual(request.permissions);
expect(user_1).toBe(usersState.snapshot.selectedUser!);
});
@ -227,11 +218,7 @@ describe('UsersState', () => {
usersState.create(request).subscribe();
expect(usersState.snapshot.users.values).toEqual([
{ isCurrentUser: false, user: newUser },
{ isCurrentUser: false, user: oldUsers[0] },
{ isCurrentUser: true, user: oldUsers[1] }
]);
expect(usersState.snapshot.users.values).toEqual([newUser, ...oldUsers]);
expect(usersState.snapshot.usersPager.numberOfItems).toBe(201);
});
});

55
src/Squidex/app/features/administration/state/users.state.ts

@ -7,12 +7,11 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
import '@app/framework/utils/rxjs-extensions';
import {
AuthService,
DialogService,
ImmutableArray,
Pager,
@ -27,14 +26,6 @@ import {
UsersService
} from './../services/users.service';
export interface SnapshotUser {
// The user.
user: UserDto;
// Indicates if the user is the current user.
isCurrentUser: boolean;
}
interface Snapshot {
// The current users.
users: UsersList;
@ -49,10 +40,10 @@ interface Snapshot {
isLoaded?: boolean;
// The selected user.
selectedUser?: SnapshotUser | null;
selectedUser?: UserDto | null;
}
export type UsersList = ImmutableArray<SnapshotUser>;
export type UsersList = ImmutableArray<UserDto>;
export type UsersResult = { total: number, users: UsersList };
@Injectable()
@ -74,14 +65,13 @@ export class UsersState extends State<Snapshot> {
distinctUntilChanged());
constructor(
private readonly authState: AuthService,
private readonly dialogs: DialogService,
private readonly usersService: UsersService
) {
super({ users: ImmutableArray.empty(), usersPager: new Pager(0) });
}
public select(id: string | null): Observable<SnapshotUser | null> {
public select(id: string | null): Observable<UserDto | null> {
return this.loadUser(id).pipe(
tap(selectedUser => {
this.next(s => ({ ...s, selectedUser }));
@ -94,13 +84,13 @@ export class UsersState extends State<Snapshot> {
return of(null);
}
const found = this.snapshot.users.find(x => x.user.id === id);
const found = this.snapshot.users.find(x => x.id === id);
if (found) {
return of(found);
}
return this.usersService.getUser(id).pipe(map(x => this.createUser(x)), catchError(() => of(null)));
return this.usersService.getUser(id).pipe(catchError(() => of(null)));
}
public load(isReload = false): Observable<any> {
@ -125,12 +115,12 @@ export class UsersState extends State<Snapshot> {
this.next(s => {
const usersPager = s.usersPager.setCount(total);
const users = ImmutableArray.of(items.map(x => this.createUser(x)));
const users = ImmutableArray.of(items);
let selectedUser = s.selectedUser;
if (selectedUser) {
selectedUser = users.find(x => x.user.id === selectedUser!.user.id) || selectedUser;
selectedUser = users.find(x => x.id === selectedUser!.id) || selectedUser;
}
return { ...s, users, usersPager, selectedUser, isLoaded: true };
@ -143,7 +133,7 @@ export class UsersState extends State<Snapshot> {
return this.usersService.postUser(request).pipe(
tap(created => {
this.next(s => {
const users = s.users.pushFront(this.createUser(created));
const users = s.users.pushFront(created);
const usersPager = s.usersPager.incrementCount();
return { ...s, users, usersPager };
@ -154,7 +144,7 @@ export class UsersState extends State<Snapshot> {
public update(user: UserDto, request: UpdateUserDto): Observable<UserDto> {
return this.usersService.putUser(user.id, request).pipe(
map(() => update(user, request)),
switchMap(() => this.usersService.getUser(user.id)),
tap(updated => {
this.replaceUser(updated);
}),
@ -163,7 +153,7 @@ export class UsersState extends State<Snapshot> {
public lock(user: UserDto): Observable<UserDto> {
return this.usersService.lockUser(user.id).pipe(
map(() => setLocked(user, true)),
switchMap(() => this.usersService.getUser(user.id)),
tap(updated => {
this.replaceUser(updated);
}),
@ -172,7 +162,7 @@ export class UsersState extends State<Snapshot> {
public unlock(user: UserDto): Observable<UserDto> {
return this.usersService.unlockUser(user.id).pipe(
map(() => setLocked(user, false)),
switchMap(() => this.usersService.getUser(user.id)),
tap(updated => {
this.replaceUser(updated);
}),
@ -199,30 +189,15 @@ export class UsersState extends State<Snapshot> {
private replaceUser(user: UserDto) {
return this.next(s => {
const users = s.users.map(u => u.user.id === user.id ? this.createUser(user) : u);
const users = s.users.map(u => u.id === user.id ? user : u);
const selectedUser =
s.selectedUser &&
s.selectedUser.user.id !== user.id ?
s.selectedUser.id !== user.id ?
s.selectedUser :
users.find(x => x.user.id === user.id);
users.find(x => x.id === user.id);
return { ...s, users, selectedUser };
});
}
private get userId() {
return this.authState.user!.id;
}
private createUser(user: UserDto): SnapshotUser {
return { user, isCurrentUser: user.id === this.userId };
}
}
const update = (user: UserDto, request: UpdateUserDto) =>
user.with(request);
const setLocked = (user: UserDto, isLocked: boolean) =>
user.with({ isLocked });

23
src/Squidex/app/framework/angular/http/hateos.pipes.ts

@ -0,0 +1,23 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Resource } from '@app/framework/internal';
@Pipe({
name: 'sqxHasLink',
pure: true
})
export class HasLinkPipe implements PipeTransform {
public transform(value: Resource, rel: string) {
return value._links && !!value._links[rel];
}
}
@Pipe({
name: 'sqxHasNoLink',
pure: true
})
export class HasNoLinkPipe implements PipeTransform {
public transform(value: Resource, rel: string) {
return !value._links || !value._links[rel];
}
}

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

@ -31,6 +31,7 @@ export * from './angular/forms/validators';
export * from './angular/http/caching.interceptor';
export * from './angular/http/loading.interceptor';
export * from './angular/http/hateos.pipes';
export * from './angular/http/http-extensions';
export * from './angular/modals/dialog-renderer.component';

1
src/Squidex/app/framework/internal.ts

@ -23,6 +23,7 @@ export * from './utils/date-helper';
export * from './utils/date-time';
export * from './utils/duration';
export * from './utils/error';
export * from './utils/hateos';
export * from './utils/interpolator';
export * from './utils/immutable-array';
export * from './utils/math-helper';

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

@ -42,6 +42,8 @@ import {
FormHintComponent,
FromNowPipe,
FullDateTimePipe,
HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective,
IFrameEditorComponent,
IgnoreScrollbarDirective,
@ -122,6 +124,8 @@ import {
FormHintComponent,
FromNowPipe,
FullDateTimePipe,
HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective,
IFrameEditorComponent,
IgnoreScrollbarDirective,
@ -188,6 +192,8 @@ import {
FormsModule,
FromNowPipe,
FullDateTimePipe,
HasLinkPipe,
HasNoLinkPipe,
HoverBackgroundDirective,
IFrameEditorComponent,
IgnoreScrollbarDirective,

25
src/Squidex/app/framework/utils/hateos.ts

@ -0,0 +1,25 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export interface Resource {
_links?: { [rel: string]: ResourceLink };
}
export type ResourceLinks = { [rel: string]: ResourceLink };
export type ResourceLink = { href: string; method: ResourceMethod; };
export function withLinks<T extends Resource>(value: T, source: Resource) {
value._links = source._links;
return value;
}
export type ResourceMethod =
'get' |
'post' |
'put' |
'delete';
Loading…
Cancel
Save