Browse Source

Started to remove link checks from components.

pull/363/head
Sebastian Stehle 7 years ago
parent
commit
3fa711c451
  1. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  2. 8
      src/Squidex/app/features/administration/administration-area.component.html
  3. 6
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  4. 4
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  5. 6
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  6. 14
      src/Squidex/app/features/administration/services/event-consumers.service.spec.ts
  7. 43
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  8. 14
      src/Squidex/app/features/administration/services/users.service.spec.ts
  9. 42
      src/Squidex/app/features/administration/services/users.service.ts
  10. 1
      src/Squidex/app/features/administration/state/users.state.spec.ts
  11. 19
      src/Squidex/app/features/administration/state/users.state.ts
  12. 21
      src/Squidex/app/features/content/pages/content/content-page.component.html
  13. 3
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  14. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  15. 3
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  16. 15
      src/Squidex/app/features/content/shared/content-item.component.html
  17. 3
      src/Squidex/app/features/content/shared/content-item.component.ts
  18. 2
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  19. 3
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  20. 10
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  21. 12
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  22. 3
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  23. 3
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  24. 17
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  25. 3
      src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts
  26. 3
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts
  27. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  28. 4
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  29. 6
      src/Squidex/app/features/settings/pages/clients/client.component.html
  30. 8
      src/Squidex/app/features/settings/pages/clients/client.component.ts
  31. 2
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  32. 6
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  33. 2
      src/Squidex/app/features/settings/pages/languages/language.component.html
  34. 3
      src/Squidex/app/features/settings/pages/languages/language.component.ts
  35. 2
      src/Squidex/app/features/settings/pages/languages/languages-page.component.html
  36. 5
      src/Squidex/app/features/settings/pages/patterns/pattern.component.ts
  37. 2
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html
  38. 2
      src/Squidex/app/features/settings/pages/roles/role.component.html
  39. 3
      src/Squidex/app/features/settings/pages/roles/role.component.ts
  40. 2
      src/Squidex/app/features/settings/pages/roles/roles-page.component.html
  41. 16
      src/Squidex/app/features/settings/settings-area.component.html
  42. 34
      src/Squidex/app/framework/angular/http/hateos.pipes.ts
  43. 1
      src/Squidex/app/framework/declarations.ts
  44. 6
      src/Squidex/app/framework/module.ts
  45. 14
      src/Squidex/app/framework/state.ts
  46. 32
      src/Squidex/app/framework/utils/hateos.ts
  47. 3
      src/Squidex/app/shared/components/asset-dialog.component.ts
  48. 4
      src/Squidex/app/shared/components/asset.component.html
  49. 3
      src/Squidex/app/shared/components/asset.component.ts
  50. 6
      src/Squidex/app/shared/components/schema-category.component.html
  51. 3
      src/Squidex/app/shared/components/schema-category.component.ts
  52. 26
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  53. 42
      src/Squidex/app/shared/services/app-languages.service.ts
  54. 19
      src/Squidex/app/shared/services/apps.service.spec.ts
  55. 42
      src/Squidex/app/shared/services/apps.service.ts
  56. 19
      src/Squidex/app/shared/services/assets.service.spec.ts
  57. 74
      src/Squidex/app/shared/services/assets.service.ts
  58. 17
      src/Squidex/app/shared/services/backups.service.spec.ts
  59. 48
      src/Squidex/app/shared/services/backups.service.ts
  60. 29
      src/Squidex/app/shared/services/clients.service.spec.ts
  61. 36
      src/Squidex/app/shared/services/clients.service.ts
  62. 25
      src/Squidex/app/shared/services/contents.service.spec.ts
  63. 79
      src/Squidex/app/shared/services/contents.service.ts
  64. 25
      src/Squidex/app/shared/services/contributors.service.spec.ts
  65. 33
      src/Squidex/app/shared/services/contributors.service.ts
  66. 25
      src/Squidex/app/shared/services/patterns.service.spec.ts
  67. 36
      src/Squidex/app/shared/services/patterns.service.ts
  68. 25
      src/Squidex/app/shared/services/roles.service.spec.ts
  69. 38
      src/Squidex/app/shared/services/roles.service.ts
  70. 21
      src/Squidex/app/shared/services/rules.service.spec.ts
  71. 62
      src/Squidex/app/shared/services/rules.service.ts
  72. 91
      src/Squidex/app/shared/services/schemas.service.spec.ts
  73. 192
      src/Squidex/app/shared/services/schemas.service.ts
  74. 6
      src/Squidex/app/shared/services/users.service.spec.ts
  75. 15
      src/Squidex/app/shared/services/users.service.ts
  76. 10
      src/Squidex/app/shared/state/backups.state.spec.ts
  77. 17
      src/Squidex/app/shared/state/backups.state.ts
  78. 15
      src/Squidex/app/shared/state/clients.state.ts
  79. 4
      src/Squidex/app/shared/state/contents.forms.spec.ts
  80. 27
      src/Squidex/app/shared/state/contents.state.ts
  81. 19
      src/Squidex/app/shared/state/contributors.state.ts
  82. 17
      src/Squidex/app/shared/state/languages.state.ts
  83. 11
      src/Squidex/app/shared/state/patterns.state.ts
  84. 15
      src/Squidex/app/shared/state/roles.state.ts
  85. 6
      src/Squidex/app/shared/state/rules.state.spec.ts
  86. 2
      src/Squidex/app/shared/state/rules.state.ts
  87. 1
      src/Squidex/app/shared/state/schemas.state.spec.ts
  88. 17
      src/Squidex/app/shared/state/schemas.state.ts
  89. 13
      src/Squidex/app/shared/state/ui.state.spec.ts
  90. 38
      src/Squidex/app/shared/state/ui.state.ts
  91. 6
      src/Squidex/app/shell/pages/app/left-menu.component.html
  92. 8
      src/Squidex/app/shell/pages/internal/profile-menu.component.html

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

@ -62,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
AddSelfLink(controller.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); AddSelfLink(controller.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
if (controller.HasPermission(Permissions.AppContributorsAssign, app)) if (controller.HasPermission(Permissions.AppContributorsAssign, app) && Items.Length < MaxContributors)
{ {
AddPostLink("create", controller.Url<AppContributorsController>(x => nameof(x.PostContributor), values)); AddPostLink("create", controller.Url<AppContributorsController>(x => nameof(x.PostContributor), values));
} }

8
src/Squidex/app/features/administration/administration-area.component.html

@ -1,18 +1,18 @@
<sqx-title message="Administration"></sqx-title> <sqx-title message="Administration"></sqx-title>
<div class="sidebar" *ngIf="uiState.resources | async; let resources"> <div class="sidebar">
<ul class="nav nav-panel flex-column"> <ul class="nav nav-panel flex-column">
<li class="nav-item" *ngIf="resources | sqxHasLink:'admin/events'"> <li class="nav-item" *ngIf="uiState.canReadEvents | async">
<a class="nav-link" routerLink="event-consumers" routerLinkActive="active"> <a class="nav-link" routerLink="event-consumers" routerLinkActive="active">
<i class="nav-icon icon-time"></i> <div class="nav-text">Consumers</div> <i class="nav-icon icon-time"></i> <div class="nav-text">Consumers</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="resources | sqxHasLink:'admin/users'"> <li class="nav-item" *ngIf="uiState.canReadUsers | async">
<a class="nav-link" routerLink="users" routerLinkActive="active"> <a class="nav-link" routerLink="users" routerLinkActive="active">
<i class="nav-icon icon-user-o"></i> <div class="nav-text">Users</div> <i class="nav-icon icon-user-o"></i> <div class="nav-text">Users</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="resources | sqxHasLink:'admin/restore'"> <li class="nav-item" *ngIf="uiState.canRestore | async">
<a class="nav-link" routerLink="restore" routerLinkActive="active"> <a class="nav-link" routerLink="restore" routerLinkActive="active">
<i class="nav-icon icon-backup"></i> <div class="nav-text">Restore</div> <i class="nav-icon icon-backup"></i> <div class="nav-text">Restore</div>
</a> </a>

6
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -42,13 +42,13 @@
<span>{{eventConsumer.position}}</span> <span>{{eventConsumer.position}}</span>
</td> </td>
<td class="cell-actions-lg"> <td class="cell-actions-lg">
<button type="button" class="btn btn-text" (click)="reset(eventConsumer)" *ngIf="eventConsumer | sqxHasLink:'reset'" title="Reset Event Consumer"> <button type="button" class="btn btn-text" (click)="reset(eventConsumer)" *ngIf="eventConsumer.canReset" title="Reset Event Consumer">
<i class="icon icon-reset"></i> <i class="icon icon-reset"></i>
</button> </button>
<button type="button" class="btn btn-text" (click)="start(eventConsumer)" *ngIf="eventConsumer | sqxHasLink:'start'" title="Start Event Consumer"> <button type="button" class="btn btn-text" (click)="start(eventConsumer)" *ngIf="eventConsumer.canStart" title="Start Event Consumer">
<i class="icon icon-play"></i> <i class="icon icon-play"></i>
</button> </button>
<button type="button" class="btn btn-text" (click)="stop(eventConsumer)" *ngIf="eventConsumer | sqxHasLink:'stop'" title="Stop Event Consumer"> <button type="button" class="btn btn-text" (click)="stop(eventConsumer)" *ngIf="eventConsumer.canStop" title="Stop Event Consumer">
<i class="icon icon-pause"></i> <i class="icon icon-pause"></i>
</button> </button>
</td> </td>

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

@ -9,7 +9,7 @@ import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { hasAnyLink, ResourceOwner } from '@app/shared'; import { ResourceOwner } from '@app/shared';
import { import {
CreateUserDto, CreateUserDto,
@ -47,7 +47,7 @@ export class UserPageComponent extends ResourceOwner implements OnInit {
if (selectedUser) { if (selectedUser) {
this.userForm.load(selectedUser); this.userForm.load(selectedUser);
this.isEditable = hasAnyLink(this.user, 'update'); this.isEditable = this.user.canUpdate;
if (!this.isEditable) { if (!this.isEditable) {
this.userForm.form.disable(); this.userForm.form.disable();

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

@ -17,7 +17,7 @@
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> <input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form> </form>
<ng-container *ngIf="usersState.links | async | sqxHasLink: 'create'"> <ng-container *ngIf="usersState.canCreate | async">
<sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut>
<button type="button" class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)"> <button type="button" class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)">
@ -63,10 +63,10 @@
<span class="user-email table-cell">{{user.email}}</span> <span class="user-email table-cell">{{user.email}}</span>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<button type="button" class="btn btn-text" (click)="lock(user)" sqxStopClick *ngIf="user | sqxHasLink:'lock'" title="Lock User"> <button type="button" class="btn btn-text" (click)="lock(user)" sqxStopClick *ngIf="user.canLock" title="Lock User">
<i class="icon icon-unlocked"></i> <i class="icon icon-unlocked"></i>
</button> </button>
<button type="button" class="btn btn-text" (click)="unlock(user)" sqxStopClick *ngIf="user | sqxHasLink:'unlock'" title="Unlock User"> <button type="button" class="btn btn-text" (click)="unlock(user)" sqxStopClick *ngIf="user.canUnlock" title="Unlock User">
<i class="icon icon-lock"></i> <i class="icon icon-lock"></i>
</button> </button>
</td> </td>

14
src/Squidex/app/features/administration/services/event-consumers.service.spec.ts

@ -8,7 +8,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource } from '@app/framework'; import { ApiUrlConfig, Resource, ResourceLinks } from '@app/framework';
import { import {
EventConsumerDto, EventConsumerDto,
@ -151,16 +151,14 @@ describe('EventConsumersService', () => {
}); });
export function createEventConsumer(id: number, suffix = '') { export function createEventConsumer(id: number, suffix = '') {
const result = new EventConsumerDto( const links: ResourceLinks = {
reset: { method: 'PUT', href: `/event-consumers/${id}/reset` }
};
return new EventConsumerDto(links,
`event-consumer${id}`, `event-consumer${id}`,
true, true,
true, true,
`failure-${id}${suffix}`, `failure-${id}${suffix}`,
`position-${id}${suffix}`); `position-${id}${suffix}`);
result._links['reset'] = {
method: 'PUT', href: `/event-consumers/${id}/reset`
};
return result;
} }

43
src/Squidex/app/features/administration/services/event-consumers.service.ts

@ -12,31 +12,41 @@ import { map } from 'rxjs/operators';
import { import {
ApiUrlConfig, ApiUrlConfig,
hasAnyLink,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks
withLinks
} from '@app/shared'; } from '@app/shared';
export class EventConsumersDto { export class EventConsumersDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
constructor( constructor(
public readonly items: EventConsumerDto[] public readonly items: EventConsumerDto[], links?: ResourceLinks
) { ) {
this._links = links || {};
} }
} }
export class EventConsumerDto { export class EventConsumerDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
constructor( public readonly canStop: boolean;
public readonly canStart: boolean;
public readonly canRestart: boolean;
constructor(links: ResourceLinks,
public readonly name: string, public readonly name: string,
public readonly isStopped?: boolean, public readonly isStopped?: boolean,
public readonly isResetting?: boolean, public readonly isResetting?: boolean,
public readonly error?: string, public readonly error?: string,
public readonly position?: string public readonly position?: string
) { ) {
this._links = links;
this.canStop = hasAnyLink(links, 'stop');
this.canStart = hasAnyLink(links, 'start');
this.canRestart = hasAnyLink(links, 'canReset');
} }
} }
@ -52,10 +62,10 @@ export class EventConsumersService {
const url = this.apiUrl.buildUrl('/api/event-consumers'); const url = this.apiUrl.buildUrl('/api/event-consumers');
return this.http.get<{ items: any[] } & Resource>(url).pipe( return this.http.get<{ items: any[] } & Resource>(url).pipe(
map(body => { map(({ items, _links }) => {
const eventConsumers = body.items.map(item => parseEventConsumer(item)); const eventConsumers = items.map(item => parseEventConsumer(item));
return withLinks(new EventConsumersDto(eventConsumers), body); return new EventConsumersDto(eventConsumers, _links);
}), }),
pretifyError('Failed to load event consumers. Please reload.')); pretifyError('Failed to load event consumers. Please reload.'));
} }
@ -98,12 +108,11 @@ export class EventConsumersService {
} }
function parseEventConsumer(response: any): EventConsumerDto { function parseEventConsumer(response: any): EventConsumerDto {
return withLinks( return new EventConsumerDto(
new EventConsumerDto( response._links,
response.name, response.name,
response.isStopped, response.isStopped,
response.isResetting, response.isResetting,
response.error, response.error,
response.position), response.position);
response);
} }

14
src/Squidex/app/features/administration/services/users.service.spec.ts

@ -8,7 +8,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource } from '@app/framework'; import { ApiUrlConfig, Resource, ResourceLinks } from '@app/framework';
import { import {
UserDto, UserDto,
@ -227,7 +227,11 @@ describe('UsersService', () => {
}); });
export function createUser(id: number, suffix = '') { export function createUser(id: number, suffix = '') {
const result = new UserDto( const links: ResourceLinks = {
update: { method: 'PUT', href: `/users/${id}` }
};
return new UserDto(links,
`${id}`, `${id}`,
`user${id}${suffix}@domain.com`, `user${id}${suffix}@domain.com`,
`user${id}${suffix}`, `user${id}${suffix}`,
@ -235,10 +239,4 @@ export function createUser(id: number, suffix = '') {
`Permission${id}${suffix}` `Permission${id}${suffix}`
], ],
true); true);
result._links['update'] = {
method: 'PUT', href: `/users/${id}`
};
return result;
} }

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

