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));
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));
}

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

@ -1,18 +1,18 @@
<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">
<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">
<i class="nav-icon icon-time"></i> <div class="nav-text">Consumers</div>
</a>
</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">
<i class="nav-icon icon-user-o"></i> <div class="nav-text">Users</div>
</a>
</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">
<i class="nav-icon icon-backup"></i> <div class="nav-text">Restore</div>
</a>

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

@ -42,13 +42,13 @@
<span>{{eventConsumer.position}}</span>
</td>
<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>
</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>
</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>
</button>
</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 { ActivatedRoute, Router } from '@angular/router';
import { hasAnyLink, ResourceOwner } from '@app/shared';
import { ResourceOwner } from '@app/shared';
import {
CreateUserDto,
@ -47,7 +47,7 @@ export class UserPageComponent extends ResourceOwner implements OnInit {
if (selectedUser) {
this.userForm.load(selectedUser);
this.isEditable = hasAnyLink(this.user, 'update');
this.isEditable = this.user.canUpdate;
if (!this.isEditable) {
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" />
</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>
<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>
</td>
<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>
</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>
</button>
</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 { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource } from '@app/framework';
import { ApiUrlConfig, Resource, ResourceLinks } from '@app/framework';
import {
EventConsumerDto,
@ -151,16 +151,14 @@ describe('EventConsumersService', () => {
});
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}`,
true,
true,
`failure-${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 {
ApiUrlConfig,
hasAnyLink,
pretifyError,
Resource,
ResourceLinks,
withLinks
ResourceLinks
} from '@app/shared';
export class EventConsumersDto {
public readonly _links: ResourceLinks = {};
public readonly _links: ResourceLinks;
constructor(
public readonly items: EventConsumerDto[]
public readonly items: EventConsumerDto[], links?: ResourceLinks
) {
this._links = links || {};
}
}
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 isStopped?: boolean,
public readonly isResetting?: boolean,
public readonly error?: 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');
return this.http.get<{ items: any[] } & Resource>(url).pipe(
map(body => {
const eventConsumers = body.items.map(item => parseEventConsumer(item));
map(({ items, _links }) => {
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.'));
}
@ -98,12 +108,11 @@ export class EventConsumersService {
}
function parseEventConsumer(response: any): EventConsumerDto {
return withLinks(
new EventConsumerDto(
response.name,
response.isStopped,
response.isResetting,
response.error,
response.position),
response);
return new EventConsumerDto(
response._links,
response.name,
response.isStopped,
response.isResetting,
response.error,
response.position);
}

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

@ -8,7 +8,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource } from '@app/framework';
import { ApiUrlConfig, Resource, ResourceLinks } from '@app/framework';
import {
UserDto,
@ -227,7 +227,11 @@ describe('UsersService', () => {
});
export function createUser(id: number, suffix = '') {
const result = new UserDto(
const links: ResourceLinks = {
update: { method: 'PUT', href: `/users/${id}` }
};
return new UserDto(links,
`${id}`,
`user${id}${suffix}@domain.com`,
`user${id}${suffix}`,
@ -235,10 +239,4 @@ export function createUser(id: number, suffix = '') {
`Permission${id}${suffix}`
],
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 {
ApiUrlConfig,
hasAnyLink,
pretifyError,
Resource,
ResourceLinks,
ResultSet,
withLinks
ResultSet
} from '@app/shared';
export class UsersDto extends ResultSet<UserDto> {
public readonly _links: ResourceLinks = {};
public get canCreate() {
return hasAnyLink(this._links, 'create');
}
}
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 email: string,
public readonly displayName: string,
public readonly permissions: string[] = [],
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 || ''}`);
return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe(
map(body => {
const users = body.items.map(item => parseUser(item));
map(({ total, items, _links }) => {
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.'));
}
@ -128,12 +139,11 @@ export class UsersService {
}
function parseUser(response: any) {
return withLinks(
new UserDto(
response.id,
response.email,
response.displayName,
response.permissions,
response.isLocked),
response);
return new UserDto(
response._links,
response.id,
response.email,
response.displayName,
response.permissions,
response.isLocked);
}

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

@ -16,7 +16,6 @@ import {
UsersService
} from '@app/features/administration/internal';
import { UsersState } from './users.state';
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.
usersQuery?: string;
// The resource links.
links: ResourceLinks;
// Indicates if the users are loaded.
isLoaded?: boolean;
// The selected user.
selectedUser?: UserDto | null;
// Indicates if the user can create a user.
canCreate?: boolean;
// The resource links.
_links?: ResourceLinks;
}
export type UsersList = ImmutableArray<UserDto>;
@ -68,15 +71,15 @@ export class UsersState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
public links =
this.changes.pipe(map(x => x.links),
public canCreate =
this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged());
constructor(
private readonly dialogs: DialogService,
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> {
@ -116,7 +119,7 @@ export class UsersState extends State<Snapshot> {
this.snapshot.usersPager.pageSize,
this.snapshot.usersPager.skip,
this.snapshot.usersQuery).pipe(
tap(({ total, items, _links: links }) => {
tap(({ total, items, _links, canCreate }) => {
if (isReload) {
this.dialogs.notifyInfo('Users reloaded.');
}
@ -131,7 +134,7 @@ export class UsersState extends State<Snapshot> {
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));

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

@ -17,11 +17,11 @@
<ng-container menu>
<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
</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
</button>
@ -45,13 +45,13 @@
<ng-container *ngIf="content.isPending || !schema.isSingleton">
<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
</a>
<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
</a>
@ -63,7 +63,7 @@
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete"
[class.disabled]="content | sqxHasNoLink:'delete'"
[class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
@ -74,13 +74,12 @@
</ng-container>
</div>
<ng-container *ngIf="content | sqxHasLink:'update'">
<button type="button" class="btn btn-secondary ml-1" (click)="saveAsProposal()" *ngIf="content | sqxHasLink:'update/change'">
Save as Proposal
</button>
<button type="button" class="btn btn-secondary ml-1" (click)="saveAsProposal()" *ngIf="content.canProposeChange">
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
</button>

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

@ -24,7 +24,6 @@ import {
EditContentForm,
fadeAnimation,
FieldDto,
hasAnyLink,
ImmutableArray,
LanguagesState,
MessageBus,
@ -175,7 +174,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
}
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() {

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">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.mutableValues"></sqx-language-selector>
</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)">
<i class="icon-plus"></i> New
</button>

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

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

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

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

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

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

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

@ -12,7 +12,7 @@
</div>
<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-image">
<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 {
Form,
hasAnyLink,
ImmutableArray,
RuleDto,
RuleElementDto,
@ -64,7 +63,7 @@ export class RuleWizardComponent implements AfterViewInit, OnInit {
}
public ngOnInit() {
this.isEditable = !this.rule || hasAnyLink(this.rule, 'update');
this.isEditable = !this.rule || this.rule.canUpdate;
if (this.mode === MODE_EDIT_ACTION) {
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>
<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>
<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">
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
</button>
</div>
@ -51,11 +51,11 @@
</span>
</td>
<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 class="cell-actions">
<button type="button" class="btn btn-text-danger"
[disabled]="rule | sqxHasNoLink:'delete'"
[disabled]="!rule.canDelete"
(sqxConfirmClick)="delete(rule)"
confirmTitle="Delete rule"
confirmText="Do you really want to delete the rule?">
@ -82,7 +82,7 @@
<ng-container sidebar>
<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">
<i class="icon-time"></i>
</a>

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

@ -30,21 +30,21 @@
</button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<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
</a>
<a class="dropdown-item" (click)="disableField()" *ngIf="field | sqxHasLink:'disable'">
<a class="dropdown-item" (click)="disableField()" *ngIf="field.canDisable">
Disable in UI
</a>
<a class="dropdown-item" (click)="hideField()" *ngIf="field | sqxHasLink:'hide'">
<a class="dropdown-item" (click)="hideField()" *ngIf="field.canHide">
Hide in API
</a>
<a class="dropdown-item" (click)="showField()" *ngIf="field | sqxHasLink:'show'">
<a class="dropdown-item" (click)="showField()" *ngIf="field.canShow">
Show in API
</a>
</ng-container>
<ng-container *ngIf="field | sqxHasLink:'lock'">
<ng-container *ngIf="field.canLock">
<div class="dropdown-divider"></div>
<a class="dropdown-item"
@ -59,7 +59,7 @@
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete"
[class.disabled]="field | sqxHasNoLink:'delete'"
[class.disabled]="!field.canDelete"
(sqxConfirmClick)="deleteField()"
confirmTitle="Delete 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,
EditFieldForm,
fadeAnimation,
hasAnyLink,
ImmutableArray,
ModalModel,
NestedFieldDto,
@ -61,7 +60,7 @@ export class FieldComponent implements OnChanges {
public ngOnChanges(changes: SimpleChanges) {
if (changes['field']) {
this.isEditable = hasAnyLink(this.field, 'update');
this.isEditable = this.field.canUpdate;
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 {
EditSchemaForm,
hasAnyLink,
SchemaDetailsDto,
SchemasState
} from '@app/shared';
@ -38,7 +37,7 @@ export class SchemaEditFormComponent implements OnInit {
}
public ngOnInit() {
this.isEditable = hasAnyLink(this.schema, 'update');
this.isEditable = this.schema.canUpdate;
this.editForm.load(this.schema.properties);

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

@ -11,10 +11,10 @@
</button>
<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
</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
</button>
</div>
@ -31,7 +31,7 @@
Edit Preview Urls
</a>
<ng-container *ngIf="schemasState.links | async | sqxHasLink:'create'">
<ng-container *ngIf="schemasState.canCreate">
<div class="dropdown-divider"></div>
<a class="dropdown-item" (click)="cloneSchema()">
@ -39,10 +39,11 @@
</a>
</ng-container>
<ng-container *ngIf="schema | sqxHasLink:'delete'">
<ng-container>
<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()"
confirmTitle="Delete 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">
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
</button>
</div>
<ng-container *ngIf="patternsState.patterns | async; let patterns">
<div class="schemas"
[sqxSortDisabled]="schema | sqxHasNoLink:'update'"
[sqxSortDisabled]="!schema.canOrderFields"
[sqxSortModel]="schema.fields"
(sqxSort)="sortFields($event)">
<div *ngFor="let field of schema.fields; trackBy: trackByField.bind(this)">
@ -80,7 +81,7 @@
</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>
</button>
</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 {
AddPreviewUrlForm,
ConfigurePreviewUrlsForm,
hasAnyLink,
SchemaDetailsDto,
SchemasState
} from '@app/shared';
@ -41,7 +40,7 @@ export class SchemaPreviewUrlsFormComponent implements OnInit {
}
public ngOnInit() {
this.isEditable = hasAnyLink(this.schema, 'update');
this.isEditable = this.schema.canUpdateUrls;
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 {
EditScriptsForm,
hasAnyLink,
SchemaDetailsDto,
SchemasState
} from '@app/shared';
@ -40,7 +39,7 @@ export class SchemaScriptsFormComponent implements OnInit {
}
public ngOnInit() {
this.isEditable = hasAnyLink(this.schema, 'update');
this.isEditable = this.schema.canUpdateScripts;
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>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<ng-container>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="addSchemaDialog.show()" [disabled]="schemasState.links | sqxHasNoLink:'create'"></sqx-shortcut>
<ng-container *ngIf="schemasState.canCreate | async">
<sqx-shortcut keys="ctrl+shift+g" (trigger)="addSchemaDialog.show()"></sqx-shortcut>
<button type="button" class="btn btn-success subheader-button" (click)="createSchema()"
[disabled]="schemasState.links | async | sqxHasNoLink:'create'"
title="New Schema (CTRL + SHIFT + G)"
titlePosition="top-left">
<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>
<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
</button>
</ng-container>
@ -79,7 +79,7 @@
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-danger mt-1"
[disabled]="backup | sqxHasNoLink:'delete'"
[disabled]="!backup.canDelete"
(sqxConfirmClick)="delete(backup)"
confirmTitle="Delete 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}}
</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>
</div>
<div class="col-auto">
@ -29,7 +29,7 @@
</div>
<div class="col-auto cell-actions no-padding">
<button type="button" class="btn btn-text-danger"
[disabled]="client | sqxHasNoLink:'delete'"
[disabled]="!client.canRevoke"
(sqxConfirmClick)="revoke()"
confirmTitle="Revoke client"
confirmText="Do you really want to revoke the client?">
@ -72,7 +72,7 @@
</label>
<div class="col cell-input">
<select class="form-control"
[disabled]="!isEditable"
[disabled]="!client.canUpdate"
[ngModel]="client.role"
(ngModelChange)="update($event)">
<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,
DialogModel,
DialogService,
hasAnyLink,
RenameClientForm,
RoleDto
} from '@app/shared';
@ -37,7 +36,6 @@ export class ClientComponent implements OnChanges {
public clientRoles: RoleDto[];
public isRenaming = false;
public isEditable = false;
public connectToken: AccessTokenDto;
public connectDialog = new DialogModel();
@ -61,8 +59,6 @@ export class ClientComponent implements OnChanges {
}
public ngOnChanges() {
this.isEditable = hasAnyLink(this.client, 'update');
this.renameForm.load(this.client);
const app = this.appsState.appName;
@ -83,7 +79,7 @@ export class ClientComponent implements OnChanges {
}
public toggleRename() {
if (!this.isEditable) {
if (!this.client.canUpdate) {
return;
}
@ -91,7 +87,7 @@ export class ClientComponent implements OnChanges {
}
public rename() {
if (!this.isEditable) {
if (!this.client.canUpdate) {
return;
}

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

@ -26,7 +26,7 @@
</sqx-client>
</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()">
<div class="row no-gutters">
<div class="col">

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

@ -35,12 +35,12 @@
<select class="form-control"
[ngModel]="contributor.role"
(ngModelChange)="changeRole(contributor, $event)"
[disabled]="contributor | sqxHasNoLink:'update'">
[disabled]="!contributor.canUpdate">
<option *ngFor="let role of roles" [ngValue]="role.name">{{role.name}}</option>
</select>
</td>
<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>
</button>
</td>
@ -50,7 +50,7 @@
</table>
<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()">
<div class="row no-gutters">
<div class="col">

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

@ -14,7 +14,7 @@
</button>
<button type="button" class="btn btn-text-danger"
[disabled]="language | sqxHasNoLink:'delete'"
[disabled]="!language.canDelete"
(sqxConfirmClick)="remove()"
confirmTitle="Remove 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,
EditLanguageForm,
fadeAnimation,
hasAnyLink,
ImmutableArray,
LanguagesState
} from '@app/shared';
@ -96,7 +95,7 @@ export class LanguageComponent implements OnChanges {
}
private resetForm() {
this.isEditable = hasAnyLink(this.language, 'update');
this.isEditable = this.language.canUpdate;
this.editForm.load(this.language);

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

@ -22,7 +22,7 @@
</sqx-language>
<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()">
<div class="row no-gutters">
<div class="col">

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

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

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

@ -23,7 +23,7 @@
[pattern]="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>

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

@ -17,7 +17,7 @@
</button>
<button type="button" class="btn btn-text-danger"
[disabled]="role | sqxHasNoLink:'delete'"
[disabled]="!role.canDelete"
(sqxConfirmClick)="remove()"
confirmTitle="Delete 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,
EditPermissionsForm,
fadeAnimation,
hasAnyLink,
RoleDto,
RolesState
} from '@app/shared';
@ -51,7 +50,7 @@ export class RoleComponent implements OnChanges {
}
public ngOnChanges() {
this.isEditable = hasAnyLink(this.role, 'update');
this.isEditable = this.role.canUpdate;
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">
<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()">
<div class="row no-gutters">
<div class="col">

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

@ -7,49 +7,49 @@
<ng-container content>
<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">
Backups
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'clients'">
<li class="nav-item" *ngIf="selectedApp.canReadClients">
<a class="nav-link" routerLink="clients" routerLinkActive="active">
Clients
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'contributors'">
<li class="nav-item" *ngIf="selectedApp.canReadContributors">
<a class="nav-link" routerLink="contributors" routerLinkActive="active">
Contributors
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'roles'">
<li class="nav-item" *ngIf="selectedApp.canReadRoles">
<a class="nav-link" routerLink="roles" routerLinkActive="active">
Roles
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'languages'">
<li class="nav-item" *ngIf="selectedApp.canReadLanguages">
<a class="nav-link" routerLink="languages" routerLinkActive="active">
Languages
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'patterns'">
<li class="nav-item" *ngIf="selectedApp.canReadPatterns">
<a class="nav-link" routerLink="patterns" routerLinkActive="active">
Patterns
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'plans'">
<li class="nav-item" *ngIf="selectedApp.canReadPlans">
<a class="nav-link" routerLink="plans" routerLinkActive="active">
Subscription
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'delete'">
<li class="nav-item" *ngIf="selectedApp.canDelete">
<a class="nav-link" routerLink="more" routerLinkActive="active">
More
<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/loading.interceptor';
export * from './angular/http/hateos.pipes';
export * from './angular/http/http-extensions';
export * from './angular/modals/dialog-renderer.component';

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

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

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

@ -14,6 +14,7 @@ import { ErrorDto } from './utils/error';
import { Types } from './utils/types';
import { fullValue } from './angular/forms/forms-helper';
import { ResourceLinks } from '@app/shared';
export interface FormState {
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> {
public with(value: Partial<T>, validOnly = false): T {
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(
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 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 {
const link = getLink(value, rel);

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

@ -13,7 +13,6 @@ import {
AppsState,
AssetDto,
AssetsService,
hasAnyLink,
StatefulComponent
} from '@app/shared/internal';
@ -55,7 +54,7 @@ export class AssetDialogComponent extends StatefulComponent implements OnInit {
public ngOnInit() {
this.annotateForm.load(this.asset);
this.isReadOnly = !hasAnyLink(this.asset, 'update');
this.isReadOnly = !this.asset.canUpdate;
if (this.isReadOnly) {
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">
<i class="icon-download"></i>
</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>
</a>
<a class="file-delete ml-2" (click)="emitRemove()" *ngIf="removeMode">
@ -103,7 +103,7 @@
</a>
</td>
<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>
</button>
<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,
DialogService,
fadeAnimation,
hasAnyLink,
StatefulComponent,
Types,
UploadCanceled
@ -113,7 +112,7 @@ export class AssetComponent extends StatefulComponent<State> implements OnInit {
}
public updateFile(files: FileList) {
if (files.length === 1 && hasAnyLink(this.asset, 'upload')) {
if (files.length === 1 && this.asset.canUpload) {
this.setProgress(1);
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="header clearfix">
@ -8,14 +8,14 @@
<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>
</button>
</div>
<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">
<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">
<div class="row" *ngIf="!forContent; else simpleMode">
<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 {
fadeAnimation,
hasAnyLink,
ImmutableArray,
LocalStoreService,
SchemaDetailsDto,
@ -123,7 +122,7 @@ export class SchemaCategoryComponent extends StatefulComponent<State> implements
}
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) {

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

@ -16,8 +16,8 @@ import {
AppLanguagesPayload,
AppLanguagesService,
Resource,
Version,
withLinks
ResourceLinks,
Version
} from '@app/shared/internal';
describe('AppLanguagesService', () => {
@ -169,19 +169,17 @@ describe('AppLanguagesService', () => {
export function createLanguages(...codes: string[]): AppLanguagesPayload {
return {
items: codes.map((code, i) =>
withLinks(
new AppLanguageDto(code, code,
i === 0,
i % 2 === 1,
codes.filter(x => x !== code)),
{
_links: {
update: { method: 'PUT', href: `/languages/${code}` }
}
})),
items: codes.map((code, i) => createLanguage(code, codes, i)),
_links: {
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 {
AnalyticsService,
ApiUrlConfig,
hasAnyLink,
HTTP,
mapVersioned,
pretifyError,
Resource,
ResourceLinks,
Version,
Versioned,
withLinks
Versioned
} from '@app/framework';
export type AppLanguagesDto = Versioned<AppLanguagesPayload>;
export type AppLanguagesPayload = {
readonly items: AppLanguageDto[]
readonly items: AppLanguageDto[];
readonly canCreate: boolean;
} & Resource;
export class AppLanguageDto {
public readonly _links: ResourceLinks = {};
public readonly _links: ResourceLinks;
public readonly canUpdate: boolean;
public readonly canDelete: boolean;
constructor(
links: ResourceLinks,
public readonly iso2Code: string,
public readonly englishName: string,
public readonly isMaster: boolean,
public readonly isOptional: boolean,
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) {
const items: any[] = body.items;
function parseLanguages(response: any) {
const items: any[] = response.items;
const languages = items.map(item =>
withLinks(
new AppLanguageDto(
item.iso2Code,
item.englishName,
item.isMaster,
item.isOptional,
item.fallback || []),
item));
return withLinks({ items: languages, _links: {} }, body);
new AppLanguageDto(item._links,
item.iso2Code,
item.englishName,
item.isMaster,
item.isOptional,
item.fallback || []));
const _links = response._links;
return { items: languages, _links, canCreate: hasAnyLink(_links, 'create') };
}

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

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

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

@ -16,14 +16,25 @@ import {
DateTime,
pretifyError,
Resource,
ResourceLinks,
withLinks
ResourceLinks
} from '@app/framework';
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 name: string,
public readonly permissions: string[],
@ -34,6 +45,7 @@ export class AppDto {
public readonly planName?: string,
public readonly planUpgrade?: string
) {
this._links = links;
}
}
@ -90,16 +102,14 @@ export class AppsService {
}
function parseApp(response: any) {
return withLinks(
new AppDto(
response.id,
response.name,
response.permissions,
DateTime.parseISO_UTC(response.created),
DateTime.parseISO_UTC(response.lastModified),
response.canAccessApi,
response.canAccessContent,
response.planName,
response.planUpgrade),
response);
return new AppDto(response._links,
response.id,
response.name,
response.permissions,
DateTime.parseISO_UTC(response.created),
DateTime.parseISO_UTC(response.lastModified),
response.canAccessApi,
response.canAccessContent,
response.planName,
response.planUpgrade);
}

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

@ -17,6 +17,7 @@ import {
DateTime,
ErrorDto,
Resource,
ResourceLinks,
Version
} from '@app/shared/internal';
@ -332,7 +333,15 @@ describe('AssetsService', () => {
});
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}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${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`,
tags || ['tag1', 'tag2'],
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,
DateTime,
ErrorDto,
hasAnyLink,
HTTP,
Metadata,
pretifyError,
@ -23,24 +24,22 @@ import {
ResultSet,
Types,
Version,
Versioned,
withLinks
Versioned
} from '@app/framework';
export class AssetsDto extends ResultSet<AssetDto> {
public readonly _links: ResourceLinks = {};
}
export class AssetsDto extends ResultSet<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 _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 created: DateTime,
public readonly createdBy: string,
@ -59,6 +58,15 @@ export class AssetDto {
public readonly tags: string[],
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}`);
return HTTP.getVersioned(this.http, url).pipe(
map(({ payload }) => {
const { total, items } = <{ total: number, items: any[] }>payload.body;
const assets = new AssetsDto(total, items.map(item => parseAsset(item)));
return this.http.get<{ total: number, items: any[] } & Resource>(url).pipe(
map(({ total, items, _links }) => {
const assets = items.map(item => parseAsset(item)); ;
return withLinks(assets, payload.body);
return new AssetsDto(total, assets, _links);
}),
pretifyError('Failed to load assets. Please reload.'));
}
@ -249,22 +255,20 @@ function getFormData(file: File) {
}
function parseAsset(response: any) {
return withLinks(
new AssetDto(
response.id,
DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
response.fileName,
response.fileHash,
response.fileType,
response.fileSize,
response.fileVersion,
response.mimeType,
response.isImage,
response.pixelWidth,
response.pixelHeight,
response.slug,
response.tags || [],
new Version(response.version.toString())),
response);
return new AssetDto(response._links, response._meta,
response.id,
DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
response.fileName,
response.fileHash,
response.fileType,
response.fileSize,
response.fileVersion,
response.mimeType,
response.isImage,
response.pixelWidth,
response.pixelHeight,
response.slug,
response.tags || [],
new Version(response.version.toString()));
}

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

@ -58,7 +58,8 @@ describe('BackupsService', () => {
stopped: '2017-02-04',
handledEvents: 13,
handledAssets: 17,
status: 'Failed'
status: 'Failed',
_links: {}
},
{
id: '2',
@ -66,18 +67,16 @@ describe('BackupsService', () => {
stopped: null,
handledEvents: 23,
handledAssets: 27,
status: 'Completed'
status: 'Completed',
_links: {}
}]
});
expect(backups!).toEqual(
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 BackupsDto([
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',

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

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

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

@ -17,8 +17,8 @@ import {
ClientsPayload,
ClientsService,
Resource,
Version,
withLinks
ResourceLinks,
Version
} from '@app/shared/internal';
describe('ClientsService', () => {
@ -154,11 +154,11 @@ describe('ClientsService', () => {
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;
});
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');
@ -190,17 +190,18 @@ describe('ClientsService', () => {
export function createClients(...ids: number[]): ClientsPayload {
return {
items: ids.map(id =>
withLinks(
new ClientDto(`id${id}`, `Client ${id}`, `secret${id}`, `Role${id}`),
{
_links: {
update: { method: 'PUT', href: `/clients/id${id}` }
}
}
)),
items: ids.map(id => createClient(id)),
_links: {
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 {
AnalyticsService,
ApiUrlConfig,
hasAnyLink,
HTTP,
mapVersioned,
pretifyError,
Resource,
ResourceLinks,
Version,
Versioned,
withLinks
Versioned
} from '@app/framework';
export type ClientsDto = Versioned<ClientsPayload>;
export type ClientsPayload = {
readonly items: ClientDto[]
readonly items: ClientDto[];
readonly canCreate: boolean;
} & Resource;
export class ClientDto {
public readonly _links: ResourceLinks = {};
public readonly _links: ResourceLinks;
public readonly canUpdate: boolean;
public readonly canRevoke: boolean;
constructor(
links: ResourceLinks,
public readonly id: string,
public readonly name: string,
public readonly secret: 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 clients = items.map(item =>
withLinks(
new ClientDto(
item.id,
item.name || item.id,
item.secret,
item.role),
item));
return withLinks({ items: clients, _links: {} }, response);
new ClientDto(item._links,
item.id,
item.name || item.id,
item.secret,
item.role));
const _links = response._links;
return { items: clients, _links, canCreate: hasAnyLink(_links, 'create') };
}

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

@ -16,6 +16,7 @@ import {
ContentsService,
DateTime,
Resource,
ResourceLinks,
ScheduleDto,
Version,
Versioned
@ -123,7 +124,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(createContent(12));
req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12));
}));
@ -144,7 +145,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(createContent(12));
req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12));
}));
@ -192,7 +193,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush(createContent(12));
req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12));
}));
@ -219,7 +220,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('PATCH');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush(createContent(12));
req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12));
}));
@ -244,7 +245,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush(createContent(12));
req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12));
}));
@ -269,7 +270,7 @@ describe('ContentsService', () => {
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush(createContent(12));
req.flush(contentResponse(12));
expect(content!).toEqual(createContent(12));
}));
@ -309,7 +310,7 @@ describe('ContentsService', () => {
isPending: true,
data: {},
dataDraft: {},
version: id,
version: `${id}`,
_links: {
update: { method: 'PUT', href: `/contents/id${id}` }
}
@ -318,7 +319,11 @@ describe('ContentsService', () => {
});
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}`,
`Status${id}${suffix}`,
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}`));
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,
ApiUrlConfig,
DateTime,
hasAnyLink,
HTTP,
mapVersioned,
pretifyError,
@ -21,8 +22,7 @@ import {
ResourceLinks,
ResultSet,
Version,
Versioned,
withLinks
Versioned
} from '@app/framework';
export class ScheduleDto {
@ -35,15 +35,27 @@ export class ScheduleDto {
}
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 {
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 status: string,
public readonly created: DateTime,
@ -56,6 +68,9 @@ export class ContentDto {
public readonly dataDraft: object,
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}`);
return HTTP.getVersioned(this.http, url).pipe(
map(({ payload }) => {
const body = payload.body;
const items: any[] = body.items;
return this.http.get<{ total: number, items: [] } & Resource>(url).pipe(
map(({ total, items, _links }) => {
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.'));
}
@ -223,31 +234,19 @@ export class ContentsService {
}
function parseContent(response: any) {
return nextStatuses(withLinks(
new ContentDto(
response.id,
response.status,
DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
response.scheduleJob
? new ScheduleDto(
response.scheduleJob.status,
response.scheduleJob.scheduledBy,
DateTime.parseISO_UTC(response.scheduleJob.dueTime))
: null,
response.isPending === true,
response.data,
response.dataDraft,
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;
return new ContentDto(response._links,
response.id,
response.status,
DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
response.scheduleJob
? new ScheduleDto(
response.scheduleJob.status,
response.scheduleJob.scheduledBy,
DateTime.parseISO_UTC(response.scheduleJob.dueTime))
: null,
response.isPending === true,
response.data,
response.dataDraft,
new Version(response.version));
}

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

@ -16,8 +16,8 @@ import {
ContributorsPayload,
ContributorsService,
Resource,
Version,
withLinks
ResourceLinks,
Version
} from '@app/shared/internal';
describe('ContributorsService', () => {
@ -138,21 +138,22 @@ describe('ContributorsService', () => {
export function createContributors(...ids: number[]): ContributorsPayload {
return {
items: ids.map(id =>
withLinks(
new ContributorDto(`id${id}`, id % 2 === 0 ? 'Owner' : 'Developer'),
{
_links: {
update: { method: 'PUT', href: `/contributors/id${id}` }
}
}
)),
items: ids.map(id => createContributor(id)),
maxContributors: ids.length * 13,
_links: {
create: { method: 'POST', href: '/contributors' }
},
_meta: {
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 {
AnalyticsService,
ApiUrlConfig,
hasAnyLink,
HTTP,
mapVersioned,
pretifyError,
Resource,
ResourceLinks,
Version,
Versioned,
withLinks
Versioned
} from '@app/framework';
export type ContributorsDto = Versioned<ContributorsPayload>;
export type ContributorsPayload = {
readonly items: ContributorDto[],
readonly maxContributors: number
readonly items: ContributorDto[];
readonly maxContributors: number;
readonly canCreate: boolean;
} & Resource;
export class ContributorDto {
public readonly _links: ResourceLinks = {};
public readonly _links: ResourceLinks;
public readonly canUpdate: boolean;
public readonly canRevoke: boolean;
constructor(
links: ResourceLinks,
public readonly contributorId: 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 contributors = items.map(item =>
withLinks(
new ContributorDto(
item.contributorId,
item.role),
item));
new ContributorDto(item._links,
item.contributorId,
item.role));
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,
PatternsService,
Resource,
Version,
withLinks
ResourceLinks,
Version
} from '@app/shared/internal';
describe('PatternsService', () => {
@ -168,17 +168,18 @@ describe('PatternsService', () => {
export function createPatterns(...ids: number[]): PatternsPayload {
return {
items: ids.map(id =>
withLinks(
new PatternDto(`id${id}`, `Name${id}`, `Pattern${id}`, `Message${id}`),
{
_links: {
update: { method: 'PUT', href: `/patterns/id${id}` }
}
}
)),
items: ids.map(id => createPattern(id)),
_links: {
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 {
AnalyticsService,
ApiUrlConfig,
hasAnyLink,
HTTP,
mapVersioned,
pretifyError,
Resource,
ResourceLinks,
Version,
Versioned,
withLinks
Versioned
} from '@app/framework';
export type PatternsDto = Versioned<PatternsPayload>;
export type PatternsPayload = {
items: PatternDto[]
readonly items: PatternDto[],
readonly canCreate: boolean;
} & Resource;
export class PatternDto {
public readonly _links: ResourceLinks = {};
public readonly canDelete: boolean;
public readonly canUpdate: boolean;
constructor(
links: ResourceLinks,
public readonly id: string,
public readonly name: string,
public readonly pattern: 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 patterns = items.map(item =>
withLinks(
new PatternDto(
item.id,
item.name,
item.pattern,
item.message),
item));
return withLinks({ items: patterns, _links: {} }, response);
new PatternDto(item._links,
item.id,
item.name,
item.pattern,
item.message));
const _links = response._links;
return { items: patterns, _links, canCreate: hasAnyLink(_links, 'create') };
}

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

@ -12,12 +12,12 @@ import {
AnalyticsService,
ApiUrlConfig,
Resource,
ResourceLinks,
RoleDto,
RolesDto,
RolesPayload,
RolesService,
Version,
withLinks
Version
} from '@app/shared/internal';
describe('RolesService', () => {
@ -188,17 +188,18 @@ describe('RolesService', () => {
export function createRoles(...ids: number[]): RolesPayload {
return {
items: ids.map(id =>
withLinks(
new RoleDto(`name${id}`, id * 2, id * 3, [`permission${id}`], id % 2 === 0),
{
_links: {
update: { method: 'PUT', href: `/roles/id${id}` }
}
}
)),
items: ids.map(id => createRole(id)),
_links: {
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 {
AnalyticsService,
ApiUrlConfig,
hasAnyLink,
HTTP,
mapVersioned,
pretifyError,
Resource,
ResourceLinks,
Version,
Versioned,
withLinks
Versioned
} from '@app/framework';
export type RolesDto = Versioned<RolesPayload>;
export type RolesPayload = {
items: RoleDto[]
readonly items: RoleDto[];
readonly canCreate: boolean;
} & Resource;
export class RoleDto {
public readonly _links: ResourceLinks = {};
public readonly _links: ResourceLinks;
public readonly canDelete: boolean;
public readonly canUpdate: boolean;
constructor(
links: ResourceLinks,
public readonly name: string,
public readonly numClients: number,
public readonly numContributors: number,
public readonly permissions: string[],
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 roles = items.map(item =>
withLinks(
new RoleDto(
item.name,
item.numClients,
item.numContributors,
item.permissions,
item.isDefaultRole),
item));
return withLinks({ items: roles, _links: {} }, response);
new RoleDto(item._links,
item.name,
item.numClients,
item.numContributors,
item.permissions,
item.isDefaultRole));
const _links = response._links;
return { items: roles, _links, canCreate: hasAnyLink(_links, 'create') };
}

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

@ -14,6 +14,7 @@ import {
ApiUrlConfig,
DateTime,
Resource,
ResourceLinks,
RuleDto,
RuleElementDto,
RuleElementPropertyDto,
@ -126,7 +127,7 @@ describe('RulesService', () => {
});
expect(rules!).toEqual(
new RulesDto(2, [
new RulesDto([
createRule(12),
createRule(13)
]));
@ -390,12 +391,14 @@ describe('RulesService', () => {
});
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}`,
`creator-${id}`,
`modifier-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10`),
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10`),
DateTime.parseISO_UTC(`${id % 1000 + 2000}-12-12T10:10:00`), `creator-${id}`,
DateTime.parseISO_UTC(`${id % 1000 + 2000}-11-11T10:10:00`), `modifier-${id}`,
new Version(`${id}`),
id % 2 === 0,
{
@ -410,10 +413,4 @@ export function createRule(id: number, suffix = '') {
actionType: `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,
ApiUrlConfig,
DateTime,
hasAnyLink,
HTTP,
Model,
pretifyError,
Resource,
ResourceLinks,
ResultSet,
Version,
withLinks
Version
} from '@app/framework';
export const ALL_TRIGGERS = {
@ -76,18 +76,26 @@ export class RuleElementPropertyDto {
}
export class RulesDto extends ResultSet<RuleDto> {
public readonly _links: ResourceLinks = {};
constructor(items: RuleDto[], links?: {}) {
super(items.length, items, links);
}
}
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(
links: ResourceLinks,
public readonly id: string,
public readonly createdBy: string,
public readonly lastModifiedBy: string,
public readonly created: DateTime,
public readonly createdBy: string,
public readonly lastModified: DateTime,
public readonly lastModifiedBy: string,
public readonly version: Version,
public readonly isEnabled: boolean,
public readonly trigger: any,
@ -95,15 +103,19 @@ export class RuleDto {
public readonly action: any,
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> {
public readonly _links: ResourceLinks = {};
public readonly _links: ResourceLinks;
}
export class RuleEventDto extends Model<RuleEventDto> {
public readonly _links: ResourceLinks = {};
public readonly _links: ResourceLinks;
constructor(
public readonly id: string,
@ -176,13 +188,11 @@ export class RulesService {
public getRules(appName: string): Observable<RulesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules`);
return HTTP.getVersioned(this.http, url).pipe(
map(({ payload }) => {
const items: any[] = payload.body.items;
return this.http.get<{ items: [] } & Resource>(url).pipe(
map(({ items, _links }) => {
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.'));
}
@ -311,19 +321,15 @@ export class RulesService {
}
}
function parseRule(resource: any) {
return withLinks(
new RuleDto(
resource.id,
resource.createdBy,
resource.lastModifiedBy,
DateTime.parseISO_UTC(resource.created),
DateTime.parseISO_UTC(resource.lastModified),
new Version(resource.version.toString()),
resource.isEnabled,
resource.trigger,
resource.trigger.triggerType,
resource.action,
resource.action.actionType),
resource);
function parseRule(response: any) {
return new RuleDto(response._links,
response.id,
DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
new Version(response.version.toString()),
response.isEnabled,
response.trigger,
response.trigger.triggerType,
response.action,
response.action.actionType);
}

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

@ -15,6 +15,7 @@ import {
DateTime,
NestedFieldDto,
Resource,
ResourceLinks,
RootFieldDto,
SchemaDetailsDto,
SchemaDto,
@ -69,15 +70,21 @@ describe('SchemasService', () => {
items: [
schemaResponse(12),
schemaResponse(13)
]
],
_links: {
create: { method: 'POST', href: '/schemas' }
}
});
expect(schemas!).toEqual({
canCreate: true,
items: [
createSchema(12),
createSchema(13)
],
_links: {}
_links: {
create: { method: 'POST', href: '/schemas' }
}
});
}));
@ -657,7 +664,8 @@ describe('SchemasService', () => {
isDisabled: true,
properties: {
fieldType: 'String'
}
},
_links: {}
},
{
fieldId: 102,
@ -667,9 +675,11 @@ describe('SchemasService', () => {
isDisabled: true,
properties: {
fieldType: 'Number'
}
},
_links: {}
}
]
],
_links: {}
},
{
fieldId: 12,
@ -680,7 +690,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'Assets'
}
},
_links: {}
},
{
fieldId: 13,
@ -691,7 +702,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'Boolean'
}
},
_links: {}
},
{
fieldId: 14,
@ -702,7 +714,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'DateTime'
}
},
_links: {}
},
{
fieldId: 15,
@ -713,7 +726,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'Geolocation'
}
},
_links: {}
},
{
fieldId: 16,
@ -724,7 +738,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'Json'
}
},
_links: {}
},
{
fieldId: 17,
@ -735,7 +750,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'Number'
}
},
_links: {}
},
{
fieldId: 18,
@ -746,7 +762,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'References'
}
},
_links: {}
},
{
fieldId: 19,
@ -757,7 +774,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'String'
}
},
_links: {}
},
{
fieldId: 20,
@ -768,7 +786,8 @@ describe('SchemasService', () => {
partitioning: 'language',
properties: {
fieldType: 'Tags'
}
},
_links: {}
}
],
scripts: {
@ -786,7 +805,11 @@ describe('SchemasService', () => {
});
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-name${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}-11-11T10:10:00`), `modifier-${id}`,
new Version(`${id}`));
result._links['update'] = { method: 'PUT', href: `/schemas/${id}` };
return result;
}
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-name${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}`,
version,
[
new RootFieldDto(11, 'field11', createProperties('Array'), 'language', true, true, true, [
new NestedFieldDto(101, 'field101', createProperties('String'), 11, true, true, true),
new NestedFieldDto(102, 'field102', createProperties('Number'), 11, 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({}, 102, 'field102', createProperties('Number'), 11, 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(14, 'field14', createProperties('DateTime'), '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(17, 'field17', createProperties('Number'), '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(20, 'field20', createProperties('Tags'), '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({}, 14, 'field14', createProperties('DateTime'), '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({}, 17, 'field17', createProperties('Number'), '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({}, 20, 'field20', createProperties('Tags'), 'language', true, true, true)
],
{
query: '<script-query>',
@ -838,8 +861,4 @@ export function createSchemaDetails(id: number, version: Version, suffix = '') {
{
'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,
ApiUrlConfig,
DateTime,
hasAnyLink,
HTTP,
pretifyError,
Resource,
ResourceLinks,
StringHelper,
Version,
Versioned,
withLinks
Versioned
} from '@app/framework';
import { createProperties, FieldPropertiesDto } from './schemas.types';
export type SchemasDto = { items: SchemaDto[] } & Resource;
export type SchemasDto = {
readonly items: SchemaDto[];
export class SchemaDto {
public readonly _links: ResourceLinks = {};
public get displayName() {
return StringHelper.firstNonEmpty(this.properties.label, this.name);
}
readonly canCreate: boolean;
} & Resource;
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 name: string,
public readonly category: string,
@ -48,6 +61,20 @@ export class SchemaDto {
public readonly lastModifiedBy: string,
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 listFieldsEditable: RootFieldDto[];
constructor(id: string, name: string, category: string,
constructor(links: ResourceLinks, id: string, name: string, category: string,
properties: SchemaPropertiesDto,
isSingleton: boolean,
isPublished: boolean,
@ -68,7 +95,7 @@ export class SchemaDetailsDto extends SchemaDto {
public readonly scripts = {},
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) {
let listFields = this.fields.filter(x => x.properties.isListField && x.properties.isContentField);
@ -88,7 +115,17 @@ export class SchemaDetailsDto extends SchemaDto {
}
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 {
return !this.isDisabled && this.properties['inlineEditable'] === true;
@ -102,7 +139,7 @@ export class FieldDto {
return this.properties.placeholder || '';
}
constructor(
constructor(links: ResourceLinks,
public readonly fieldId: number,
public readonly name: string,
public readonly properties: FieldPropertiesDto,
@ -110,6 +147,17 @@ export class FieldDto {
public readonly isHidden: 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');
}
constructor(fieldId: number, name: string, properties: FieldPropertiesDto,
constructor(links: ResourceLinks, fieldId: number, name: string, properties: FieldPropertiesDto,
public readonly partitioning: string,
isLocked: boolean = false,
isHidden: boolean = false,
isDisabled: boolean = false,
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];
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,
isLocked: boolean = false,
isHidden: 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(
map(({ payload }) => {
const body = 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);
return parseSchemas(payload.body);
}),
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) {
const fields = response.fields.map((item: any) => {
const propertiesDto =
@ -503,47 +553,41 @@ function parseSchemaWithDetails(response: any, version: Version) {
nestedItem.properties.fieldType,
nestedItem.properties);
return withLinks(
new NestedFieldDto(
nestedItem.fieldId,
nestedItem.name,
nestedPropertiesDto,
item.fieldId,
nestedItem.isLocked,
nestedItem.isHidden,
nestedItem.isDisabled),
nestedItem);
return new NestedFieldDto(nestedItem._links,
nestedItem.fieldId,
nestedItem.name,
nestedPropertiesDto,
item.fieldId,
nestedItem.isLocked,
nestedItem.isHidden,
nestedItem.isDisabled);
});
}
return withLinks(
new RootFieldDto(
item.fieldId,
item.name,
propertiesDto,
item.partitioning,
item.isLocked,
item.isHidden,
item.isDisabled,
nested || []),
item);
return new RootFieldDto(item._links,
item.fieldId,
item.name,
propertiesDto,
item.partitioning,
item.isLocked,
item.isHidden,
item.isDisabled,
nested || []);
});
const properties = new SchemaPropertiesDto(response.properties.label, response.properties.hints);
return withLinks(
new SchemaDetailsDto(
response.id,
response.name,
response.category,
properties,
response.isSingleton,
response.isPublished,
DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
version,
fields,
response.scripts || {},
response.previewUrls || {}),
response);
return new SchemaDetailsDto(response._links,
response.id,
response.name,
response.category,
properties,
response.isSingleton,
response.isPublished,
DateTime.parseISO_UTC(response.created), response.createdBy,
DateTime.parseISO_UTC(response.lastModified), response.lastModifiedBy,
version,
fields,
response.scripts || {},
response.previewUrls || {});
}

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

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

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

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

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

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

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

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

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

@ -40,8 +40,11 @@ interface Snapshot {
// Indicates if the clients are loaded.
isLoaded?: boolean;
// Indicates if the user can create new clients.
canCreate?: boolean;
// The links.
links: ResourceLinks;
_links?: ResourceLinks;
}
type ClientsList = ImmutableArray<ClientDto>;
@ -56,8 +59,8 @@ export class ClientsState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
public links =
this.changes.pipe(map(x => x.links),
public canCreate =
this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged());
constructor(
@ -65,7 +68,7 @@ export class ClientsState extends State<Snapshot> {
private readonly appsState: AppsState,
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> {
@ -111,8 +114,10 @@ export class ClientsState extends State<Snapshot> {
private replaceClients(payload: ClientsPayload, version: Version) {
const clients = ImmutableArray.of(payload.items);
const { _links, canCreate } = payload;
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[]) {
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') {
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.
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.
links: ResourceLinks;
_links?: ResourceLinks;
}
export const CONTENT_STATUSES = {
@ -90,8 +96,16 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.changes.pipe(map(x => x.status),
distinctUntilChanged());
public links =
this.changes.pipe(map(x => x.links),
public canCreateAny =
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());
constructor(
@ -99,7 +113,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
private readonly contentsService: ContentsService,
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> {
@ -144,7 +158,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
this.snapshot.contentsPager.skip,
this.snapshot.contentsQuery, undefined,
this.snapshot.status).pipe(
tap(({ total, items, _links: links }) => {
tap(({ total, items, _links, canCreate, canCreateAndPublish }) => {
if (isReload) {
this.dialogs.notifyInfo('Contents reloaded.');
}
@ -158,8 +172,7 @@ export abstract class ContentsStateBase extends State<Snapshot> {
if (selectedContent) {
selectedContent = contents.find(x => x.id === selectedContent!.id) || selectedContent;
}
return { ...s, contents, contentsPager, selectedContent, isLoaded: true, links };
return { ...s, contents, contentsPager, selectedContent, isLoaded: true, _links, canCreate, canCreateAndPublish };
});
}));
}

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

@ -46,8 +46,11 @@ interface Snapshot {
// The app version.
version: Version;
// Indicates if the user can add a contributor.
canCreate?: boolean;
// The links.
links: ResourceLinks;
_links?: ResourceLinks;
}
type ContributorsList = ImmutableArray<ContributorDto>;
@ -58,10 +61,6 @@ export class ContributorsState extends State<Snapshot> {
this.changes.pipe(map(x => x.contributors),
distinctUntilChanged());
public isMaxReached =
this.changes.pipe(map(x => x.isMaxReached),
distinctUntilChanged());
public isLoaded =
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
@ -70,8 +69,8 @@ export class ContributorsState extends State<Snapshot> {
this.changes.pipe(map(x => x.maxContributors),
distinctUntilChanged());
public links =
this.changes.pipe(map(x => x.links),
public canCreate =
this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged());
constructor(
@ -79,7 +78,7 @@ export class ContributorsState extends State<Snapshot> {
private readonly appsState: AppsState,
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> {
@ -130,7 +129,9 @@ export class ContributorsState extends State<Snapshot> {
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.
isLoaded?: boolean;
// Inedicates if the user can add a language.
canCreate?: boolean;
// The links.
links: ResourceLinks;
_links?: ResourceLinks;
}
type AppLanguagesList = ImmutableArray<AppLanguageDto>;
@ -81,8 +84,8 @@ export class LanguagesState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
public links =
this.changes.pipe(map(x => x.links),
public canCreate =
this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged());
constructor(
@ -96,8 +99,7 @@ export class LanguagesState extends State<Snapshot> {
allLanguages: ImmutableArray.empty(),
allLanguagesNew: ImmutableArray.empty(),
languages: ImmutableArray.empty(),
version: Version.EMPTY,
links: {}
version: Version.EMPTY
});
}
@ -154,14 +156,17 @@ export class LanguagesState extends State<Snapshot> {
const languages = ImmutableArray.of(payload.items);
const { _links, canCreate } = payload;
return {
...s,
canCreate,
languages: languages.map(x => this.createLanguage(x, languages)),
plainLanguages: payload,
allLanguages: allLanguages,
allLanguagesNew: allLanguages.filter(x => !languages.find(l => l.iso2Code === x.iso2Code)),
isLoaded: true,
links: payload._links,
_links,
version: version
};
});

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

@ -38,6 +38,9 @@ interface Snapshot {
// Indicates if the patterns are loaded.
isLoaded?: boolean;
// Indicates if patterns can be created.
canCreate?: boolean;
// The links.
links: ResourceLinks;
}
@ -54,8 +57,8 @@ export class PatternsState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
public links =
this.changes.pipe(map(x => x.links),
public canCreate =
this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged());
constructor(
@ -109,8 +112,10 @@ export class PatternsState extends State<Snapshot> {
private replacePatterns(payload: PatternsPayload, version: Version) {
const patterns = ImmutableArray.of(payload.items);
const { _links: links, canCreate } = payload;
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.
isLoaded?: boolean;
// Indicates if the user can add a role.
canCreate?: boolean;
// The links.
links: ResourceLinks;
_links?: ResourceLinks;
}
type RolesList = ImmutableArray<RoleDto>;
@ -54,8 +57,8 @@ export class RolesState extends State<Snapshot> {
this.changes.pipe(map(x => !!x.isLoaded),
distinctUntilChanged());
public links =
this.changes.pipe(map(x => x.links),
public canCreate =
this.changes.pipe(map(x => !!x.canCreate),
distinctUntilChanged());
constructor(
@ -63,7 +66,7 @@ export class RolesState extends State<Snapshot> {
private readonly appsState: AppsState,
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> {
@ -109,8 +112,10 @@ export class RolesState extends State<Snapshot> {
private replaceRoles(payload: RolesPayload, version: Version) {
const roles = ImmutableArray.of(payload.items);
const { _links, canCreate } = payload;
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', () => {
it('should load rules', () => {
rulesService.setup(x => x.getRules(app))
.returns(() => of(new RulesDto(2, [rule1, rule2]))).verifiable();
.returns(() => of(new RulesDto([rule1, rule2]))).verifiable();
rulesState.load().subscribe();
@ -64,7 +64,7 @@ describe('RulesState', () => {
it('should show notification on load when reload is true', () => {
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();
@ -78,7 +78,7 @@ describe('RulesState', () => {
describe('Updates', () => {
beforeEach(() => {
rulesService.setup(x => x.getRules(app))
.returns(() => of(new RulesDto(2, [rule1, rule2]))).verifiable();
.returns(() => of(new RulesDto([rule1, rule2]))).verifiable();
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),
distinctUntilChanged());
public links =
public canCreate =
this.changes.pipe(map(x => x.links),
distinctUntilChanged());

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

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

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

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

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

@ -13,8 +13,7 @@ import {
ResourcesDto,
UIService,
UIState,
UsersService,
withLinks
UsersService
} from '@app/shared/internal';
import { TestValues } from './_test-helpers';
@ -38,7 +37,9 @@ describe('UIState', () => {
};
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>;
@ -63,7 +64,7 @@ describe('UIState', () => {
usersService = Mock.ofType<UsersService>();
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);
});
@ -76,7 +77,9 @@ describe('UIState', () => {
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', () => {

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 {
ResourceLinks,
hasAnyLink,
State,
Types
} from '@app/framework';
@ -30,7 +30,17 @@ interface Snapshot {
// The merged settings of app and common settings.
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()
@ -39,8 +49,20 @@ export class UIState extends State<Snapshot> {
this.changes.pipe(map(x => x.settings),
distinctUntilChanged());
public resources =
this.changes.pipe(map(x => x.resources),
public canReadEvents =
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());
public get<T>(path: string, defaultValue: T) {
@ -53,7 +75,7 @@ export class UIState extends State<Snapshot> {
private readonly uiService: UIService,
private readonly usersService: UsersService
) {
super({ settings: {}, settingsCommon: {}, settingsApp: {}, resources: {} });
super({ settings: {}, settingsCommon: {}, settingsApp: {} });
this.loadResources();
this.loadCommon();
@ -82,7 +104,11 @@ export class UIState extends State<Snapshot> {
private loadResources() {
this.usersService.getResources()
.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">
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'schemas'">
<li class="nav-item" *ngIf="selectedApp.canReadSchemas">
<a class="nav-link" routerLink="schemas" routerLinkActive="active">
<i class="nav-icon icon-schemas"></i> <div class="nav-text">Schemas</div>
</a>
@ -9,12 +9,12 @@
<i class="nav-icon icon-contents"></i> <div class="nav-text">Content</div>
</a>
</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">
<i class="nav-icon icon-assets"></i> <div class="nav-text">Assets</div>
</a>
</li>
<li class="nav-item" *ngIf="selectedApp | sqxHasLink:'rules'">
<li class="nav-item" *ngIf="selectedApp.canReadRules">
<a class="nav-link" routerLink="rules" routerLinkActive="active">
<i class="nav-icon icon-rules"></i> <div class="nav-text">Rules</div>
</a>

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

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

Loading…
Cancel
Save