@ -12,27 +12,38 @@ import { map } from 'rxjs/operators';
import { import {
ApiUrlConfig, ApiUrlConfig,
hasAnyLink,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
ResultSet, ResultSet
withLinks
} from '@app/shared'; } from '@app/shared';
export class UsersDto extends ResultSet<UserDto> { export class UsersDto extends ResultSet<UserDto> {
public readonly _links: ResourceLinks = {}; public get canCreate() {
return hasAnyLink(this._links, 'create');
}
} }
export class UserDto { export class UserDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
constructor( public readonly canLock: boolean;
public readonly canUnlock: boolean;
public readonly canUpdate: boolean;
constructor(links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly email: string, public readonly email: string,
public readonly displayName: string, public readonly displayName: string,
public readonly permissions: string[] = [], public readonly permissions: string[] = [],
public readonly isLocked?: boolean public readonly isLocked?: boolean
) { ) {
this._links = links;
this.canLock = hasAnyLink(links, 'lock');
this.canUnlock = hasAnyLink(links, 'unlock');
this.canUpdate = hasAnyLink(links, 'update');
} }
} }
@ -62,10 +73,10 @@ export class UsersService {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`); const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe( return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe(
map(body => { map(({ total, items, _links }) => {
const users = body.items.map(item => parseUser(item)); const users = items.map(item => parseUser(item));
return withLinks(new UsersDto(body.total, users), body); return new UsersDto(total, users, _links);
}), }),
pretifyError('Failed to load users. Please reload.')); pretifyError('Failed to load users. Please reload.'));
} }
@ -128,12 +139,11 @@ export class UsersService {
} }
function parseUser(response: any) { function parseUser(response: any) {
return withLinks( return new UserDto(
new UserDto( response._links,
response.id, response.id,
response.email, response.email,
response.displayName, response.displayName,
response.permissions, response.permissions,
response.isLocked), response.isLocked);
response);
} }

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

@ -16,7 +16,6 @@ import {
UsersService UsersService
} from '@app/features/administration/internal'; } from '@app/features/administration/internal';
import { UsersState } from './users.state'; import { UsersState } from './users.state';
import { createUser } from './../services/users.service.spec'; import { createUser } from './../services/users.service.spec';

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

@ -37,14 +37,17 @@ interface Snapshot {
// The query to filter users. // The query to filter users.
usersQuery?: string; usersQuery?: string;
// The resource links.
links: ResourceLinks;
// Indicates if the users are loaded. // Indicates if the users are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// The selected user. // The selected user.
selectedUser?: UserDto | null; selectedUser?: UserDto | null;
// Indicates if the user can create a user.
canCreate?: boolean;
// The resource links.
_links?: ResourceLinks;
} }
export type UsersList = ImmutableArray<UserDto>; export type UsersList = ImmutableArray<UserDto>;
@ -68,15 +71,15 @@ export class UsersState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly usersService: UsersService private readonly usersService: UsersService
) { ) {
super({ users: ImmutableArray.empty(), usersPager: new Pager(0), links: {} }); super({ users: ImmutableArray.empty(), usersPager: new Pager(0) });
} }
public select(id: string | null): Observable<UserDto | null> { public select(id: string | null): Observable<UserDto | null> {
@ -116,7 +119,7 @@ export class UsersState extends State<Snapshot> {
this.snapshot.usersPager.pageSize, this.snapshot.usersPager.pageSize,
this.snapshot.usersPager.skip, this.snapshot.usersPager.skip,
this.snapshot.usersQuery).pipe( this.snapshot.usersQuery).pipe(
tap(({ total, items, _links: links }) => { tap(({ total, items, _links, canCreate }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Users reloaded.'); this.dialogs.notifyInfo('Users reloaded.');
} }
@ -131,7 +134,7 @@ export class UsersState extends State<Snapshot> {
selectedUser = users.find(x => x.id === selectedUser!.id) || selectedUser; selectedUser = users.find(x => x.id === selectedUser!.id) || selectedUser;
} }
return { ...s, users, usersPager, selectedUser, isLoaded: true, links }; return { ...s, users, usersPager, selectedUser, isLoaded: true, _links };
}); });
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));

21
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -17,11 +17,11 @@
<ng-container menu> <ng-container menu>
<ng-container *ngIf="!content; else notNew"> <ng-container *ngIf="!content; else notNew">
<button type="button" class="btn btn-secondary" (click)="save()" *ngIf="contentsState.links | async | sqxHasLink:'create'"> <button type="button" class="btn btn-secondary" (click)="save()" *ngIf="contentsState.canCreate | async">
Save Save
</button> </button>
<button type="submit" class="btn btn-primary ml-1" title="CTRL + S" *ngIf="contentsState.links | async | sqxHasLink:'create/publish'"> <button type="submit" class="btn btn-primary ml-1" title="CTRL + S" *ngIf="contentsState.canCreateAndPublish | async">
Save and Publish Save and Publish
</button> </button>
@ -45,13 +45,13 @@
<ng-container *ngIf="content.isPending || !schema.isSingleton"> <ng-container *ngIf="content.isPending || !schema.isSingleton">
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade> <div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="discardChanges()" *ngIf="content | sqxHasLink:'changes/discard'"> <a class="dropdown-item" (click)="discardChanges()" *ngIf="content.canDiscardChanges">
Discard changes Discard changes
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" (click)="publishChanges()" *ngIf="content | sqxHasLink:'changes/publish'"> <a class="dropdown-item" (click)="publishChanges()" *ngIf="content.canPublishChanges">
Publish changes Publish changes
</a> </a>
@ -63,7 +63,7 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete"
[class.disabled]="content | sqxHasNoLink:'delete'" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="Delete content" confirmTitle="Delete content"
confirmText="Do you really want to delete the content?"> confirmText="Do you really want to delete the content?">
@ -74,13 +74,12 @@
</ng-container> </ng-container>
</div> </div>
<button type="button" class="btn btn-secondary ml-1" (click)="saveAsProposal()" *ngIf="content.canProposeChange">
<ng-container *ngIf="content | sqxHasLink:'update'"> Save as Proposal
<button type="button" class="btn btn-secondary ml-1" (click)="saveAsProposal()" *ngIf="content | sqxHasLink:'update/change'"> </button>
Save as Proposal
</button>
<button type="submit" class="btn btn-primary ml-1" title="CTRL + S" *ngIf="content | sqxHasLink:'update'"> <ng-container *ngIf="content.canUpdate">
<button type="submit" class="btn btn-primary ml-1" title="CTRL + S">
Save Save
</button> </button>

3
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -24,7 +24,6 @@ import {
EditContentForm, EditContentForm,
fadeAnimation, fadeAnimation,
FieldDto, FieldDto,
hasAnyLink,
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
MessageBus, MessageBus,
@ -175,7 +174,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
} }
private loadContent(data: any) { private loadContent(data: any) {
this.contentForm.loadContent(data, this.content && !hasAnyLink(this.content, 'update')); this.contentForm.loadContent(data, this.content && !this.content.canUpdate);
} }
public discardChanges() { public discardChanges() {

2
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -36,7 +36,7 @@
<div class="col-auto pl-1" *ngIf="languages.length > 1"> <div class="col-auto pl-1" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.mutableValues"></sqx-language-selector> <sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.mutableValues"></sqx-language-selector>
</div> </div>
<div class="col-auto pl-1" *ngIf="contentsState.links | async | sqxHasLink:'create'"> <div class="col-auto pl-1" *ngIf="contentsState.canCreateAny | async">
<button type="button" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)"> <button type="button" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)">
<i class="icon-plus"></i> New <i class="icon-plus"></i> New
</button> </button>

3
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -16,7 +16,6 @@ import {
ContentQueryStatus, ContentQueryStatus,
ContentsState, ContentsState,
FilterState, FilterState,
hasAnyLink,
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
ModalModel, ModalModel,
@ -231,7 +230,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
} }
} }
if (!hasAnyLink(content, 'delete')) { if (!content.canDelete) {
this.selectionCanDelete = false; this.selectionCanDelete = false;
} }
} else { } else {

15
src/Squidex/app/features/content/shared/content-item.component.html

@ -55,19 +55,10 @@
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade> <div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="emitPublish()" *ngIf="content.status === 'Draft'"> <a class="dropdown-item" *ngFor="let status of content.statusUpdates" (click)="emitChangeStatus(status)">
Publish Status to {{status}}
</a> </a>
<a class="dropdown-item" (click)="emitUnpublish()" *ngIf="content.status === 'Published'"> <a class="dropdown-item" (click)="emitClone(); dropdown.hide()" *ngIf="canClone">
Unpublish
</a>
<a class="dropdown-item" (click)="emitArchive()" *ngIf="content.status !== 'Archived'">
Archive
</a>
<a class="dropdown-item" (click)="emitRestore()" *ngIf="content.status === 'Archived'">
Restore
</a>
<a class="dropdown-item" (click)="emitClone(); dropdown.hide()" *ngIf="content.status !== 'Archived'">
Clone Clone
</a> </a>

3
src/Squidex/app/features/content/shared/content-item.component.ts

@ -58,6 +58,9 @@ export class ContentItemComponent implements OnChanges {
@Input() @Input()
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
@Input()
public canClone: boolean;
@Input() @Input()
public isReadOnly = false; public isReadOnly = false;

2
src/Squidex/app/features/dashboard/pages/dashboard-page.component.html

@ -12,7 +12,7 @@
</div> </div>
<div class="clearfix"> <div class="clearfix">
<a class="card card-href" [routerLink]="['schemas', { showDialog: true }]" *ngIf="app | sqxHasLink:'schemas/create'"> <a class="card card-href" [routerLink]="['schemas', { showDialog: true }]" *ngIf="app.canCreateSchema">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/dashboard-schema.png" /> <img src="./images/dashboard-schema.png" />

3
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts

@ -10,7 +10,6 @@ import { FormGroup } from '@angular/forms';
import { import {
Form, Form,
hasAnyLink,
ImmutableArray, ImmutableArray,
RuleDto, RuleDto,
RuleElementDto, RuleElementDto,
@ -64,7 +63,7 @@ export class RuleWizardComponent implements AfterViewInit, OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.isEditable = !this.rule || hasAnyLink(this.rule, 'update'); this.isEditable = !this.rule || this.rule.canUpdate;
if (this.mode === MODE_EDIT_ACTION) { if (this.mode === MODE_EDIT_ACTION) {
this.step = 4; this.step = 4;

10
src/Squidex/app/features/rules/pages/rules/rules-page.component.html

@ -12,7 +12,7 @@
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<ng-container *ngIf="rulesState.links | async | sqxHasLink:'create'"> <ng-container *ngIf="rulesState.canCreate | async">
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<button type="button" class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)"> <button type="button" class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)">
@ -26,7 +26,7 @@
<div class="table-items-row table-items-row-empty" *ngIf="rules.length === 0"> <div class="table-items-row table-items-row-empty" *ngIf="rules.length === 0">
No rule created yet. No rule created yet.
<button type="button" class="btn btn-success btn-sm ml-2" (click)="createNew()" *ngIf="rulesState.links | async | sqxHasLink:'create'"> <button type="button" class="btn btn-success btn-sm ml-2" (click)="createNew()" *ngIf="rulesState.canCreate | async">
<i class="icon icon-plus"></i> Add Rule <i class="icon icon-plus"></i> Add Rule
</button> </button>
</div> </div>
@ -51,11 +51,11 @@
</span> </span>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<sqx-toggle [disabled]="rule | sqxHasNoLink:'delete'" [ngModel]="rule.isEnabled" (ngModelChange)="toggle(rule)"></sqx-toggle> <sqx-toggle [disabled]="!rule.canDisable && !rule.canEnable" [ngModel]="rule.isEnabled" (ngModelChange)="toggle(rule)"></sqx-toggle>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<button type="button" class="btn btn-text-danger" <button type="button" class="btn btn-text-danger"
[disabled]="rule | sqxHasNoLink:'delete'" [disabled]="!rule.canDelete"
(sqxConfirmClick)="delete(rule)" (sqxConfirmClick)="delete(rule)"
confirmTitle="Delete rule" confirmTitle="Delete rule"
confirmText="Do you really want to delete the rule?"> confirmText="Do you really want to delete the rule?">
@ -82,7 +82,7 @@
<ng-container sidebar> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<ng-container *ngIf="rulesState.links | async | sqxHasLink:'create'"> <ng-container *ngIf="rulesState.canCreate | async">
<a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" title="History" titlePosition="left"> <a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>

12
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -30,21 +30,21 @@
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade> <div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<ng-container *ngIf="field.properties.isContentField"> <ng-container *ngIf="field.properties.isContentField">
<a class="dropdown-item" (click)="enableField()" *ngIf="field | sqxHasLink:'enable'"> <a class="dropdown-item" (click)="enableField()" *ngIf="field.canEnable">
Enable in UI Enable in UI
</a> </a>
<a class="dropdown-item" (click)="disableField()" *ngIf="field | sqxHasLink:'disable'"> <a class="dropdown-item" (click)="disableField()" *ngIf="field.canDisable">
Disable in UI Disable in UI
</a> </a>
<a class="dropdown-item" (click)="hideField()" *ngIf="field | sqxHasLink:'hide'"> <a class="dropdown-item" (click)="hideField()" *ngIf="field.canHide">
Hide in API Hide in API
</a> </a>
<a class="dropdown-item" (click)="showField()" *ngIf="field | sqxHasLink:'show'"> <a class="dropdown-item" (click)="showField()" *ngIf="field.canShow">
Show in API Show in API
</a> </a>
</ng-container> </ng-container>
<ng-container *ngIf="field | sqxHasLink:'lock'"> <ng-container *ngIf="field.canLock">
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" <a class="dropdown-item"
@ -59,7 +59,7 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete"
[class.disabled]="field | sqxHasNoLink:'delete'" [class.disabled]="!field.canDelete"
(sqxConfirmClick)="deleteField()" (sqxConfirmClick)="deleteField()"
confirmTitle="Delete field" confirmTitle="Delete field"
confirmText="Do you really want to delete the field?"> confirmText="Do you really want to delete the field?">

3
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -13,7 +13,6 @@ import {
DialogModel, DialogModel,
EditFieldForm, EditFieldForm,
fadeAnimation, fadeAnimation,
hasAnyLink,
ImmutableArray, ImmutableArray,
ModalModel, ModalModel,
NestedFieldDto, NestedFieldDto,
@ -61,7 +60,7 @@ export class FieldComponent implements OnChanges {
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['field']) { if (changes['field']) {
this.isEditable = hasAnyLink(this.field, 'update'); this.isEditable = this.field.canUpdate;
this.editForm.load(this.field.properties); this.editForm.load(this.field.properties);
} }

3
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts

@ -10,7 +10,6 @@ import { FormBuilder } from '@angular/forms';
import { import {
EditSchemaForm, EditSchemaForm,
hasAnyLink,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState SchemasState
} from '@app/shared'; } from '@app/shared';
@ -38,7 +37,7 @@ export class SchemaEditFormComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.isEditable = hasAnyLink(this.schema, 'update'); this.isEditable = this.schema.canUpdate;
this.editForm.load(this.schema.properties); this.editForm.load(this.schema.properties);

17
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -11,10 +11,10 @@
</button> </button>
<div class="btn-group mr-1" #buttonPublish> <div class="btn-group mr-1" #buttonPublish>
<button type="button" class="btn btn-publishing btn-toggle" [class.btn-success]="schema.isPublished" [disabled]="schema | sqxHasNoLink:'publish'" (click)="publish()"> <button type="button" class="btn btn-publishing btn-toggle" [class.btn-success]="schema.isPublished" [disabled]="!schema.canPublish" (click)="publish()">
Published Published
</button> </button>
<button type="button" class="btn btn-publishing btn-toggle" [class.btn-danger]="!schema.isPublished" [disabled]="schema | sqxHasNoLink:'unpublish'" (click)="unpublish()"> <button type="button" class="btn btn-publishing btn-toggle" [class.btn-danger]="!schema.isPublished" [disabled]="!schema.canUnpublish" (click)="unpublish()">
Unpublished Unpublished
</button> </button>
</div> </div>
@ -31,7 +31,7 @@
Edit Preview Urls Edit Preview Urls
</a> </a>
<ng-container *ngIf="schemasState.links | async | sqxHasLink:'create'"> <ng-container *ngIf="schemasState.canCreate">
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" (click)="cloneSchema()"> <a class="dropdown-item" (click)="cloneSchema()">
@ -39,10 +39,11 @@
</a> </a>
</ng-container> </ng-container>
<ng-container *ngIf="schema | sqxHasLink:'delete'"> <ng-container>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete"
[class.disabled]="!schema.canDelete"
(sqxConfirmClick)="deleteSchema()" (sqxConfirmClick)="deleteSchema()"
confirmTitle="Delete schema" confirmTitle="Delete schema"
confirmText="Do you really want to delete the schema?"> confirmText="Do you really want to delete the schema?">
@ -65,14 +66,14 @@
<div class="table-items-row table-items-row-empty" *ngIf="schema && schema.fields.length === 0"> <div class="table-items-row table-items-row-empty" *ngIf="schema && schema.fields.length === 0">
No field created yet. No field created yet.
<button type="button" class="btn btn-success btn-sm ml-2" (click)="addFieldDialog.show()" *ngIf="schemasState.links | async | sqxHasLink:'fields/add'"> <button type="button" class="btn btn-success btn-sm ml-2" (click)="addFieldDialog.show()" *ngIf="schema.canAddField">
<i class="icon icon-plus"></i> Add Field <i class="icon icon-plus"></i> Add Field
</button> </button>
</div> </div>
<ng-container *ngIf="patternsState.patterns | async; let patterns"> <ng-container *ngIf="patternsState.patterns | async; let patterns">
<div class="schemas" <div class="schemas"
[sqxSortDisabled]="schema | sqxHasNoLink:'update'" [sqxSortDisabled]="!schema.canOrderFields"
[sqxSortModel]="schema.fields" [sqxSortModel]="schema.fields"
(sqxSort)="sortFields($event)"> (sqxSort)="sortFields($event)">
<div *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)"> <div *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)">
@ -80,7 +81,7 @@
</div> </div>
</div> </div>
<button type="button" class="btn btn-success field-button" (click)="addFieldDialog.show()" *ngIf="schema | sqxHasLink:'fields/add'"> <button type="button" class="btn btn-success field-button" (click)="addFieldDialog.show()" *ngIf="schema.canAddField">
<i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">Add Field</div> <i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">Add Field</div>
</button> </button>
</ng-container> </ng-container>

3
src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.ts

@ -11,7 +11,6 @@ import { FormBuilder } from '@angular/forms';
import { import {
AddPreviewUrlForm, AddPreviewUrlForm,
ConfigurePreviewUrlsForm, ConfigurePreviewUrlsForm,
hasAnyLink,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState SchemasState
} from '@app/shared'; } from '@app/shared';
@ -41,7 +40,7 @@ export class SchemaPreviewUrlsFormComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.isEditable = hasAnyLink(this.schema, 'update'); this.isEditable = this.schema.canUpdateUrls;
this.editForm.load(this.schema.previewUrls); this.editForm.load(this.schema.previewUrls);

3
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts

@ -10,7 +10,6 @@ import { FormBuilder } from '@angular/forms';
import { import {
EditScriptsForm, EditScriptsForm,
hasAnyLink,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState SchemasState
} from '@app/shared'; } from '@app/shared';
@ -40,7 +39,7 @@ export class SchemaScriptsFormComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.isEditable = hasAnyLink(this.schema, 'update'); this.isEditable = this.schema.canUpdateScripts;
this.editForm.load(this.schema.scripts); this.editForm.load(this.schema.scripts);

5
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html

@ -8,11 +8,10 @@
<ng-container secondHeader> <ng-container secondHeader>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<ng-container> <ng-container *ngIf="schemasState.canCreate | async">
<sqx-shortcut keys="ctrl+shift+g" (trigger)="addSchemaDialog.show()" [disabled]="schemasState.links | sqxHasNoLink:'create'"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+g" (trigger)="addSchemaDialog.show()"></sqx-shortcut>
<button type="button" class="btn btn-success subheader-button" (click)="createSchema()" <button type="button" class="btn btn-success subheader-button" (click)="createSchema()"
[disabled]="schemasState.links | async | sqxHasNoLink:'create'"
title="New Schema (CTRL + SHIFT + G)" title="New Schema (CTRL + SHIFT + G)"
titlePosition="top-left"> titlePosition="top-left">
<i class="icon-plus"></i> <i class="icon-plus"></i>

4
src/Squidex/app/features/settings/pages/backups/backups-page.component.html

@ -12,7 +12,7 @@
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<button type="button" class="btn btn-success" [disabled]="backupsState.maxBackupsReached | async" *ngIf="backupsState.links | async | sqxHasLink:'create'" (click)="start()"> <button type="button" class="btn btn-success" [disabled]="backupsState.maxBackupsReached | async" *ngIf="backupsState.canCreate | async" (click)="start()">
Start Backup Start Backup
</button> </button>
</ng-container> </ng-container>
@ -79,7 +79,7 @@
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-text-danger mt-1" <button type="button" class="btn btn-text-danger mt-1"
[disabled]="backup | sqxHasNoLink:'delete'" [disabled]="!backup.canDelete"
(sqxConfirmClick)="delete(backup)" (sqxConfirmClick)="delete(backup)"
confirmTitle="Delete backup" confirmTitle="Delete backup"
confirmText="Do you really want to delete the backup?"> confirmText="Do you really want to delete the backup?">

6
src/Squidex/app/features/settings/pages/clients/client.component.html

@ -21,7 +21,7 @@
{{client.name}} {{client.name}}
</h3> </h3>
<i class="client-edit icon-pencil" *ngIf="isEditable" (click)="toggleRename()"></i> <i class="client-edit icon-pencil" *ngIf="client.canUpdate" (click)="toggleRename()"></i>
</ng-template> </ng-template>
</div> </div>
<div class="col-auto"> <div class="col-auto">
@ -29,7 +29,7 @@
</div> </div>
<div class="col-auto cell-actions no-padding"> <div class="col-auto cell-actions no-padding">
<button type="button" class="btn btn-text-danger" <button type="button" class="btn btn-text-danger"
[disabled]="client | sqxHasNoLink:'delete'" [disabled]="!client.canRevoke"
(sqxConfirmClick)="revoke()" (sqxConfirmClick)="revoke()"
confirmTitle="Revoke client" confirmTitle="Revoke client"
confirmText="Do you really want to revoke the client?"> confirmText="Do you really want to revoke the client?">
@ -72,7 +72,7 @@
</label> </label>
<div class="col cell-input"> <div class="col cell-input">
<select class="form-control" <select class="form-control"
[disabled]="!isEditable" [disabled]="!client.canUpdate"
[ngModel]="client.role" [ngModel]="client.role"
(ngModelChange)="update($event)"> (ngModelChange)="update($event)">
<option *ngFor="let role of clientRoles; trackBy: trackByRole" [ngValue]="role.name">{{role.name}}</option> <option *ngFor="let role of clientRoles; trackBy: trackByRole" [ngValue]="role.name">{{role.name}}</option>

8
src/Squidex/app/features/settings/pages/clients/client.component.ts

@ -17,7 +17,6 @@ import {
ClientsState, ClientsState,
DialogModel, DialogModel,
DialogService, DialogService,
hasAnyLink,
RenameClientForm, RenameClientForm,
RoleDto RoleDto
} from '@app/shared'; } from '@app/shared';
@ -37,7 +36,6 @@ export class ClientComponent implements OnChanges {
public clientRoles: RoleDto[]; public clientRoles: RoleDto[];
public isRenaming = false; public isRenaming = false;
public isEditable = false;
public connectToken: AccessTokenDto; public connectToken: AccessTokenDto;
public connectDialog = new DialogModel(); public connectDialog = new DialogModel();
@ -61,8 +59,6 @@ export class ClientComponent implements OnChanges {
} }
public ngOnChanges() { public ngOnChanges() {
this.isEditable = hasAnyLink(this.client, 'update');
this.renameForm.load(this.client); this.renameForm.load(this.client);
const app = this.appsState.appName; const app = this.appsState.appName;
@ -83,7 +79,7 @@ export class ClientComponent implements OnChanges {
} }
public toggleRename() { public toggleRename() {
if (!this.isEditable) { if (!this.client.canUpdate) {
return; return;
} }
@ -91,7 +87,7 @@ export class ClientComponent implements OnChanges {
} }
public rename() { public rename() {
if (!this.isEditable) { if (!this.client.canUpdate) {
return; return;
} }

2
src/Squidex/app/features/settings/pages/clients/clients-page.component.html

@ -26,7 +26,7 @@
</sqx-client> </sqx-client>
</ng-container> </ng-container>
<div class="table-items-footer" *ngIf="clientsState.links | async | sqxHasLink:'create'"> <div class="table-items-footer" *ngIf="clientsState.canCreate | async">
<form [formGroup]="addClientForm.form" (ngSubmit)="attachClient()"> <form [formGroup]="addClientForm.form" (ngSubmit)="attachClient()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">

6
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -35,12 +35,12 @@
<select class="form-control" <select class="form-control"
[ngModel]="contributor.role" [ngModel]="contributor.role"
(ngModelChange)="changeRole(contributor, $event)" (ngModelChange)="changeRole(contributor, $event)"
[disabled]="contributor | sqxHasNoLink:'update'"> [disabled]="!contributor.canUpdate">
<option *ngFor="let role of roles" [ngValue]="role.name">{{role.name}}</option> <option *ngFor="let role of roles" [ngValue]="role.name">{{role.name}}</option>
</select> </select>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<button *ngIf="!isCurrentUser" type="button" class="btn btn-text-danger" [disabled]="contributor | sqxHasNoLink:'delete'" (click)="remove(contributor)"> <button type="button" class="btn btn-text-danger" [disabled]="!contributor.canRevoke" (click)="remove(contributor)">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</td> </td>
@ -50,7 +50,7 @@
</table> </table>
<ng-container> <ng-container>
<div class="table-items-footer" *ngIf="(contributorsState.isMaxReached | async) === false && contributorsState.links | async | sqxHasLink:'create'"> <div class="table-items-footer" *ngIf="contributorsState.canCreate | async">
<form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()"> <form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">

2
src/Squidex/app/features/settings/pages/languages/language.component.html

@ -14,7 +14,7 @@
</button> </button>
<button type="button" class="btn btn-text-danger" <button type="button" class="btn btn-text-danger"
[disabled]="language | sqxHasNoLink:'delete'" [disabled]="!language.canDelete"
(sqxConfirmClick)="remove()" (sqxConfirmClick)="remove()"
confirmTitle="Remove language" confirmTitle="Remove language"
confirmText="Do you really want to remove the language?"> confirmText="Do you really want to remove the language?">

3
src/Squidex/app/features/settings/pages/languages/language.component.ts

@ -12,7 +12,6 @@ import {
AppLanguageDto, AppLanguageDto,
EditLanguageForm, EditLanguageForm,
fadeAnimation, fadeAnimation,
hasAnyLink,
ImmutableArray, ImmutableArray,
LanguagesState LanguagesState
} from '@app/shared'; } from '@app/shared';
@ -96,7 +95,7 @@ export class LanguageComponent implements OnChanges {
} }
private resetForm() { private resetForm() {
this.isEditable = hasAnyLink(this.language, 'update'); this.isEditable = this.language.canUpdate;
this.editForm.load(this.language); this.editForm.load(this.language);

2
src/Squidex/app/features/settings/pages/languages/languages-page.component.html

@ -22,7 +22,7 @@
</sqx-language> </sqx-language>
<ng-container *ngIf="languagesState.newLanguages | async; let newLanguages"> <ng-container *ngIf="languagesState.newLanguages | async; let newLanguages">
<div class="table-items-footer" *ngIf="languagesState.links | async | sqxHasLink:'create'"> <div class="table-items-footer" *ngIf="languagesState.canCreate | async">
<form [formGroup]="addLanguageForm.form" (ngSubmit)="addLanguage()"> <form [formGroup]="addLanguageForm.form" (ngSubmit)="addLanguage()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">

5
src/Squidex/app/features/settings/pages/patterns/pattern.component.ts

@ -10,7 +10,6 @@ import { FormBuilder } from '@angular/forms';
import { import {
EditPatternForm, EditPatternForm,
hasAnyLink,
PatternDto, PatternDto,
PatternsState PatternsState
} from '@app/shared'; } from '@app/shared';
@ -36,8 +35,8 @@ export class PatternComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.isEditable = !this.pattern || hasAnyLink(this.pattern, 'update'); this.isEditable = !this.pattern || this.pattern.canUpdate;
this.isDeletable = !this.pattern && hasAnyLink(this.pattern, 'delete'); this.isDeletable = this.pattern && this.pattern.canDelete;
this.editForm.load(this.pattern); this.editForm.load(this.pattern);

2
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html

@ -23,7 +23,7 @@
[pattern]="pattern"> [pattern]="pattern">
</sqx-pattern> </sqx-pattern>
<sqx-pattern *ngIf="patternsState.links | async | sqxHasLink:'create'"></sqx-pattern> <sqx-pattern *ngIf="patternsState.canCreate | async"></sqx-pattern>
</ng-container> </ng-container>
</ng-container> </ng-container>

2
src/Squidex/app/features/settings/pages/roles/role.component.html

@ -17,7 +17,7 @@
</button> </button>
<button type="button" class="btn btn-text-danger" <button type="button" class="btn btn-text-danger"
[disabled]="role | sqxHasNoLink:'delete'" [disabled]="!role.canDelete"
(sqxConfirmClick)="remove()" (sqxConfirmClick)="remove()"
confirmTitle="Delete role" confirmTitle="Delete role"
confirmText="Do you really want to delete the role?"> confirmText="Do you really want to delete the role?">

3
src/Squidex/app/features/settings/pages/roles/role.component.ts

@ -14,7 +14,6 @@ import {
AutocompleteSource, AutocompleteSource,
EditPermissionsForm, EditPermissionsForm,
fadeAnimation, fadeAnimation,
hasAnyLink,
RoleDto, RoleDto,
RolesState RolesState
} from '@app/shared'; } from '@app/shared';
@ -51,7 +50,7 @@ export class RoleComponent implements OnChanges {
} }
public ngOnChanges() { public ngOnChanges() {
this.isEditable = hasAnyLink(this.role, 'update'); this.isEditable = this.role.canUpdate;
this.editForm.load(this.role.permissions); this.editForm.load(this.role.permissions);

2
src/Squidex/app/features/settings/pages/roles/roles-page.component.html

@ -17,7 +17,7 @@
<ng-container *ngIf="(rolesState.isLoaded | async) && (rolesState.roles | async); let roles"> <ng-container *ngIf="(rolesState.isLoaded | async) && (rolesState.roles | async); let roles">
<sqx-role *ngFor="let role of roles" [role]="role" [allPermissions]="allPermissions"></sqx-role> <sqx-role *ngFor="let role of roles" [role]="role" [allPermissions]="allPermissions"></sqx-role>
<div class="table-items-footer" *ngIf="rolesState.links | async | sqxHasLink:'create'"> <div class="table-items-footer" *ngIf="rolesState.canCreate | async">
<form [formGroup]="addRoleForm.form" (ngSubmit)="addRole()"> <form [formGroup]="addRoleForm.form" (ngSubmit)="addRole()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">

16
src/Squidex/app/features/settings/settings-area.component.html

@ -7,49 +7,49 @@
<ng-container content> <ng-container content>
<ul class="nav nav-panel nav-dark flex-column" *ngIf="appsState.selectedValidApp | async; let selectedApp"> <ul class="nav nav-panel nav-dark flex-column" *ngIf="appsState.selectedValidApp | async; let selectedApp">
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'backups'"> <li class="nav-item" *ngIf="selectedApp.canReadBackups">
<a class="nav-link" routerLink="backups" routerLinkActive="active"> <a class="nav-link" routerLink="backups" routerLinkActive="active">
Backups Backups
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'clients'"> <li class="nav-item" *ngIf="selectedApp.canReadClients">
<a class="nav-link" routerLink="clients" routerLinkActive="active"> <a class="nav-link" routerLink="clients" routerLinkActive="active">
Clients Clients
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'contributors'"> <li class="nav-item" *ngIf="selectedApp.canReadContributors">
<a class="nav-link" routerLink="contributors" routerLinkActive="active"> <a class="nav-link" routerLink="contributors" routerLinkActive="active">
Contributors Contributors
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'roles'"> <li class="nav-item" *ngIf="selectedApp.canReadRoles">
<a class="nav-link" routerLink="roles" routerLinkActive="active"> <a class="nav-link" routerLink="roles" routerLinkActive="active">
Roles Roles
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'languages'"> <li class="nav-item" *ngIf="selectedApp.canReadLanguages">
<a class="nav-link" routerLink="languages" routerLinkActive="active"> <a class="nav-link" routerLink="languages" routerLinkActive="active">
Languages Languages
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'patterns'"> <li class="nav-item" *ngIf="selectedApp.canReadPatterns">
<a class="nav-link" routerLink="patterns" routerLinkActive="active"> <a class="nav-link" routerLink="patterns" routerLinkActive="active">
Patterns Patterns
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'plans'"> <li class="nav-item" *ngIf="selectedApp.canReadPlans">
<a class="nav-link" routerLink="plans" routerLinkActive="active"> <a class="nav-link" routerLink="plans" routerLinkActive="active">
Subscription Subscription
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'delete'"> <li class="nav-item" *ngIf="selectedApp.canDelete">
<a class="nav-link" routerLink="more" routerLinkActive="active"> <a class="nav-link" routerLink="more" routerLinkActive="active">
More More
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>

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

@ -1,34 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Pipe, PipeTransform } from '@angular/core';
import {
hasAnyLink,
Resource,
ResourceLinks
} from '@app/framework/internal';
@Pipe({
name: 'sqxHasLink',
pure: true
})
export class HasLinkPipe implements PipeTransform {
public transform(value: Resource | ResourceLinks, ...rels: string[]) {
return hasAnyLink(value, ...rels);
}
}
@Pipe({
name: 'sqxHasNoLink',
pure: true
})
export class HasNoLinkPipe implements PipeTransform {
public transform(value: Resource | ResourceLinks, ...rels: string[]) {
return !hasAnyLink(value, ...rels);
}
}

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

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

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

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

14
src/Squidex/app/framework/state.ts

@ -14,6 +14,7 @@ import { ErrorDto } from './utils/error';
import { Types } from './utils/types'; import { Types } from './utils/types';
import { fullValue } from './angular/forms/forms-helper'; import { fullValue } from './angular/forms/forms-helper';
import { ResourceLinks } from '@app/shared';
export interface FormState { export interface FormState {
submitted: boolean; submitted: boolean;
@ -108,10 +109,6 @@ export class Form<T extends AbstractControl, V> {
} }
} }
export function createModel<T>(c: { new(): T; }, values: Partial<T>): T {
return Object.assign(new c(), values);
}
export class Model<T> { export class Model<T> {
public with(value: Partial<T>, validOnly = false): T { public with(value: Partial<T>, validOnly = false): T {
return this.clone(value, validOnly); return this.clone(value, validOnly);
@ -145,12 +142,15 @@ export class Model<T> {
} }
} }
export class ResultSet<T> extends Model<ResultSet<T>> { export class ResultSet<T> {
public readonly _links: ResourceLinks;
constructor( constructor(
public readonly total: number, public readonly total: number,
public readonly items: T[] public readonly items: T[],
links?: ResourceLinks
) { ) {
super(); this._links = links || {};
} }
} }

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

@ -16,38 +16,6 @@ export type ResourceLink = { href: string; method: ResourceMethod; };
export type Metadata = { [rel: string]: string }; export type Metadata = { [rel: string]: string };
export function withLinks<T extends Resource>(value: T, source: Resource) {
if (value._links && source._links) {
if (!value._links) {
value._links = {};
}
for (let key in source._links) {
if (source._links.hasOwnProperty(key)) {
value._links[key] = source._links[key];
}
}
Object.freeze(value._links);
}
if (source._meta) {
if (!value._meta) {
value._meta = {};
}
for (let key in source._meta) {
if (source._meta.hasOwnProperty(key)) {
value._meta[key] = source._meta[key];
}
}
Object.freeze(value._meta);
}
return value;
}
function hasLink(value: Resource | ResourceLinks, rel: string): boolean { function hasLink(value: Resource | ResourceLinks, rel: string): boolean {
const link = getLink(value, rel); const link = getLink(value, rel);

3
src/Squidex/app/shared/components/asset-dialog.component.ts

@ -13,7 +13,6 @@ import {
AppsState, AppsState,
AssetDto, AssetDto,
AssetsService, AssetsService,
hasAnyLink,
StatefulComponent StatefulComponent
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -55,7 +54,7 @@ export class AssetDialogComponent extends StatefulComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.annotateForm.load(this.asset); this.annotateForm.load(this.asset);
this.isReadOnly = !hasAnyLink(this.asset, 'update'); this.isReadOnly = !this.asset.canUpdate;
if (this.isReadOnly) { if (this.isReadOnly) {
this.annotateForm.form.disable(); this.annotateForm.form.disable();

4
src/Squidex/app/shared/components/asset.component.html

@ -23,7 +23,7 @@
<a class="file-download ml-2" [href]="asset | sqxAssetUrl" sqxStopClick sqxExternalLink="noicon"> <a class="file-download ml-2" [href]="asset | sqxAssetUrl" sqxStopClick sqxExternalLink="noicon">
<i class="icon-download"></i> <i class="icon-download"></i>
</a> </a>
<a class="file-delete ml-2" (click)="emitDelete()" *ngIf="!isDisabled && !removeMode && asset | sqxHasLink:'delete'"> <a class="file-delete ml-2" (click)="emitDelete()" *ngIf="!isDisabled && !removeMode && asset.canDelete">
<i class="icon-delete"></i> <i class="icon-delete"></i>
</a> </a>
<a class="file-delete ml-2" (click)="emitRemove()" *ngIf="removeMode"> <a class="file-delete ml-2" (click)="emitRemove()" *ngIf="removeMode">
@ -103,7 +103,7 @@
</a> </a>
</td> </td>
<td class="col-actions text-right" *ngIf="!isDisabled || removeMode"> <td class="col-actions text-right" *ngIf="!isDisabled || removeMode">
<button type="button" class="btn btn-text-danger" (click)="emitDelete()" *ngIf="!isDisabled && !removeMode && asset | sqxHasLink:'delete'"> <button type="button" class="btn btn-text-danger" (click)="emitDelete()" *ngIf="!isDisabled && !removeMode && asset.canDelete">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" (click)="emitRemove()" *ngIf="removeMode"> <button type="button" class="btn btn-text-secondary" (click)="emitRemove()" *ngIf="removeMode">

3
src/Squidex/app/shared/components/asset.component.ts

@ -13,7 +13,6 @@ import {
DialogModel, DialogModel,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
hasAnyLink,
StatefulComponent, StatefulComponent,
Types, Types,
UploadCanceled UploadCanceled
@ -113,7 +112,7 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
} }
public updateFile(files: FileList) { public updateFile(files: FileList) {
if (files.length === 1 && hasAnyLink(this.asset, 'upload')) { if (files.length === 1 && this.asset.canUpload) {
this.setProgress(1); this.setProgress(1);
this.assetUploader.uploadAsset(this.asset, files[0]) this.assetUploader.uploadAsset(this.asset, files[0])

6
src/Squidex/app/shared/components/schema-category.component.html

@ -1,4 +1,4 @@
<div *ngIf="!isReadonly || snapshot.schemasFiltered.length > 0" dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)"> <div *ngIf="!forContent || snapshot.schemasFiltered.length > 0" dnd-droppable class="droppable category" [allowDrop]="allowDrop" (onDropSuccess)="changeCategory($event.dragData)">
<div class="drop-indicator"></div> <div class="drop-indicator"></div>
<div class="header clearfix"> <div class="header clearfix">
@ -8,14 +8,14 @@
<h3>{{snapshot.displayName}} ({{snapshot.schemasFiltered.length}})</h3> <h3>{{snapshot.displayName}} ({{snapshot.schemasFiltered.length}})</h3>
<button type="button" class="btn btn-sm btn-text-secondary float-right" *ngIf="snapshot.schemasForCategory.length === 0 && !isReadonly" (click)="emitRemove()"> <button type="button" class="btn btn-sm btn-text-secondary float-right" *ngIf="snapshot.schemasForCategory.length === 0 && !forContent" (click)="emitRemove()">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade> <ul class="nav nav-panel nav-dark nav-dark-bordered flex-column" *ngIf="snapshot.isOpen" @fade>
<ng-container *ngFor="let schema of snapshot.schemasFiltered; trackBy: trackBySchema"> <ng-container *ngFor="let schema of snapshot.schemasFiltered; trackBy: trackBySchema">
<li class="nav-item" dnd-draggable [dragEnabled]="!forContent && schema | sqxHasLink:'update/category'" [dragData]="schema"> <li class="nav-item" dnd-draggable [dragEnabled]="!forContent && schema.canUpdateCategory" [dragData]="schema">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active"> <a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active">
<div class="row" *ngIf="!forContent; else simpleMode"> <div class="row" *ngIf="!forContent; else simpleMode">
<div class="col-4"> <div class="col-4">

3
src/Squidex/app/shared/components/schema-category.component.ts

@ -9,7 +9,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, In
import { import {
fadeAnimation, fadeAnimation,
hasAnyLink,
ImmutableArray, ImmutableArray,
LocalStoreService, LocalStoreService,
SchemaDetailsDto, SchemaDetailsDto,
@ -123,7 +122,7 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
} }
private isSameCategory(schema: SchemaDto): boolean { private isSameCategory(schema: SchemaDto): boolean {
return ((!this.name && !schema.category) || schema.category === this.name) && (!this.forContent || hasAnyLink(schema, 'contents')); return ((!this.name && !schema.category) || schema.category === this.name) && (!this.forContent || schema.canReadContents);
} }
public changeCategory(schema: SchemaDto) { public changeCategory(schema: SchemaDto) {

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

@ -16,8 +16,8 @@ import {
AppLanguagesPayload, AppLanguagesPayload,
AppLanguagesService, AppLanguagesService,
Resource, Resource,
Version, ResourceLinks,
withLinks Version
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('AppLanguagesService', () => { describe('AppLanguagesService', () => {
@ -169,19 +169,17 @@ describe('AppLanguagesService', () => {
export function createLanguages(...codes: string[]): AppLanguagesPayload { export function createLanguages(...codes: string[]): AppLanguagesPayload {
return { return {
items: codes.map((code, i) => items: codes.map((code, i) => createLanguage(code, codes, i)),
withLinks(
new AppLanguageDto(code, code,
i === 0,
i % 2 === 1,
codes.filter(x => x !== code)),
{
_links: {
update: { method: 'PUT', href: `/languages/${code}` }
}
})),
_links: { _links: {
create: { method: 'POST', href: '/languages' } create: { method: 'POST', href: '/languages' }
} },
canCreate: true
}; };
}
function createLanguage(code: string, codes: string[], i: number) {
const links: ResourceLinks = {
update: { method: 'PUT', href: `/languages/${code}` }
};
return new AppLanguageDto(links, code, code, i === 0, i % 2 === 1, codes.filter(x => x !== code))
} }

42
src/Squidex/app/shared/services/app-languages.service.ts

@ -13,31 +13,41 @@ import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
hasAnyLink,
HTTP, HTTP,
mapVersioned, mapVersioned,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
Version, Version,
Versioned, Versioned
withLinks
} from '@app/framework'; } from '@app/framework';
export type AppLanguagesDto = Versioned<AppLanguagesPayload>; export type AppLanguagesDto = Versioned<AppLanguagesPayload>;
export type AppLanguagesPayload = { export type AppLanguagesPayload = {
readonly items: AppLanguageDto[] readonly items: AppLanguageDto[];
readonly canCreate: boolean;
} & Resource; } & Resource;
export class AppLanguageDto { export class AppLanguageDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
public readonly canUpdate: boolean;
public readonly canDelete: boolean;
constructor( constructor(
links: ResourceLinks,
public readonly iso2Code: string, public readonly iso2Code: string,
public readonly englishName: string, public readonly englishName: string,
public readonly isMaster: boolean, public readonly isMaster: boolean,
public readonly isOptional: boolean, public readonly isOptional: boolean,
public readonly fallback: string[] public readonly fallback: string[]
) { ) {
this._links = links;
this.canUpdate = hasAnyLink(links, 'update');
this.canDelete = hasAnyLink(links, 'delete');
} }
} }
@ -114,18 +124,18 @@ export class AppLanguagesService {
} }
} }
function parseLanguages(body: any) { function parseLanguages(response: any) {
const items: any[] = body.items; const items: any[] = response.items;
const languages = items.map(item => const languages = items.map(item =>
withLinks( new AppLanguageDto(item._links,
new AppLanguageDto( item.iso2Code,
item.iso2Code, item.englishName,
item.englishName, item.isMaster,
item.isMaster, item.isOptional,
item.isOptional, item.fallback || []));
item.fallback || []),
item)); const _links = response._links;
return withLinks({ items: languages, _links: {} }, body); return { items: languages, _links, canCreate: hasAnyLink(_links, 'create') };
} }

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

@ -14,7 +14,8 @@ import {
AppDto, AppDto,
AppsService, AppsService,
DateTime, DateTime,
Resource Resource,
ResourceLinks
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('AppsService', () => { describe('AppsService', () => {
@ -116,19 +117,17 @@ describe('AppsService', () => {
}); });
export function createApp(id: number, suffix = '') { export function createApp(id: number, suffix = '') {
const result = new AppDto( const links: ResourceLinks = {
`id${id}`, schemas: { method: 'GET', href: '/schemas' }
`name${id}${suffix}`, };
return new AppDto(links,
`id${id}`, `name${id}${suffix}`,
['Owner'], ['Owner'],
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`),
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`),
id % 2 === 0, id % 2 === 0,
id % 2 === 0, id % 2 === 0,
'Free', 'Free',
'Basic' 'Basic');
);
result._links['schemas'] = { method: 'GET', href: '/schemas' };
return result;
} }

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

@ -16,14 +16,25 @@ import {
DateTime, DateTime,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks
withLinks
} from '@app/framework'; } from '@app/framework';
export class AppDto { export class AppDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
constructor( public readonly canCreateSchema: boolean;
public readonly canDelete: boolean;
public readonly canReadBackups: boolean;
public readonly canReadClients: boolean;
public readonly canReadContributors: boolean;
public readonly canReadPatterns: boolean;
public readonly canReadLanguages: boolean;
public readonly canReadPlans: boolean;
public readonly canReadRoles: boolean;
public readonly canReadRules: boolean;
public readonly canReadSchemas: boolean;
constructor(links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly name: string, public readonly name: string,
public readonly permissions: string[], public readonly permissions: string[],
@ -34,6 +45,7 @@ export class AppDto {
public readonly planName?: string, public readonly planName?: string,
public readonly planUpgrade?: string public readonly planUpgrade?: string
) { ) {
this._links = links;
} }
} }
@ -90,16 +102,14 @@ export class AppsService {
} }
function parseApp(response: any) { function parseApp(response: any) {
return withLinks( return new AppDto(response._links,
new AppDto( response.id,
response.id, response.name,
response.name, response.permissions,
response.permissions, DateTime.parseISO_UTC(response.created),
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.lastModified),
DateTime.parseISO_UTC(response.lastModified), response.canAccessApi,
response.canAccessApi, response.canAccessContent,
response.canAccessContent, response.planName,
response.planName, response.planUpgrade);
response.planUpgrade),
response);
} }

19
src/Squidex/app/shared/services/assets.service.spec.ts

@ -17,6 +17,7 @@ import {
DateTime, DateTime,
ErrorDto, ErrorDto,
Resource, Resource,
ResourceLinks,
Version Version
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -332,7 +333,15 @@ describe('AssetsService', () => {
}); });
export function createAsset(id: number, tags?: string[], suffix = '') { export function createAsset(id: number, tags?: string[], suffix = '') {
const result = new AssetDto( const links: ResourceLinks = {
update: { method: 'PUT', href: `/assets/${id}` }
};
const meta = {
isDuplicate: 'true'
};
return new AssetDto(links, meta,
`id${id}`, `id${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
@ -348,12 +357,4 @@ export function createAsset(id: number, tags?: string[], suffix = '') {
`my-name${id}${suffix}.png`, `my-name${id}${suffix}.png`,
tags || ['tag1', 'tag2'], tags || ['tag1', 'tag2'],
new Version(`${id}`)); new Version(`${id}`));
result._links['update'] = {
method: 'PUT', href: `/assets/${id}`
};
result._meta['isDuplicate'] = 'true';
return result;
} }

74
src/Squidex/app/shared/services/assets.service.ts

@ -15,6 +15,7 @@ import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
ErrorDto, ErrorDto,
hasAnyLink,
HTTP, HTTP,
Metadata, Metadata,
pretifyError, pretifyError,
@ -23,24 +24,22 @@ import {
ResultSet, ResultSet,
Types, Types,
Version, Version,
Versioned, Versioned
withLinks
} from '@app/framework'; } from '@app/framework';
export class AssetsDto extends ResultSet<AssetDto> { export class AssetsDto extends ResultSet<AssetDto> {}
public readonly _links: ResourceLinks = {};
}
export class AssetDto { export class AssetDto {
public get canPreview() {
return this.isImage || (this.mimeType === 'image/svg+xml' && this.fileSize < 100 * 1024);
}
public readonly _meta: Metadata = {}; public readonly _meta: Metadata = {};
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
constructor( public readonly canDelete: boolean;
public readonly canPreview: boolean;
public readonly canUpdate: boolean;
public readonly canUpload: boolean;
constructor(links: ResourceLinks, meta: Metadata,
public readonly id: string, public readonly id: string,
public readonly created: DateTime, public readonly created: DateTime,
public readonly createdBy: string, public readonly createdBy: string,
@ -59,6 +58,15 @@ export class AssetDto {
public readonly tags: string[], public readonly tags: string[],
public readonly version: Version public readonly version: Version
) { ) {
this.canPreview = this.isImage || (this.mimeType === 'image/svg+xml' && this.fileSize < 100 * 1024);
this._links = links;
this.canDelete = hasAnyLink(links, 'delete');
this.canUpdate = hasAnyLink(links, 'update');
this.canUpload = hasAnyLink(links, 'upload');
this._meta = meta;
} }
} }
@ -117,13 +125,11 @@ export class AssetsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`);
return HTTP.getVersioned(this.http, url).pipe( return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe(
map(({ payload }) => { map(({ total, items, _links }) => {
const { total, items } = <{ total: number, items: any[] }>payload.body; const assets = items.map(item => parseAsset(item)); ;
const assets = new AssetsDto(total, items.map(item => parseAsset(item)));
return withLinks(assets, payload.body); return new AssetsDto(total, assets, _links);
}), }),
pretifyError('Failed to load assets. Please reload.')); pretifyError('Failed to load assets. Please reload.'));
} }
@ -249,22 +255,20 @@ function getFormData(file: File) {
} }
function parseAsset(response: any) { function parseAsset(response: any) {
return withLinks( return new AssetDto(response._links, response._meta,
new AssetDto( response.id,
response.id, DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.created), response.createdBy, DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy, response.fileName,
response.fileName, response.fileHash,
response.fileHash, response.fileType,
response.fileType, response.fileSize,
response.fileSize, response.fileVersion,
response.fileVersion, response.mimeType,
response.mimeType, response.isImage,
response.isImage, response.pixelWidth,
response.pixelWidth, response.pixelHeight,
response.pixelHeight, response.slug,
response.slug, response.tags || [],
response.tags || [], new Version(response.version.toString()));
new Version(response.version.toString())),
response);
} }

17
src/Squidex/app/shared/services/backups.service.spec.ts

@ -58,7 +58,8 @@ describe('BackupsService', () => {
stopped: '2017-02-04', stopped: '2017-02-04',
handledEvents: 13, handledEvents: 13,
handledAssets: 17, handledAssets: 17,
status: 'Failed' status: 'Failed',
_links: {}
}, },
{ {
id: '2', id: '2',
@ -66,18 +67,16 @@ describe('BackupsService', () => {
stopped: null, stopped: null,
handledEvents: 23, handledEvents: 23,
handledAssets: 27, handledAssets: 27,
status: 'Completed' status: 'Completed',
_links: {}
}] }]
}); });
expect(backups!).toEqual( expect(backups!).toEqual(
new BackupsDto( new BackupsDto([
2, new BackupDto({}, '1', DateTime.parseISO_UTC('2017-02-03'), DateTime.parseISO_UTC('2017-02-04'), 13, 17, 'Failed'),
[ new BackupDto({}, '2', DateTime.parseISO_UTC('2018-02-03'), null, 23, 27, 'Completed')
new BackupDto('1', DateTime.parseISO_UTC('2017-02-03'), DateTime.parseISO_UTC('2017-02-04'), 13, 17, 'Failed'), ]));
new BackupDto('2', DateTime.parseISO_UTC('2018-02-03'), null, 23, 27, 'Completed')
]
));
})); }));
it('should make get request to get restore', it('should make get request to get restore',

48
src/Squidex/app/shared/services/backups.service.ts

@ -14,22 +14,35 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
hasAnyLink,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
ResultSet, ResultSet,
Types, Types
withLinks
} from '@app/framework'; } from '@app/framework';
export class BackupsDto extends ResultSet<BackupDto> { export class BackupsDto extends ResultSet<BackupDto> {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
constructor(items: BackupDto[], links?: {}) {
super(items.length, items);
this._links = links || {};
}
public get canCreate() {
return hasAnyLink(this._links, 'create');
}
} }
export class BackupDto { export class BackupDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
public readonly canDelete: boolean;
constructor( constructor(
links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly started: DateTime, public readonly started: DateTime,
public readonly stopped: DateTime | null, public readonly stopped: DateTime | null,
@ -37,6 +50,9 @@ export class BackupDto {
public readonly handledAssets: number, public readonly handledAssets: number,
public readonly status: string public readonly status: string
) { ) {
this._links = links;
this.canDelete = hasAnyLink(links, 'delete');
} }
} }
@ -68,11 +84,11 @@ export class BackupsService {
public getBackups(appName: string): Observable<BackupsDto> { public getBackups(appName: string): Observable<BackupsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`);
return this.http.get<{ items: any[] } & Resource>(url).pipe( return this.http.get<{ items: any[], _links: {} } & Resource>(url).pipe(
map(body => { map(({ items, _links }) => {
const backups = body.items.map(item => parseBackup(item)); const backups = items.map(item => parseBackup(item));
return withLinks(new BackupsDto(backups.length, backups), body); return new BackupsDto(backups, _links);
}), }),
pretifyError('Failed to load backups.')); pretifyError('Failed to load backups.'));
} }
@ -139,13 +155,11 @@ function parseRestore(response: any) {
} }
function parseBackup(response: any) { function parseBackup(response: any) {
return withLinks( return new BackupDto(response._links,
new BackupDto( response.id,
response.id, DateTime.parseISO_UTC(response.started),
DateTime.parseISO_UTC(response.started), response.stopped ? DateTime.parseISO_UTC(response.stopped) : null,
response.stopped ? DateTime.parseISO_UTC(response.stopped) : null, response.handledEvents,
response.handledEvents, response.handledAssets,
response.handledAssets, response.status);
response.status),
response);
} }

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

@ -17,8 +17,8 @@ import {
ClientsPayload, ClientsPayload,
ClientsService, ClientsService,
Resource, Resource,
Version, ResourceLinks,
withLinks Version
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('ClientsService', () => { describe('ClientsService', () => {
@ -154,11 +154,11 @@ describe('ClientsService', () => {
let accessTokenDto: AccessTokenDto; let accessTokenDto: AccessTokenDto;
clientsService.createToken('my-app', new ClientDto('myClientId', 'myClient', 'mySecret', 'Editor')).subscribe(result => { clientsService.createToken('my-app', createClient(13)).subscribe(result => {
accessTokenDto = result; accessTokenDto = result;
}); });
const body = 'grant_type=client_credentials&scope=squidex-api&client_id=my-app:myClientId&client_secret=mySecret'; const body = 'grant_type=client_credentials&scope=squidex-api&client_id=my-app:id13&client_secret=secret13';
const req = httpMock.expectOne('http://service/p/identity-server/connect/token'); const req = httpMock.expectOne('http://service/p/identity-server/connect/token');
@ -190,17 +190,18 @@ describe('ClientsService', () => {
export function createClients(...ids: number[]): ClientsPayload { export function createClients(...ids: number[]): ClientsPayload {
return { return {
items: ids.map(id => items: ids.map(id => createClient(id)),
withLinks(
new ClientDto(`id${id}`, `Client ${id}`, `secret${id}`, `Role${id}`),
{
_links: {
update: { method: 'PUT', href: `/clients/id${id}` }
}
}
)),
_links: { _links: {
create: { method: 'POST', href: '/clients' } create: { method: 'POST', href: '/clients' }
} },
canCreate: true
};
}
export function createClient(id: number) {
const links: ResourceLinks = {
update: { method: 'PUT', href: `/clients/id${id}` }
}; };
return new ClientDto(links, `id${id}`, `Client ${id}`, `secret${id}`, `Role${id}`);
} }

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

@ -13,30 +13,40 @@ import { map, tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
hasAnyLink,
HTTP, HTTP,
mapVersioned, mapVersioned,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
Version, Version,
Versioned, Versioned
withLinks
} from '@app/framework'; } from '@app/framework';
export type ClientsDto = Versioned<ClientsPayload>; export type ClientsDto = Versioned<ClientsPayload>;
export type ClientsPayload = { export type ClientsPayload = {
readonly items: ClientDto[] readonly items: ClientDto[];
readonly canCreate: boolean;
} & Resource; } & Resource;
export class ClientDto { export class ClientDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
public readonly canUpdate: boolean;
public readonly canRevoke: boolean;
constructor( constructor(
links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly name: string, public readonly name: string,
public readonly secret: string, public readonly secret: string,
public readonly role: string public readonly role: string
) { ) {
this._links = links;
this.canUpdate = hasAnyLink(links, 'update');
this.canRevoke = hasAnyLink(links, 'delete');
} }
} }
@ -142,13 +152,13 @@ function parseClients(response: any): ClientsPayload {
const items: any[] = response.items; const items: any[] = response.items;
const clients = items.map(item => const clients = items.map(item =>
withLinks( new ClientDto(item._links,
new ClientDto( item.id,
item.id, item.name || item.id,
item.name || item.id, item.secret,
item.secret, item.role));
item.role),
item)); const _links = response._links;
return withLinks({ items: clients, _links: {} }, response); return { items: clients, _links, canCreate: hasAnyLink(_links, 'create') };
} }

25
src/Squidex/app/shared/services/contents.service.spec.ts

@ -16,6 +16,7 @@ import {
ContentsService, ContentsService,
DateTime, DateTime,
Resource, Resource,
ResourceLinks,
ScheduleDto, ScheduleDto,
Version, Version,
Versioned Versioned
@ -123,7 +124,7 @@ describe('ContentsService', () => {
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(createContent(12)); req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12)); expect(content!).toEqual(createContent(12));
})); }));
@ -144,7 +145,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull(); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(createContent(12)); req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12)); expect(content!).toEqual(createContent(12));
})); }));
@ -192,7 +193,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value); expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush(createContent(12)); req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12)); expect(content!).toEqual(createContent(12));
})); }));
@ -219,7 +220,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('PATCH'); expect(req.request.method).toEqual('PATCH');
expect(req.request.headers.get('If-Match')).toBe(version.value); expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush(createContent(12)); req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12)); expect(content!).toEqual(createContent(12));
})); }));
@ -244,7 +245,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('PUT'); expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value); expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush(createContent(12)); req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12)); expect(content!).toEqual(createContent(12));
})); }));
@ -269,7 +270,7 @@ describe('ContentsService', () => {
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(createContent(12)); req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12)); expect(content!).toEqual(createContent(12));
})); }));
@ -309,7 +310,7 @@ describe('ContentsService', () => {
isPending: true, isPending: true,
data: {}, data: {},
dataDraft: {}, dataDraft: {},
version: id, version: `${id}`,
_links: { _links: {
update: { method: 'PUT', href: `/contents/id${id}` } update: { method: 'PUT', href: `/contents/id${id}` }
} }
@ -318,7 +319,11 @@ describe('ContentsService', () => {
}); });
export function createContent(id: number, suffix = '') { export function createContent(id: number, suffix = '') {
const result = new ContentDto( const links: ResourceLinks = {
update: { method: 'PUT', href: `/contents/id${id}` }
};
return new ContentDto(links,
`id${id}`, `id${id}`,
`Status${id}${suffix}`, `Status${id}${suffix}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
@ -328,8 +333,4 @@ export function createContent(id: number, suffix = '') {
{}, {},
{}, {},
new Version(`${id}`)); new Version(`${id}`));
result._links['update'] = { method: 'PUT', href: `/contents/id${id}` };
return result;
} }

79
src/Squidex/app/shared/services/contents.service.ts

@ -14,6 +14,7 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
hasAnyLink,
HTTP, HTTP,
mapVersioned, mapVersioned,
pretifyError, pretifyError,
@ -21,8 +22,7 @@ import {
ResourceLinks, ResourceLinks,
ResultSet, ResultSet,
Version, Version,
Versioned, Versioned
withLinks
} from '@app/framework'; } from '@app/framework';
export class ScheduleDto { export class ScheduleDto {
@ -35,15 +35,27 @@ export class ScheduleDto {
} }
export class ContentsDto extends ResultSet<ContentDto> { export class ContentsDto extends ResultSet<ContentDto> {
public readonly _links: ResourceLinks = {}; public get canCreate() {
return hasAnyLink(this._links, 'create');
}
public get canCreateAndPublish() {
return hasAnyLink(this._links, 'create/publish');
}
} }
export class ContentDto { export class ContentDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
public readonly statusUpdates: string[] = []; public readonly statusUpdates: string[];
constructor( public readonly canDelete: boolean;
public readonly canDiscardChanges: boolean;
public readonly canProposeChange: boolean;
public readonly canPublishChanges: boolean;
public readonly canUpdate: boolean;
constructor(links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly status: string, public readonly status: string,
public readonly created: DateTime, public readonly created: DateTime,
@ -56,6 +68,9 @@ export class ContentDto {
public readonly dataDraft: object, public readonly dataDraft: object,
public readonly version: Version public readonly version: Version
) { ) {
this._links = links;
this.statusUpdates = Object.keys(links).filter(x => x.startsWith('status/')).map(x => x.substr(7));
} }
} }
@ -103,15 +118,11 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`);
return HTTP.getVersioned(this.http, url).pipe( return this.http.get<{ total: number, items: [] } & Resource>(url).pipe(
map(({ payload }) => { map(({ total, items, _links }) => {
const body = payload.body;
const items: any[] = body.items;
const contents = items.map(x => parseContent(x)); const contents = items.map(x => parseContent(x));
return withLinks(new ContentsDto(body.total, contents), body); return new ContentsDto(total, contents, _links);
}), }),
pretifyError('Failed to load contents. Please reload.')); pretifyError('Failed to load contents. Please reload.'));
} }
@ -223,31 +234,19 @@ export class ContentsService {
} }
function parseContent(response: any) { function parseContent(response: any) {
return nextStatuses(withLinks( return new ContentDto(response._links,
new ContentDto( response.id,
response.id, response.status,
response.status, DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.created), response.createdBy, DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy, response.scheduleJob
response.scheduleJob ? new ScheduleDto(
? new ScheduleDto( response.scheduleJob.status,
response.scheduleJob.status, response.scheduleJob.scheduledBy,
response.scheduleJob.scheduledBy, DateTime.parseISO_UTC(response.scheduleJob.dueTime))
DateTime.parseISO_UTC(response.scheduleJob.dueTime)) : null,
: null, response.isPending === true,
response.isPending === true, response.data,
response.data, response.dataDraft,
response.dataDraft, new Version(response.version));
new Version(response.version)),
response));
}
function nextStatuses(content: ContentDto) {
const nexts = Object.keys(content._links).filter(x => x.startsWith('status/')).map(x => x.substr(7));
for (let next of nexts) {
content.statusUpdates.push(next);
}
return content;
} }

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

@ -16,8 +16,8 @@ import {
ContributorsPayload, ContributorsPayload,
ContributorsService, ContributorsService,
Resource, Resource,
Version, ResourceLinks,
withLinks Version
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('ContributorsService', () => { describe('ContributorsService', () => {
@ -138,21 +138,22 @@ describe('ContributorsService', () => {
export function createContributors(...ids: number[]): ContributorsPayload { export function createContributors(...ids: number[]): ContributorsPayload {
return { return {
items: ids.map(id => items: ids.map(id => createContributor(id)),
withLinks(
new ContributorDto(`id${id}`, id % 2 === 0 ? 'Owner' : 'Developer'),
{
_links: {
update: { method: 'PUT', href: `/contributors/id${id}` }
}
}
)),
maxContributors: ids.length * 13, maxContributors: ids.length * 13,
_links: { _links: {
create: { method: 'POST', href: '/contributors' } create: { method: 'POST', href: '/contributors' }
}, },
_meta: { _meta: {
isInvited: 'true' isInvited: 'true'
} },
canCreate: true
}; };
}
export function createContributor(id: number) {
const links: ResourceLinks = {
update: { method: 'PUT', href: `/contributors/id${id}` }
};
return new ContributorDto(links, `id${id}`, id % 2 === 0 ? 'Owner' : 'Developer');
} }

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

@ -13,29 +13,40 @@ import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
hasAnyLink,
HTTP, HTTP,
mapVersioned, mapVersioned,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
Version, Version,
Versioned, Versioned
withLinks
} from '@app/framework'; } from '@app/framework';
export type ContributorsDto = Versioned<ContributorsPayload>; export type ContributorsDto = Versioned<ContributorsPayload>;
export type ContributorsPayload = { export type ContributorsPayload = {
readonly items: ContributorDto[], readonly items: ContributorDto[];
readonly maxContributors: number
readonly maxContributors: number;
readonly canCreate: boolean;
} & Resource; } & Resource;
export class ContributorDto { export class ContributorDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
public readonly canUpdate: boolean;
public readonly canRevoke: boolean;
constructor( constructor(
links: ResourceLinks,
public readonly contributorId: string, public readonly contributorId: string,
public readonly role: string public readonly role: string
) { ) {
this._links = links;
this.canUpdate = hasAnyLink(links, 'update');
this.canRevoke = hasAnyLink(links, 'delete');
} }
} }
@ -99,11 +110,11 @@ function parseContributors(response: any) {
const items: any[] = response.items; const items: any[] = response.items;
const contributors = items.map(item => const contributors = items.map(item =>
withLinks( new ContributorDto(item._links,
new ContributorDto( item.contributorId,
item.contributorId, item.role));
item.role),
item)); const { maxContributors, _links, _meta }= response;
return withLinks({ items: contributors, maxContributors: response.maxContributors, _links: {} }, response); return { items: contributors, maxContributors, _links, _meta, canCreate: hasAnyLink(_links, 'create') };
} }

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

@ -16,8 +16,8 @@ import {
PatternsPayload, PatternsPayload,
PatternsService, PatternsService,
Resource, Resource,
Version, ResourceLinks,
withLinks Version
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('PatternsService', () => { describe('PatternsService', () => {
@ -168,17 +168,18 @@ describe('PatternsService', () => {
export function createPatterns(...ids: number[]): PatternsPayload { export function createPatterns(...ids: number[]): PatternsPayload {
return { return {
items: ids.map(id => items: ids.map(id => createPattern(id)),
withLinks(
new PatternDto(`id${id}`, `Name${id}`, `Pattern${id}`, `Message${id}`),
{
_links: {
update: { method: 'PUT', href: `/patterns/id${id}` }
}
}
)),
_links: { _links: {
create: { method: 'POST', href: '/patterns' } create: { method: 'POST', href: '/patterns' }
} },
canCreate: true
};
}
export function createPattern(id: number) {
const links: ResourceLinks = {
update: { method: 'PUT', href: `/patterns/id${id}` }
}; };
return new PatternDto(links, `id${id}`, `Name${id}`, `Pattern${id}`, `Message${id}`);
} }

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

@ -13,30 +13,42 @@ import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
hasAnyLink,
HTTP, HTTP,
mapVersioned, mapVersioned,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
Version, Version,
Versioned, Versioned
withLinks
} from '@app/framework'; } from '@app/framework';
export type PatternsDto = Versioned<PatternsPayload>; export type PatternsDto = Versioned<PatternsPayload>;
export type PatternsPayload = { export type PatternsPayload = {
items: PatternDto[] readonly items: PatternDto[],
readonly canCreate: boolean;
} & Resource; } & Resource;
export class PatternDto { export class PatternDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks = {};
public readonly canDelete: boolean;
public readonly canUpdate: boolean;
constructor( constructor(
links: ResourceLinks,
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
) { ) {
this._links = links;
this.canDelete = hasAnyLink(links, 'delete');
this.canUpdate = hasAnyLink(links, 'update');
Object.freeze(this);
} }
} }
@ -113,13 +125,13 @@ function parsePatterns(response: any) {
const items: any[] = response.items; const items: any[] = response.items;
const patterns = items.map(item => const patterns = items.map(item =>
withLinks( new PatternDto(item._links,
new PatternDto( item.id,
item.id, item.name,
item.name, item.pattern,
item.pattern, item.message));
item.message),
item)); const _links = response._links;
return withLinks({ items: patterns, _links: {} }, response); return { items: patterns, _links, canCreate: hasAnyLink(_links, 'create') };
} }

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

@ -12,12 +12,12 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
Resource, Resource,
ResourceLinks,
RoleDto, RoleDto,
RolesDto, RolesDto,
RolesPayload, RolesPayload,
RolesService, RolesService,
Version, Version
withLinks
} from '@app/shared/internal'; } from '@app/shared/internal';
describe('RolesService', () => { describe('RolesService', () => {
@ -188,17 +188,18 @@ describe('RolesService', () => {
export function createRoles(...ids: number[]): RolesPayload { export function createRoles(...ids: number[]): RolesPayload {
return { return {
items: ids.map(id => items: ids.map(id => createRole(id)),
withLinks(
new RoleDto(`name${id}`, id * 2, id * 3, [`permission${id}`], id % 2 === 0),
{
_links: {
update: { method: 'PUT', href: `/roles/id${id}` }
}
}
)),
_links: { _links: {
create: { method: 'POST', href: '/roles' } create: { method: 'POST', href: '/roles' }
} },
canCreate: true
};
}
export function createRole(id: number) {
const links: ResourceLinks = {
update: { method: 'PUT', href: `/roles/id${id}` }
}; };
return new RoleDto(links, `name${id}`, id * 2, id * 3, [`permission${id}`], id % 2 === 0);
} }

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

@ -13,31 +13,41 @@ import { tap } from 'rxjs/operators';
import { import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
hasAnyLink,
HTTP, HTTP,
mapVersioned, mapVersioned,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
Version, Version,
Versioned, Versioned
withLinks
} from '@app/framework'; } from '@app/framework';
export type RolesDto = Versioned<RolesPayload>; export type RolesDto = Versioned<RolesPayload>;
export type RolesPayload = { export type RolesPayload = {
items: RoleDto[] readonly items: RoleDto[];
readonly canCreate: boolean;
} & Resource; } & Resource;
export class RoleDto { export class RoleDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
public readonly canDelete: boolean;
public readonly canUpdate: boolean;
constructor( constructor(
links: ResourceLinks,
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[],
public readonly isDefaultRole: boolean public readonly isDefaultRole: boolean
) { ) {
this._links = links;
this.canDelete = hasAnyLink(links, 'delete');
this.canUpdate = hasAnyLink(links, 'update');
} }
} }
@ -123,14 +133,14 @@ export function parseRoles(response: any) {
const items: any[] = response.items; const items: any[] = response.items;
const roles = items.map(item => const roles = items.map(item =>
withLinks( new RoleDto(item._links,
new RoleDto( item.name,
item.name, item.numClients,
item.numClients, item.numContributors,
item.numContributors, item.permissions,
item.permissions, item.isDefaultRole));
item.isDefaultRole),
item)); const _links = response._links;
return withLinks({ items: roles, _links: {} }, response); return { items: roles, _links, canCreate: hasAnyLink(_links, 'create') };
} }

21
src/Squidex/app/shared/services/rules.service.spec.ts

@ -14,6 +14,7 @@ import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
Resource, Resource,
ResourceLinks,
RuleDto, RuleDto,
RuleElementDto, RuleElementDto,
RuleElementPropertyDto, RuleElementPropertyDto,
@ -126,7 +127,7 @@ describe('RulesService', () => {
}); });
expect(rules!).toEqual( expect(rules!).toEqual(
new RulesDto(2, [ new RulesDto([
createRule(12), createRule(12),
createRule(13) createRule(13)
])); ]));
@ -390,12 +391,14 @@ describe('RulesService', () => {
}); });
export function createRule(id: number, suffix = '') { export function createRule(id: number, suffix = '') {
const result = new RuleDto( const links: ResourceLinks = {
update: { method: 'PUT', href: `/rules/${id}` }
};
return new RuleDto(links,
`id${id}`, `id${id}`,
`creator-${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
`modifier-${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10`),
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10`),
new Version(`${id}`), new Version(`${id}`),
id % 2 === 0, id % 2 === 0,
{ {
@ -410,10 +413,4 @@ export function createRule(id: number, suffix = '') {
actionType: `Webhook${id}${suffix}` actionType: `Webhook${id}${suffix}`
}, },
`Webhook${id}${suffix}`); `Webhook${id}${suffix}`);
result._links['update'] = {
method: 'PUT', href: `/rules/${id}`
};
return result;
} }

62
src/Squidex/app/shared/services/rules.service.ts

@ -14,14 +14,14 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
hasAnyLink,
HTTP, HTTP,
Model, Model,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
ResultSet, ResultSet,
Version, Version
withLinks
} from '@app/framework'; } from '@app/framework';
export const ALL_TRIGGERS = { export const ALL_TRIGGERS = {
@ -76,18 +76,26 @@ export class RuleElementPropertyDto {
} }
export class RulesDto extends ResultSet<RuleDto> { export class RulesDto extends ResultSet<RuleDto> {
public readonly _links: ResourceLinks = {}; constructor(items: RuleDto[], links?: {}) {
super(items.length, items, links);
}
} }
export class RuleDto { export class RuleDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
public readonly canDelete: boolean;
public readonly canDisable: boolean;
public readonly canEnable: boolean;
public readonly canUpdate: boolean;
constructor( constructor(
links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly createdBy: string,
public readonly lastModifiedBy: string,
public readonly created: DateTime, public readonly created: DateTime,
public readonly createdBy: string,
public readonly lastModified: DateTime, public readonly lastModified: DateTime,
public readonly lastModifiedBy: string,
public readonly version: Version, public readonly version: Version,
public readonly isEnabled: boolean, public readonly isEnabled: boolean,
public readonly trigger: any, public readonly trigger: any,
@ -95,15 +103,19 @@ export class RuleDto {
public readonly action: any, public readonly action: any,
public readonly actionType: string public readonly actionType: string
) { ) {
this.canDelete = hasAnyLink(links, 'delete');
this.canDisable = hasAnyLink(links, 'disable');
this.canEnable = hasAnyLink(links, 'enable');
this.canUpdate = hasAnyLink(links, 'update');
} }
} }
export class RuleEventsDto extends ResultSet<RuleEventDto> { export class RuleEventsDto extends ResultSet<RuleEventDto> {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
} }
export class RuleEventDto extends Model<RuleEventDto> { export class RuleEventDto extends Model<RuleEventDto> {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
constructor( constructor(
public readonly id: string, public readonly id: string,
@ -176,13 +188,11 @@ export class RulesService {
public getRules(appName: string): Observable<RulesDto> { public getRules(appName: string): Observable<RulesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);
return HTTP.getVersioned(this.http, url).pipe( return this.http.get<{ items: [] } & Resource>(url).pipe(
map(({ payload }) => { map(({ items, _links }) => {
const items: any[] = payload.body.items;
const rules = items.map(item => parseRule(item)); const rules = items.map(item => parseRule(item));
return withLinks(new RulesDto(rules.length, rules), payload.body); return new RulesDto(rules, _links);
}), }),
pretifyError('Failed to load Rules. Please reload.')); pretifyError('Failed to load Rules. Please reload.'));
} }
@ -311,19 +321,15 @@ export class RulesService {
} }
} }
function parseRule(resource: any) { function parseRule(response: any) {
return withLinks( return new RuleDto(response._links,
new RuleDto( response.id,
resource.id, DateTime.parseISO_UTC(response.created), response.createdBy,
resource.createdBy, DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
resource.lastModifiedBy, new Version(response.version.toString()),
DateTime.parseISO_UTC(resource.created), response.isEnabled,
DateTime.parseISO_UTC(resource.lastModified), response.trigger,
new Version(resource.version.toString()), response.trigger.triggerType,
resource.isEnabled, response.action,
resource.trigger, response.action.actionType);
resource.trigger.triggerType,
resource.action,
resource.action.actionType),
resource);
} }

91
src/Squidex/app/shared/services/schemas.service.spec.ts

@ -15,6 +15,7 @@ import {
DateTime, DateTime,
NestedFieldDto, NestedFieldDto,
Resource, Resource,
ResourceLinks,
RootFieldDto, RootFieldDto,
SchemaDetailsDto, SchemaDetailsDto,
SchemaDto, SchemaDto,
@ -69,15 +70,21 @@ describe('SchemasService', () => {
items: [ items: [
schemaResponse(12), schemaResponse(12),
schemaResponse(13) schemaResponse(13)
] ],
_links: {
create: { method: 'POST', href: '/schemas' }
}
}); });
expect(schemas!).toEqual({ expect(schemas!).toEqual({
canCreate: true,
items: [ items: [
createSchema(12), createSchema(12),
createSchema(13) createSchema(13)
], ],
_links: {} _links: {
create: { method: 'POST', href: '/schemas' }
}
}); });
})); }));
@ -657,7 +664,8 @@ describe('SchemasService', () => {
isDisabled: true, isDisabled: true,
properties: { properties: {
fieldType: 'String' fieldType: 'String'
} },
_links: {}
}, },
{ {
fieldId: 102, fieldId: 102,
@ -667,9 +675,11 @@ describe('SchemasService', () => {
isDisabled: true, isDisabled: true,
properties: { properties: {
fieldType: 'Number' fieldType: 'Number'
} },
_links: {}
} }
] ],
_links: {}
}, },
{ {
fieldId: 12, fieldId: 12,
@ -680,7 +690,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'Assets' fieldType: 'Assets'
} },
_links: {}
}, },
{ {
fieldId: 13, fieldId: 13,
@ -691,7 +702,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'Boolean' fieldType: 'Boolean'
} },
_links: {}
}, },
{ {
fieldId: 14, fieldId: 14,
@ -702,7 +714,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'DateTime' fieldType: 'DateTime'
} },
_links: {}
}, },
{ {
fieldId: 15, fieldId: 15,
@ -713,7 +726,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'Geolocation' fieldType: 'Geolocation'
} },
_links: {}
}, },
{ {
fieldId: 16, fieldId: 16,
@ -724,7 +738,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'Json' fieldType: 'Json'
} },
_links: {}
}, },
{ {
fieldId: 17, fieldId: 17,
@ -735,7 +750,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'Number' fieldType: 'Number'
} },
_links: {}
}, },
{ {
fieldId: 18, fieldId: 18,
@ -746,7 +762,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'References' fieldType: 'References'
} },
_links: {}
}, },
{ {
fieldId: 19, fieldId: 19,
@ -757,7 +774,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'String' fieldType: 'String'
} },
_links: {}
}, },
{ {
fieldId: 20, fieldId: 20,
@ -768,7 +786,8 @@ describe('SchemasService', () => {
partitioning: 'language', partitioning: 'language',
properties: { properties: {
fieldType: 'Tags' fieldType: 'Tags'
} },
_links: {}
} }
], ],
scripts: { scripts: {
@ -786,7 +805,11 @@ describe('SchemasService', () => {
}); });
export function createSchema(id: number, suffix = '') { export function createSchema(id: number, suffix = '') {
const result = new SchemaDto( const links: ResourceLinks = {
update: { method: 'PUT', href: `/schemas/${id}` }
};
return new SchemaDto(links,
`schema-id${id}`, `schema-id${id}`,
`schema-name${id}${suffix}`, `schema-name${id}${suffix}`,
`category${id}${suffix}`, `category${id}${suffix}`,
@ -796,14 +819,14 @@ export function createSchema(id: number, suffix = '') {
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
new Version(`${id}`)); new Version(`${id}`));
result._links['update'] = { method: 'PUT', href: `/schemas/${id}` };
return result;
} }
export function createSchemaDetails(id: number, version: Version, suffix = '') { export function createSchemaDetails(id: number, version: Version, suffix = '') {
const result = new SchemaDetailsDto( const links: ResourceLinks = {
update: { method: 'PUT', href: `/schemas/${id}` }
};
return new SchemaDetailsDto(links,
`schema-id${id}`, `schema-id${id}`,
`schema-name${id}${suffix}`, `schema-name${id}${suffix}`,
`category${id}${suffix}`, `category${id}${suffix}`,
@ -814,19 +837,19 @@ export function createSchemaDetails(id: number, version: Version, suffix = '') {
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`, DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
version, version,
[ [
new RootFieldDto(11, 'field11', createProperties('Array'), 'language', true, true, true, [ new RootFieldDto({}, 11, 'field11', createProperties('Array'), 'language', true, true, true, [
new NestedFieldDto(101, 'field101', createProperties('String'), 11, true, true, true), new NestedFieldDto({}, 101, 'field101', createProperties('String'), 11, true, true, true),
new NestedFieldDto(102, 'field102', createProperties('Number'), 11, true, true, true) new NestedFieldDto({}, 102, 'field102', createProperties('Number'), 11, true, true, true)
]), ]),
new RootFieldDto(12, 'field12', createProperties('Assets'), 'language', true, true, true), new RootFieldDto({}, 12, 'field12', createProperties('Assets'), 'language', true, true, true),
new RootFieldDto(13, 'field13', createProperties('Boolean'), 'language', true, true, true), new RootFieldDto({}, 13, 'field13', createProperties('Boolean'), 'language', true, true, true),
new RootFieldDto(14, 'field14', createProperties('DateTime'), 'language', true, true, true), new RootFieldDto({}, 14, 'field14', createProperties('DateTime'), 'language', true, true, true),
new RootFieldDto(15, 'field15', createProperties('Geolocation'), 'language', true, true, true), new RootFieldDto({}, 15, 'field15', createProperties('Geolocation'), 'language', true, true, true),
new RootFieldDto(16, 'field16', createProperties('Json'), 'language', true, true, true), new RootFieldDto({}, 16, 'field16', createProperties('Json'), 'language', true, true, true),
new RootFieldDto(17, 'field17', createProperties('Number'), 'language', true, true, true), new RootFieldDto({}, 17, 'field17', createProperties('Number'), 'language', true, true, true),
new RootFieldDto(18, 'field18', createProperties('References'), 'language', true, true, true), new RootFieldDto({}, 18, 'field18', createProperties('References'), 'language', true, true, true),
new RootFieldDto(19, 'field19', createProperties('String'), 'language', true, true, true), new RootFieldDto({}, 19, 'field19', createProperties('String'), 'language', true, true, true),
new RootFieldDto(20, 'field20', createProperties('Tags'), 'language', true, true, true) new RootFieldDto({}, 20, 'field20', createProperties('Tags'), 'language', true, true, true)
], ],
{ {
query: '<script-query>', query: '<script-query>',
@ -838,8 +861,4 @@ export function createSchemaDetails(id: number, version: Version, suffix = '') {
{ {
'Default': 'url' 'Default': 'url'
}); });
result._links['update'] = { method: 'PUT', href: `/schemas/${id}` };
return result;
} }

192
src/Squidex/app/shared/services/schemas.service.ts

@ -14,28 +14,41 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
hasAnyLink,
HTTP, HTTP,
pretifyError, pretifyError,
Resource, Resource,
ResourceLinks, ResourceLinks,
StringHelper, StringHelper,
Version, Version,
Versioned, Versioned
withLinks
} from '@app/framework'; } from '@app/framework';
import { createProperties, FieldPropertiesDto } from './schemas.types'; import { createProperties, FieldPropertiesDto } from './schemas.types';
export type SchemasDto = { items: SchemaDto[] } & Resource; export type SchemasDto = {
readonly items: SchemaDto[];
export class SchemaDto { readonly canCreate: boolean;
public readonly _links: ResourceLinks = {}; } & Resource;
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label, this.name);
}
constructor( export class SchemaDto {
public readonly _links: ResourceLinks;
public readonly canAddField: boolean;
public readonly canDelete: boolean;
public readonly canOrderFields: boolean;
public readonly canPublish: boolean;
public readonly canReadContents: boolean;
public readonly canUnpublish: boolean;
public readonly canUpdate: boolean;
public readonly canUpdateCategory: boolean;
public readonly canUpdateScripts: boolean;
public readonly canUpdateUrls: boolean;
public readonly displayName: string;
constructor(links: ResourceLinks,
public readonly id: string, public readonly id: string,
public readonly name: string, public readonly name: string,
public readonly category: string, public readonly category: string,
@ -48,6 +61,20 @@ export class SchemaDto {
public readonly lastModifiedBy: string, public readonly lastModifiedBy: string,
public readonly version: Version public readonly version: Version
) { ) {
this._links = links;
this.canAddField = hasAnyLink(links, 'fields/add');
this.canDelete = hasAnyLink(links, 'delete');
this.canOrderFields = hasAnyLink(links, 'fields/order');
this.canPublish = hasAnyLink(links, 'publish');
this.canReadContents = hasAnyLink(links, 'contents');
this.canUnpublish = hasAnyLink(links, 'unpublish');
this.canUpdate = hasAnyLink(links, 'update');
this.canUpdateCategory = hasAnyLink(links, 'update/category');
this.canUpdateScripts = hasAnyLink(links, 'update/scripts');
this.canUpdateUrls = hasAnyLink(links, 'update/urls');
this.displayName = StringHelper.firstNonEmpty(this.properties.label, this.name);
} }
} }
@ -55,7 +82,7 @@ export class SchemaDetailsDto extends SchemaDto {
public listFields: RootFieldDto[]; public listFields: RootFieldDto[];
public listFieldsEditable: RootFieldDto[]; public listFieldsEditable: RootFieldDto[];
constructor(id: string, name: string, category: string, constructor(links: ResourceLinks, id: string, name: string, category: string,
properties: SchemaPropertiesDto, properties: SchemaPropertiesDto,
isSingleton: boolean, isSingleton: boolean,
isPublished: boolean, isPublished: boolean,
@ -68,7 +95,7 @@ export class SchemaDetailsDto extends SchemaDto {
public readonly scripts = {}, public readonly scripts = {},
public readonly previewUrls = {} public readonly previewUrls = {}
) { ) {
super(id, name, category, properties, isSingleton, isPublished, created, createdBy, lastModified, lastModifiedBy, version); super(links, id, name, category, properties, isSingleton, isPublished, created, createdBy, lastModified, lastModifiedBy, version);
if (fields) { if (fields) {
let listFields = this.fields.filter(x => x.properties.isListField && x.properties.isContentField); let listFields = this.fields.filter(x => x.properties.isListField && x.properties.isContentField);
@ -88,7 +115,17 @@ export class SchemaDetailsDto extends SchemaDto {
} }
export class FieldDto { export class FieldDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
public readonly canAddField: boolean;
public readonly canDelete: boolean;
public readonly canDisable: boolean;
public readonly canEnable: boolean;
public readonly canHide: boolean;
public readonly canLock: boolean;
public readonly canOrderFields: boolean;
public readonly canShow: boolean;
public readonly canUpdate: boolean;
public get isInlineEditable(): boolean { public get isInlineEditable(): boolean {
return !this.isDisabled && this.properties['inlineEditable'] === true; return !this.isDisabled && this.properties['inlineEditable'] === true;
@ -102,7 +139,7 @@ export class FieldDto {
return this.properties.placeholder || ''; return this.properties.placeholder || '';
} }
constructor( constructor(links: ResourceLinks,
public readonly fieldId: number, public readonly fieldId: number,
public readonly name: string, public readonly name: string,
public readonly properties: FieldPropertiesDto, public readonly properties: FieldPropertiesDto,
@ -110,6 +147,17 @@ export class FieldDto {
public readonly isHidden: boolean = false, public readonly isHidden: boolean = false,
public readonly isDisabled: boolean = false public readonly isDisabled: boolean = false
) { ) {
this._links = links;
this.canAddField = hasAnyLink(links, 'fields/add');
this.canDelete = hasAnyLink(links, 'delete');
this.canDisable = hasAnyLink(links, 'disable');
this.canEnable = hasAnyLink(links, 'enable');
this.canOrderFields = hasAnyLink(links, 'fields/order');
this.canHide = hasAnyLink(links, 'hide');
this.canLock = hasAnyLink(links, 'lock');
this.canShow = hasAnyLink(links, 'show');
this.canUpdate = hasAnyLink(links, 'update');
} }
} }
@ -130,28 +178,28 @@ export class RootFieldDto extends FieldDto {
return this.isLocalizable && this.isString && (this.properties.editor === 'Input' || this.properties.editor === 'Textarea'); return this.isLocalizable && this.isString && (this.properties.editor === 'Input' || this.properties.editor === 'Textarea');
} }
constructor(fieldId: number, name: string, properties: FieldPropertiesDto, constructor(links: ResourceLinks, fieldId: number, name: string, properties: FieldPropertiesDto,
public readonly partitioning: string, public readonly partitioning: string,
isLocked: boolean = false, isLocked: boolean = false,
isHidden: boolean = false, isHidden: boolean = false,
isDisabled: boolean = false, isDisabled: boolean = false,
public readonly nested: NestedFieldDto[] = [] public readonly nested: NestedFieldDto[] = []
) { ) {
super(fieldId, name, properties, isLocked, isHidden, isDisabled); super(links, fieldId, name, properties, isLocked, isHidden, isDisabled);
} }
} }
const NONE_FIELD = new RootFieldDto(-1, '', createProperties('String'), 'invariant'); const NONE_FIELD = new RootFieldDto({}, -1, '', createProperties('String'), 'invariant');
const NONE_FIELDS = [NONE_FIELD]; const NONE_FIELDS = [NONE_FIELD];
export class NestedFieldDto extends FieldDto { export class NestedFieldDto extends FieldDto {
constructor(fieldId: number, name: string, properties: FieldPropertiesDto, constructor(links: ResourceLinks, fieldId: number, name: string, properties: FieldPropertiesDto,
public readonly parentId: number, public readonly parentId: number,
isLocked: boolean = false, isLocked: boolean = false,
isHidden: boolean = false, isHidden: boolean = false,
isDisabled: boolean = false isDisabled: boolean = false
) { ) {
super(fieldId, name, properties, isLocked, isHidden, isDisabled); super(links, fieldId, name, properties, isLocked, isHidden, isDisabled);
} }
} }
@ -203,25 +251,7 @@ export class SchemasService {
return HTTP.getVersioned(this.http, url).pipe( return HTTP.getVersioned(this.http, url).pipe(
map(({ payload }) => { map(({ payload }) => {
const body = payload.body; return parseSchemas(payload.body);
const items: any[] = body.items;
const schemas = items.map(item =>
withLinks(
new SchemaDto(
item.id,
item.name,
item.category,
new SchemaPropertiesDto(item.properties.label, item.properties.hints),
item.isSingleton,
item.isPublished,
DateTime.parseISO_UTC(item.created), item.createdBy,
DateTime.parseISO_UTC(item.lastModified), item.lastModifiedBy,
new Version(item.version.toString())),
item));
return withLinks({ items: schemas, _links: {} }, body);
}), }),
pretifyError('Failed to load schemas. Please reload.')); pretifyError('Failed to load schemas. Please reload.'));
} }
@ -487,6 +517,26 @@ export class SchemasService {
} }
} }
function parseSchemas(response: any) {
const items: any[] = response.items;
const schemas = items.map(item =>
new SchemaDto(item._links,
item.id,
item.name,
item.category,
new SchemaPropertiesDto(item.properties.label, item.properties.hints),
item.isSingleton,
item.isPublished,
DateTime.parseISO_UTC(item.created), item.createdBy,
DateTime.parseISO_UTC(item.lastModified), item.lastModifiedBy,
new Version(item.version.toString())));
const _links = response._links;
return { items: schemas, _links, canCreate: hasAnyLink(_links, 'create') };
}
function parseSchemaWithDetails(response: any, version: Version) { function parseSchemaWithDetails(response: any, version: Version) {
const fields = response.fields.map((item: any) => { const fields = response.fields.map((item: any) => {
const propertiesDto = const propertiesDto =
@ -503,47 +553,41 @@ function parseSchemaWithDetails(response: any, version: Version) {
nestedItem.properties.fieldType, nestedItem.properties.fieldType,
nestedItem.properties); nestedItem.properties);
return withLinks( return new NestedFieldDto(nestedItem._links,
new NestedFieldDto( nestedItem.fieldId,
nestedItem.fieldId, nestedItem.name,
nestedItem.name, nestedPropertiesDto,
nestedPropertiesDto, item.fieldId,
item.fieldId, nestedItem.isLocked,
nestedItem.isLocked, nestedItem.isHidden,
nestedItem.isHidden, nestedItem.isDisabled);
nestedItem.isDisabled),
nestedItem);
}); });
} }
return withLinks( return new RootFieldDto(item._links,
new RootFieldDto( item.fieldId,
item.fieldId, item.name,
item.name, propertiesDto,
propertiesDto, item.partitioning,
item.partitioning, item.isLocked,
item.isLocked, item.isHidden,
item.isHidden, item.isDisabled,
item.isDisabled, nested || []);
nested || []),
item);
}); });
const properties = new SchemaPropertiesDto(response.properties.label, response.properties.hints); const properties = new SchemaPropertiesDto(response.properties.label, response.properties.hints);
return withLinks( return new SchemaDetailsDto(response._links,
new SchemaDetailsDto( response.id,
response.id, response.name,
response.name, response.category,
response.category, properties,
properties, response.isSingleton,
response.isSingleton, response.isPublished,
response.isPublished, DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.created), response.createdBy, DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy, version,
version, fields,
fields, response.scripts || {},
response.scripts || {}, response.previewUrls || {});
response.previewUrls || {}),
response);
} }

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

@ -135,9 +135,9 @@ describe('UsersService', () => {
} }
}); });
const expected = new ResourcesDto(); const expected = new ResourcesDto({
schemas: { method: 'GET', href: '/api/schemas' }
expected._links['schemas'] = { method: 'GET', href: '/api/schemas' }; });
expect(resources!).toEqual(expected); expect(resources!).toEqual(expected);
})); }));

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

@ -13,8 +13,7 @@ import { map } from 'rxjs/operators';
import { import {
ApiUrlConfig, ApiUrlConfig,
pretifyError, pretifyError,
ResourceLinks, ResourceLinks
withLinks
} from '@app/framework'; } from '@app/framework';
export class UserDto { export class UserDto {
@ -26,7 +25,11 @@ export class UserDto {
} }
export class ResourcesDto { export class ResourcesDto {
public readonly _links: ResourceLinks = {}; public readonly _links: ResourceLinks;
constructor(links: ResourceLinks) {
this._links = links;
}
} }
@Injectable() @Injectable()
@ -69,9 +72,9 @@ export class UsersService {
public getResources(): Observable<ResourcesDto> { public getResources(): Observable<ResourcesDto> {
const url = this.apiUrl.buildUrl(`api`); const url = this.apiUrl.buildUrl(`api`);
return this.http.get<any>(url).pipe( return this.http.get<{ _links: {} }>(url).pipe(
map(body => { map(({ _links }) => {
return withLinks(new ResourcesDto(), body); return new ResourcesDto(_links);
}), }),
pretifyError('Failed to load user. Please reload.')); pretifyError('Failed to load user. Please reload.'));
} }

10
src/Squidex/app/shared/state/backups.state.spec.ts

@ -26,8 +26,8 @@ describe('BackupsState', () => {
appsState appsState
} = TestValues; } = TestValues;
const backup1 = new BackupDto('id1', DateTime.now(), null, 1, 1, 'Started'); const backup1 = new BackupDto({}, 'id1', DateTime.now(), null, 1, 1, 'Started');
const backup2 = new BackupDto('id2', DateTime.now(), null, 2, 2, 'Started'); const backup2 = new BackupDto({}, 'id2', DateTime.now(), null, 2, 2, 'Started');
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
let backupsService: IMock<BackupsService>; let backupsService: IMock<BackupsService>;
@ -47,7 +47,7 @@ describe('BackupsState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load backups', () => { it('should load backups', () => {
backupsService.setup(x => x.getBackups(app)) backupsService.setup(x => x.getBackups(app))
.returns(() => of(new BackupsDto(2, [backup1, backup2]))).verifiable(); .returns(() => of(new BackupsDto([backup1, backup2]))).verifiable();
backupsState.load().subscribe(); backupsState.load().subscribe();
@ -59,7 +59,7 @@ describe('BackupsState', () => {
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
backupsService.setup(x => x.getBackups(app)) backupsService.setup(x => x.getBackups(app))
.returns(() => of(new BackupsDto(2, [backup1, backup2]))).verifiable(); .returns(() => of(new BackupsDto([backup1, backup2]))).verifiable();
backupsState.load(true, false).subscribe(); backupsState.load(true, false).subscribe();
@ -94,7 +94,7 @@ describe('BackupsState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
backupsService.setup(x => x.getBackups(app)) backupsService.setup(x => x.getBackups(app))
.returns(() => of(new BackupsDto(2, [backup1, backup2]))).verifiable(); .returns(() => of(new BackupsDto([backup1, backup2]))).verifiable();
backupsState.load().subscribe(); backupsState.load().subscribe();
}); });

17
src/Squidex/app/shared/state/backups.state.ts

@ -28,8 +28,11 @@ interface Snapshot {
// Indicates if the backups are loaded. // Indicates if the backups are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// Indicates if the user can create new backups.
canCreate?: boolean;
// The links. // The links.
links: ResourceLinks; _links?: ResourceLinks;
} }
type BackupsList = ImmutableArray<BackupDto>; type BackupsList = ImmutableArray<BackupDto>;
@ -48,8 +51,8 @@ export class BackupsState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
@ -57,7 +60,7 @@ export class BackupsState extends State<Snapshot> {
private readonly backupsService: BackupsService, private readonly backupsService: BackupsService,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ backups: ImmutableArray.empty(), links: {} }); super({ backups: ImmutableArray.empty() });
} }
public load(isReload = false, silent = false): Observable<any> { public load(isReload = false, silent = false): Observable<any> {
@ -66,15 +69,15 @@ export class BackupsState extends State<Snapshot> {
} }
return this.backupsService.getBackups(this.appName).pipe( return this.backupsService.getBackups(this.appName).pipe(
tap(({ items, _links: links }) => { tap(({ items, _links, canCreate }) => {
if (isReload && !silent) { if (isReload && !silent) {
this.dialogs.notifyInfo('Backups reloaded.'); this.dialogs.notifyInfo('Backups reloaded.');
} }
const backups = ImmutableArray.of(items);
this.next(s => { this.next(s => {
const backups = ImmutableArray.of(items);
return { ...s, backups, isLoaded: true, links }; return { ...s, backups, isLoaded: true, _links, canCreate };
}); });
}), }),
shareSubscribed(this.dialogs, { silent })); shareSubscribed(this.dialogs, { silent }));

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

@ -40,8 +40,11 @@ interface Snapshot {
// Indicates if the clients are loaded. // Indicates if the clients are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// Indicates if the user can create new clients.
canCreate?: boolean;
// The links. // The links.
links: ResourceLinks; _links?: ResourceLinks;
} }
type ClientsList = ImmutableArray<ClientDto>; type ClientsList = ImmutableArray<ClientDto>;
@ -56,8 +59,8 @@ export class ClientsState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
@ -65,7 +68,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: Version.EMPTY, links: {} }); super({ clients: ImmutableArray.empty(), version: Version.EMPTY });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
@ -111,8 +114,10 @@ export class ClientsState extends State<Snapshot> {
private replaceClients(payload: ClientsPayload, version: Version) { private replaceClients(payload: ClientsPayload, version: Version) {
const clients = ImmutableArray.of(payload.items); const clients = ImmutableArray.of(payload.items);
const { _links, canCreate } = payload;
this.next(s => { this.next(s => {
return { ...s, clients, isLoaded: true, version, links: payload._links }; return { ...s, clients, isLoaded: true, version, _links, canCreate };
}); });
} }

4
src/Squidex/app/shared/state/contents.forms.spec.ts

@ -370,9 +370,9 @@ describe('StringField', () => {
}); });
function createSchema(properties: SchemaPropertiesDto, index = 1, fields: RootFieldDto[]) { function createSchema(properties: SchemaPropertiesDto, index = 1, fields: RootFieldDto[]) {
return new SchemaDetailsDto('id' + index, 'schema' + index, '', properties, false, true, null!, null!, null!, null!, null!, fields); return new SchemaDetailsDto({}, 'id' + index, 'schema' + index, '', properties, false, true, null!, null!, null!, null!, null!, fields);
} }
function createField(properties: FieldPropertiesDto, index = 1, partitioning = 'languages') { function createField(properties: FieldPropertiesDto, index = 1, partitioning = 'languages') {
return new RootFieldDto(index, 'field' + index, properties, partitioning); return new RootFieldDto({}, index, 'field' + index, properties, partitioning);
} }

27
src/Squidex/app/shared/state/contents.state.ts

@ -47,8 +47,14 @@ interface Snapshot {
// The selected content. // The selected content.
selectedContent?: ContentDto | null; selectedContent?: ContentDto | null;
// Indicates if the user can create a content.
canCreate?: boolean;
// Indicates if the user can create and publish a content.
canCreateAndPublish?: boolean;
// The links. // The links.
links: ResourceLinks; _links?: ResourceLinks;
} }
export const CONTENT_STATUSES = { export const CONTENT_STATUSES = {
@ -90,8 +96,16 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.changes.pipe(map(x => x.status), this.changes.pipe(map(x => x.status),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreateAny =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate || !!x.canCreateAndPublish),
distinctUntilChanged());
public canCreate =
this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged());
public canCreateAndPublish =
this.changes.pipe(map(x => !!x.canCreateAndPublish),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
@ -99,7 +113,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly dialogs: DialogService private readonly dialogs: DialogService
) { ) {
super({ contents: ImmutableArray.of(), contentsPager: new Pager(0), status: 'PublishedDraft', links: {} }); super({ contents: ImmutableArray.of(), contentsPager: new Pager(0), status: 'PublishedDraft' });
} }
public select(id: string | null): Observable<ContentDto | null> { public select(id: string | null): Observable<ContentDto | null> {
@ -144,7 +158,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.snapshot.contentsPager.skip, this.snapshot.contentsPager.skip,
this.snapshot.contentsQuery, undefined, this.snapshot.contentsQuery, undefined,
this.snapshot.status).pipe( this.snapshot.status).pipe(
tap(({ total, items, _links: links }) => { tap(({ total, items, _links, canCreate, canCreateAndPublish }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Contents reloaded.'); this.dialogs.notifyInfo('Contents reloaded.');
} }
@ -158,8 +172,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
if (selectedContent) { if (selectedContent) {
selectedContent = contents.find(x => x.id === selectedContent!.id) || selectedContent; selectedContent = contents.find(x => x.id === selectedContent!.id) || selectedContent;
} }
return { ...s, contents, contentsPager, selectedContent, isLoaded: true, _links, canCreate, canCreateAndPublish };
return { ...s, contents, contentsPager, selectedContent, isLoaded: true, links };
}); });
})); }));
} }

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

@ -46,8 +46,11 @@ interface Snapshot {
// The app version. // The app version.
version: Version; version: Version;
// Indicates if the user can add a contributor.
canCreate?: boolean;
// The links. // The links.
links: ResourceLinks; _links?: ResourceLinks;
} }
type ContributorsList = ImmutableArray<ContributorDto>; type ContributorsList = ImmutableArray<ContributorDto>;
@ -58,10 +61,6 @@ export class ContributorsState extends State<Snapshot> {
this.changes.pipe(map(x => x.contributors), this.changes.pipe(map(x => x.contributors),
distinctUntilChanged()); distinctUntilChanged());
public isMaxReached =
this.changes.pipe(map(x => x.isMaxReached),
distinctUntilChanged());
public isLoaded = public isLoaded =
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
@ -70,8 +69,8 @@ export class ContributorsState extends State<Snapshot> {
this.changes.pipe(map(x => x.maxContributors), this.changes.pipe(map(x => x.maxContributors),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
@ -79,7 +78,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: Version.EMPTY, maxContributors: -1, links: {} }); super({ contributors: ImmutableArray.empty(), version: Version.EMPTY, maxContributors: -1 });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
@ -130,7 +129,9 @@ export class ContributorsState extends State<Snapshot> {
const contributors = ImmutableArray.of(payload.items); const contributors = ImmutableArray.of(payload.items);
return { ...s, contributors, maxContributors, isLoaded, isMaxReached, version: version, links: payload._links }; const { _links, canCreate } = payload;
return { ...s, contributors, maxContributors, isLoaded, isMaxReached, version: version, _links, canCreate };
}); });
} }

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

@ -59,8 +59,11 @@ interface Snapshot {
// Indicates if the languages are loaded. // Indicates if the languages are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// Inedicates if the user can add a language.
canCreate?: boolean;
// The links. // The links.
links: ResourceLinks; _links?: ResourceLinks;
} }
type AppLanguagesList = ImmutableArray<AppLanguageDto>; type AppLanguagesList = ImmutableArray<AppLanguageDto>;
@ -81,8 +84,8 @@ export class LanguagesState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
@ -96,8 +99,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: Version.EMPTY, version: Version.EMPTY
links: {}
}); });
} }
@ -154,14 +156,17 @@ export class LanguagesState extends State<Snapshot> {
const languages = ImmutableArray.of(payload.items); const languages = ImmutableArray.of(payload.items);
const { _links, canCreate } = payload;
return { return {
...s, ...s,
canCreate,
languages: languages.map(x => this.createLanguage(x, languages)), languages: languages.map(x => this.createLanguage(x, languages)),
plainLanguages: payload, plainLanguages: payload,
allLanguages: allLanguages, allLanguages: allLanguages,
allLanguagesNew: allLanguages.filter(x => !languages.find(l => l.iso2Code === x.iso2Code)), allLanguagesNew: allLanguages.filter(x => !languages.find(l => l.iso2Code === x.iso2Code)),
isLoaded: true, isLoaded: true,
links: payload._links, _links,
version: version version: version
}; };
}); });

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

@ -38,6 +38,9 @@ interface Snapshot {
// Indicates if the patterns are loaded. // Indicates if the patterns are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// Indicates if patterns can be created.
canCreate?: boolean;
// The links. // The links.
links: ResourceLinks; links: ResourceLinks;
} }
@ -54,8 +57,8 @@ export class PatternsState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
@ -109,8 +112,10 @@ export class PatternsState extends State<Snapshot> {
private replacePatterns(payload: PatternsPayload, version: Version) { private replacePatterns(payload: PatternsPayload, version: Version) {
const patterns = ImmutableArray.of(payload.items); const patterns = ImmutableArray.of(payload.items);
const { _links: links, canCreate } = payload;
this.next(s => { this.next(s => {
return { ...s, patterns, isLoaded: true, version, links: payload._links }; return { ...s, patterns, isLoaded: true, version, links, canCreate };
}); });
} }

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

@ -38,8 +38,11 @@ interface Snapshot {
// Indicates if the roles are loaded. // Indicates if the roles are loaded.
isLoaded?: boolean; isLoaded?: boolean;
// Indicates if the user can add a role.
canCreate?: boolean;
// The links. // The links.
links: ResourceLinks; _links?: ResourceLinks;
} }
type RolesList = ImmutableArray<RoleDto>; type RolesList = ImmutableArray<RoleDto>;
@ -54,8 +57,8 @@ export class RolesState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
@ -63,7 +66,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: Version.EMPTY, links: {} }); super({ roles: ImmutableArray.empty(), version: Version.EMPTY });
} }
public load(isReload = false): Observable<any> { public load(isReload = false): Observable<any> {
@ -109,8 +112,10 @@ export class RolesState extends State<Snapshot> {
private replaceRoles(payload: RolesPayload, version: Version) { private replaceRoles(payload: RolesPayload, version: Version) {
const roles = ImmutableArray.of(payload.items); const roles = ImmutableArray.of(payload.items);
const { _links, canCreate } = payload;
this.next(s => { this.next(s => {
return { ...s, roles, isLoaded: true, version, links: payload._links }; return { ...s, roles, isLoaded: true, version, _links, canCreate };
}); });
} }

6
src/Squidex/app/shared/state/rules.state.spec.ts

@ -52,7 +52,7 @@ describe('RulesState', () => {
describe('Loading', () => { describe('Loading', () => {
it('should load rules', () => { it('should load rules', () => {
rulesService.setup(x => x.getRules(app)) rulesService.setup(x => x.getRules(app))
.returns(() => of(new RulesDto(2, [rule1, rule2]))).verifiable(); .returns(() => of(new RulesDto([rule1, rule2]))).verifiable();
rulesState.load().subscribe(); rulesState.load().subscribe();
@ -64,7 +64,7 @@ describe('RulesState', () => {
it('should show notification on load when reload is true', () => { it('should show notification on load when reload is true', () => {
rulesService.setup(x => x.getRules(app)) rulesService.setup(x => x.getRules(app))
.returns(() => of(new RulesDto(2, [rule1, rule2]))).verifiable(); .returns(() => of(new RulesDto([rule1, rule2]))).verifiable();
rulesState.load(true).subscribe(); rulesState.load(true).subscribe();
@ -78,7 +78,7 @@ describe('RulesState', () => {
describe('Updates', () => { describe('Updates', () => {
beforeEach(() => { beforeEach(() => {
rulesService.setup(x => x.getRules(app)) rulesService.setup(x => x.getRules(app))
.returns(() => of(new RulesDto(2, [rule1, rule2]))).verifiable(); .returns(() => of(new RulesDto([rule1, rule2]))).verifiable();
rulesState.load().subscribe(); rulesState.load().subscribe();
}); });

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

@ -48,7 +48,7 @@ export class RulesState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => x.links),
distinctUntilChanged()); distinctUntilChanged());

1
src/Squidex/app/shared/state/schemas.state.spec.ts

@ -35,6 +35,7 @@ describe('SchemasState', () => {
const schema2 = createSchema(2); const schema2 = createSchema(2);
const oldSchemas = { const oldSchemas = {
canCreate: true,
items: [ items: [
schema1, schema1,
schema2 schema2

17
src/Squidex/app/shared/state/schemas.state.ts

@ -49,8 +49,11 @@ interface Snapshot {
// The selected schema. // The selected schema.
selectedSchema?: SchemaDetailsDto | null; selectedSchema?: SchemaDetailsDto | null;
// Indicates if the user can create a schema.
canCreate?: boolean;
// The links. // The links.
links: ResourceLinks; _links?: ResourceLinks;
} }
export type SchemasList = ImmutableArray<SchemaDto>; export type SchemasList = ImmutableArray<SchemaDto>;
@ -85,8 +88,8 @@ export class SchemasState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded), this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged()); distinctUntilChanged());
public links = public canCreate =
this.changes.pipe(map(x => x.links), this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged()); distinctUntilChanged());
constructor( constructor(
@ -94,7 +97,7 @@ export class SchemasState extends State<Snapshot> {
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly schemasService: SchemasService private readonly schemasService: SchemasService
) { ) {
super({ schemas: ImmutableArray.empty(), categories: buildCategories({}), links: {} }); super({ schemas: ImmutableArray.empty(), categories: buildCategories({}) });
} }
public select(idOrName: string | null): Observable<SchemaDetailsDto | null> { public select(idOrName: string | null): Observable<SchemaDetailsDto | null> {
@ -128,11 +131,13 @@ export class SchemasState extends State<Snapshot> {
} }
return this.next(s => { return this.next(s => {
const schemas = ImmutableArray.of(payload.items).sortByStringAsc(x => x.displayName); const { _links, canCreate, items } = payload;
const schemas = ImmutableArray.of(items).sortByStringAsc(x => x.displayName);
const categories = buildCategories(s.categories, schemas); const categories = buildCategories(s.categories, schemas);
return { ...s, schemas, isLoaded: true, categories, links: payload._links }; return { ...s, schemas, isLoaded: true, categories, _links, canCreate };
}); });
}), }),
shareSubscribed(this.dialogs)); shareSubscribed(this.dialogs));

13
src/Squidex/app/shared/state/ui.state.spec.ts

@ -13,8 +13,7 @@ import {
ResourcesDto, ResourcesDto,
UIService, UIService,
UIState, UIState,
UsersService, UsersService
withLinks
} from '@app/shared/internal'; } from '@app/shared/internal';
import { TestValues } from './_test-helpers'; import { TestValues } from './_test-helpers';
@ -38,7 +37,9 @@ describe('UIState', () => {
}; };
const resources: ResourceLinks = { const resources: ResourceLinks = {
schemas: { method: 'GET', href: '/api/schemas' } ['admin/events']: { method: 'GET', href: '/api/events' },
['admin/restore']: { method: 'GET', href: '/api/restore' },
['admin/users']: { method: 'GET', href: '/api/users' },
}; };
let usersService: IMock<UsersService>; let usersService: IMock<UsersService>;
@ -63,7 +64,7 @@ describe('UIState', () => {
usersService = Mock.ofType<UsersService>(); usersService = Mock.ofType<UsersService>();
usersService.setup(x => x.getResources()) usersService.setup(x => x.getResources())
.returns(() => of(withLinks(new ResourcesDto(), { _links: resources }))); .returns(() => of(new ResourcesDto(resources)));
uiState = new UIState(appsState.object, uiService.object, usersService.object); uiState = new UIState(appsState.object, uiService.object, usersService.object);
}); });
@ -76,7 +77,9 @@ describe('UIState', () => {
canCreateApps: true canCreateApps: true
}); });
expect(uiState.snapshot.resources).toEqual(resources); expect(uiState.snapshot.canReadEvents).toBeTruthy();
expect(uiState.snapshot.canReadUsers).toBeTruthy();
expect(uiState.snapshot.canRestore).toBeTruthy();
}); });
it('should add value to snapshot when set', () => { it('should add value to snapshot when set', () => {

38
src/Squidex/app/shared/state/ui.state.ts

@ -9,7 +9,7 @@ import { Injectable } from '@angular/core';
import { distinctUntilChanged, map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { import {
ResourceLinks, hasAnyLink,
State, State,
Types Types
} from '@app/framework'; } from '@app/framework';
@ -30,7 +30,17 @@ interface Snapshot {
// The merged settings of app and common settings. // The merged settings of app and common settings.
settings: object & any; settings: object & any;
resources: ResourceLinks; // Indicates if the user can read events.
canReadEvents?: boolean;
// Indicates if the user can read users.
canReadUsers?: boolean;
// Indicates if the user can restore backups.
canRestore?: boolean;
// Indicates if the user can use at least one admin resource.
canUserAdminResource?: boolean;
} }
@Injectable() @Injectable()
@ -39,8 +49,20 @@ export class UIState extends State<Snapshot> {
this.changes.pipe(map(x => x.settings), this.changes.pipe(map(x => x.settings),
distinctUntilChanged()); distinctUntilChanged());
public resources = public canReadEvents =
this.changes.pipe(map(x => x.resources), this.changes.pipe(map(x => !!x.canReadEvents),
distinctUntilChanged());
public canReadUsers =
this.changes.pipe(map(x => !!x.canReadUsers),
distinctUntilChanged());
public canRestore =
this.changes.pipe(map(x => !!x.canRestore),
distinctUntilChanged());
public canUserAdminResource =
this.changes.pipe(map(x => !!x.canRestore || !!x.canReadUsers || !!x.canReadEvents),
distinctUntilChanged()); distinctUntilChanged());
public get<T>(path: string, defaultValue: T) { public get<T>(path: string, defaultValue: T) {
@ -53,7 +75,7 @@ export class UIState extends State<Snapshot> {
private readonly uiService: UIService, private readonly uiService: UIService,
private readonly usersService: UsersService private readonly usersService: UsersService
) { ) {
super({ settings: {}, settingsCommon: {}, settingsApp: {}, resources: {} }); super({ settings: {}, settingsCommon: {}, settingsApp: {} });
this.loadResources(); this.loadResources();
this.loadCommon(); this.loadCommon();
@ -82,7 +104,11 @@ export class UIState extends State<Snapshot> {
private loadResources() { private loadResources() {
this.usersService.getResources() this.usersService.getResources()
.subscribe(payload => { .subscribe(payload => {
this.next(s => ({ ...s, resources: payload._links })); this.next(s => ({ ...s,
canReadEvents: hasAnyLink(payload, 'admin/events'),
canReadUsers: hasAnyLink(payload, 'admin/users'),
canRestore: hasAnyLink(payload, 'admin/restore')
}));
}); });
} }

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

@ -1,5 +1,5 @@
<ul class="nav flex-column" *ngIf="appsState.selectedValidApp | async; let selectedApp"> <ul class="nav flex-column" *ngIf="appsState.selectedValidApp | async; let selectedApp">
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'schemas'"> <li class="nav-item" *ngIf="selectedApp.canReadSchemas">
<a class="nav-link" routerLink="schemas" routerLinkActive="active"> <a class="nav-link" routerLink="schemas" routerLinkActive="active">
<i class="nav-icon icon-schemas"></i> <div class="nav-text">Schemas</div> <i class="nav-icon icon-schemas"></i> <div class="nav-text">Schemas</div>
</a> </a>
@ -9,12 +9,12 @@
<i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div> <i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'assets'"> <li class="nav-item" *ngIf="selectedApp.canReadAssets">
<a class="nav-link" routerLink="assets/filters" routerLinkActive="active"> <a class="nav-link" routerLink="assets/filters" routerLinkActive="active">
<i class="nav-icon icon-assets"></i> <div class="nav-text">Assets</div> <i class="nav-icon icon-assets"></i> <div class="nav-text">Assets</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'rules'"> <li class="nav-item" *ngIf="selectedApp.canReadRules">
<a class="nav-link" routerLink="rules" routerLinkActive="active"> <a class="nav-link" routerLink="rules" routerLinkActive="active">
<i class="nav-icon icon-rules"></i> <div class="nav-text">Rules</div> <i class="nav-icon icon-rules"></i> <div class="nav-text">Rules</div>
</a> </a>

8
src/Squidex/app/shell/pages/internal/profile-menu.component.html

@ -17,11 +17,9 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<ng-container *ngIf="uiState.resources | async; let resources"> <a class="dropdown-item" routerLink="/app/administration" *ngIf="uiState.canUserAdminResource | async">
<a class="dropdown-item" routerLink="/app/administration" *ngIf="resources | sqxHasLink:'admin/events':'admin/restore':'admin/users'"> Administration
Administration </a>
</a>
</ng-container>
<a class="dropdown-item" [sqxPopupLink]="snapshot.profileUrl"> <a class="dropdown-item" [sqxPopupLink]="snapshot.profileUrl">
Profile Profile

Loading…
Cancel
Save