From 48e5fe5f3c75ca42f02ddc36fcc4401117cfee95 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 5 Sep 2022 13:25:11 +0200 Subject: [PATCH] Simplify clients. (#917) * Simplify clients. * Fix tests. --- .../Areas/Api/Controllers/UI/MyUIOptions.cs | 9 - backend/src/Squidex/appsettings.json | 7 +- frontend/.eslintrc.js | 1 + .../pages/users/user-page.component.ts | 4 +- .../services/event-consumers.service.spec.ts | 7 +- .../services/event-consumers.service.ts | 27 +- .../services/users.service.spec.ts | 18 +- .../administration/services/users.service.ts | 54 ++-- .../state/event-consumers.state.spec.ts | 8 +- .../administration/state/users.forms.ts | 4 +- .../administration/state/users.state.spec.ts | 26 +- .../administration/state/users.state.ts | 6 +- .../pages/graphql/graphql-page.component.ts | 2 +- .../pages/content/content-page.component.ts | 2 +- .../content/editor/content-field.component.ts | 2 +- .../pages/contents/contents-page.component.ts | 4 +- .../shared/forms/array-editor.component.ts | 2 +- .../shared/forms/array-item.component.ts | 4 +- .../shared/forms/component.component.ts | 2 +- .../shared/forms/field-editor.component.html | 2 +- .../reference-dropdown.component.ts | 2 +- .../references/reference-item.component.ts | 6 +- .../references/references-tags.component.ts | 2 +- .../src/app/features/rules/pages/messages.ts | 2 +- .../rules/pages/rule/rule-page.component.ts | 2 +- .../pages/schema/ui/field-list.component.ts | 4 +- .../pages/backups/backup.component.ts | 2 +- .../pages/templates/template.component.ts | 8 +- .../pages/workflows/workflow.component.ts | 4 +- .../angular/compensate-scrollbar.directive.ts | 2 +- .../forms/editors/code-editor.component.ts | 2 +- .../forms/editors/tag-editor.stories.ts | 2 +- .../framework/angular/if-once.directive.ts | 2 +- frontend/src/app/framework/internal.ts | 1 - frontend/src/app/framework/module.ts | 3 +- .../framework/services/analytics.service.ts | 68 ----- .../app/framework/services/dialog.service.ts | 2 +- frontend/src/app/framework/state.ts | 19 +- .../app/framework/utils/modal-positioner.ts | 2 +- .../src/app/framework/utils/text-measurer.ts | 4 +- .../shared/components/app-form.component.ts | 2 +- .../contents/content-list-cell.directive.ts | 46 +-- .../contents/content-list-field.component.ts | 4 +- .../contents/content-list-header.component.ts | 10 +- .../forms/markdown-editor.component.ts | 2 +- .../components/forms/rich-editor.component.ts | 2 +- frontend/src/app/shared/components/pipes.ts | 2 +- .../content-selector-item.component.ts | 4 +- .../references/content-selector.component.ts | 6 +- .../references/reference-input.component.ts | 2 +- .../services/app-languages.service.spec.ts | 6 +- .../shared/services/app-languages.service.ts | 74 ++--- .../app/shared/services/apps.service.spec.ts | 15 +- .../src/app/shared/services/apps.service.ts | 80 +++--- .../shared/services/assets.service.spec.ts | 24 +- .../src/app/shared/services/assets.service.ts | 192 +++++++------ .../shared/services/backups.service.spec.ts | 11 +- .../app/shared/services/backups.service.ts | 49 ++-- .../shared/services/clients.service.spec.ts | 6 +- .../app/shared/services/clients.service.ts | 84 +++--- .../app/shared/services/comments.service.ts | 9 +- .../shared/services/contents.service.spec.ts | 16 +- .../app/shared/services/contents.service.ts | 171 ++++++----- .../services/contributors.service.spec.ts | 12 +- .../shared/services/contributors.service.ts | 69 +++-- .../app/shared/services/history.service.ts | 21 +- .../app/shared/services/languages.service.ts | 10 +- .../app/shared/services/news.service.spec.ts | 19 +- .../src/app/shared/services/news.service.ts | 44 +-- .../app/shared/services/plans.service.spec.ts | 3 +- .../src/app/shared/services/plans.service.ts | 75 ++--- frontend/src/app/shared/services/query.ts | 2 +- .../app/shared/services/roles.service.spec.ts | 6 +- .../src/app/shared/services/roles.service.ts | 71 +++-- .../app/shared/services/rules.service.spec.ts | 45 ++- .../src/app/shared/services/rules.service.ts | 175 +++++++----- .../shared/services/schemas.service.spec.ts | 8 +- .../app/shared/services/schemas.service.ts | 265 ++++++++++-------- .../src/app/shared/services/schemas.spec.ts | 18 +- .../src/app/shared/services/search.service.ts | 18 +- .../shared/services/stock-photo.service.ts | 17 +- .../shared/services/templates.service.spec.ts | 7 +- .../app/shared/services/templates.service.ts | 32 ++- .../shared/services/translations.service.ts | 12 +- .../src/app/shared/services/ui.service.ts | 6 +- .../app/shared/services/users.service.spec.ts | 12 +- .../src/app/shared/services/users.service.ts | 17 +- .../shared/services/workflows.service.spec.ts | 10 +- .../app/shared/services/workflows.service.ts | 124 +++++--- .../app/shared/state/asset-uploader.state.ts | 2 +- .../src/app/shared/state/assets.state.spec.ts | 40 +-- .../app/shared/state/backups.state.spec.ts | 8 +- .../src/app/shared/state/comments.state.ts | 11 +- .../shared/state/contents.forms-helpers.ts | 2 +- .../app/shared/state/contents.forms.spec.ts | 16 +- .../src/app/shared/state/contents.state.ts | 2 +- .../app/shared/state/contributors.state.ts | 2 +- .../src/app/shared/state/resolvers.spec.ts | 20 +- frontend/src/app/shared/state/resolvers.ts | 8 +- .../shared/state/rule-events.state.spec.ts | 10 +- .../src/app/shared/state/rule-events.state.ts | 34 ++- .../shared/state/rule-simulator.state.spec.ts | 5 +- .../src/app/shared/state/rules.state.spec.ts | 12 +- .../app/shared/state/table-settings.spec.ts | 52 ++-- .../src/app/shared/state/table-settings.ts | 24 +- .../app/shared/state/template.state.spec.ts | 6 +- .../src/app/shared/state/ui.state.spec.ts | 4 +- 107 files changed, 1322 insertions(+), 1181 deletions(-) delete mode 100644 frontend/src/app/framework/services/analytics.service.ts diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs index c0df1c0c3..07ff74b34 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs @@ -20,9 +20,6 @@ namespace Squidex.Areas.Api.Controllers.UI [JsonPropertyName("map")] public MapOptions Map { get; set; } - [JsonPropertyName("google")] - public GoogleOptions Google { get; set; } - [JsonPropertyName("referencesDropdownItemCount")] public int ReferencesDropdownItemCount { get; set; } = 100; @@ -64,11 +61,5 @@ namespace Squidex.Areas.Api.Controllers.UI [JsonPropertyName("key")] public string Key { get; set; } } - - public sealed class GoogleOptions - { - [JsonPropertyName("analyticsId")] - public string AnalyticsId { get; set; } - } } } diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 07d7321d2..5641ba469 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -152,12 +152,7 @@ "showInfo": false, // The number of content items for dropdown selector. - "referencesDropdownItemCount": 100, - - "google": { - // The Google analytics ID. - "analyticsId": "UA-99989790-2" - } + "referencesDropdownItemCount": 100 }, "email": { diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 254ec9863..a8f1ac994 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -107,6 +107,7 @@ module.exports = { "no-plusplus": "off", "no-prototype-builtins": "off", "no-restricted-syntax": "off", + "no-trailing-spaces": "error", "no-underscore-dangle": "off", "object-curly-newline": [ "error", diff --git a/frontend/src/app/features/administration/pages/users/user-page.component.ts b/frontend/src/app/features/administration/pages/users/user-page.component.ts index d8781c4e9..da8f2d2e7 100644 --- a/frontend/src/app/features/administration/pages/users/user-page.component.ts +++ b/frontend/src/app/features/administration/pages/users/user-page.component.ts @@ -7,7 +7,7 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { CreateUserDto, UserDto, UserForm, UsersState } from '@app/features/administration/internal'; +import { UpsertUserDto, UserDto, UserForm, UsersState } from '@app/features/administration/internal'; import { ResourceOwner } from '@app/shared'; @Component({ @@ -63,7 +63,7 @@ export class UserPageComponent extends ResourceOwner implements OnInit { }, }); } else { - this.usersState.create(value) + this.usersState.create(value) .subscribe({ next: () => { this.back(); diff --git a/frontend/src/app/features/administration/services/event-consumers.service.spec.ts b/frontend/src/app/features/administration/services/event-consumers.service.spec.ts index cb3e5eead..7316407f3 100644 --- a/frontend/src/app/features/administration/services/event-consumers.service.spec.ts +++ b/frontend/src/app/features/administration/services/event-consumers.service.spec.ts @@ -47,11 +47,12 @@ describe('EventConsumersService', () => { ], }); - expect(eventConsumers!).toEqual( - new EventConsumersDto(2, [ + expect(eventConsumers!).toEqual({ + items: [ createEventConsumer(12), createEventConsumer(13), - ])); + ], + }); })); it('should make put request to start event consumer', diff --git a/frontend/src/app/features/administration/services/event-consumers.service.ts b/frontend/src/app/features/administration/services/event-consumers.service.ts index b7360d23e..69012ac60 100644 --- a/frontend/src/app/features/administration/services/event-consumers.service.ts +++ b/frontend/src/app/features/administration/services/event-consumers.service.ts @@ -9,17 +9,14 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks, ResultSet } from '@app/shared'; +import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks } from '@app/shared'; -export class EventConsumersDto extends ResultSet { -} - -export class EventConsumerDto { +export class EventConsumerDto implements Resource { public readonly _links: ResourceLinks; - public readonly canStop: boolean; - public readonly canStart: boolean; public readonly canReset: boolean; + public readonly canStart: boolean; + public readonly canStop: boolean; constructor(links: ResourceLinks, public readonly name: string, @@ -31,12 +28,17 @@ export class EventConsumerDto { ) { this._links = links; - this.canStop = hasAnyLink(links, 'stop'); - this.canStart = hasAnyLink(links, 'start'); this.canReset = hasAnyLink(links, 'reset'); + this.canStart = hasAnyLink(links, 'start'); + this.canStop = hasAnyLink(links, 'stop'); } } +export type EventConsumersDto = Readonly<{ + // The list of event consumers. + items: ReadonlyArray; +}>; + @Injectable() export class EventConsumersService { constructor( @@ -92,10 +94,11 @@ export class EventConsumersService { } } -function parseEventConsumers(response: { items: any[] } & Resource) { - const items = response.items.map(parseEventConsumer); +function parseEventConsumers(response: { items: any[] } & Resource): EventConsumersDto { + const { items: list } = response; + const items = list.map(parseEventConsumer); - return new EventConsumersDto(items.length, items, response._links); + return { items }; } function parseEventConsumer(response: any): EventConsumerDto { diff --git a/frontend/src/app/features/administration/services/users.service.spec.ts b/frontend/src/app/features/administration/services/users.service.spec.ts index 963f91ae3..ae3547d64 100644 --- a/frontend/src/app/features/administration/services/users.service.spec.ts +++ b/frontend/src/app/features/administration/services/users.service.spec.ts @@ -48,11 +48,14 @@ describe('UsersService', () => { ], }); - expect(users!).toEqual( - new UsersDto(100, [ + expect(users!).toEqual({ + total: 100, + items: [ createUser(12), createUser(13), - ])); + ], + canCreate: false, + }); })); it('should make get request with query to get many users', @@ -76,11 +79,14 @@ describe('UsersService', () => { ], }); - expect(users!).toEqual( - new UsersDto(100, [ + expect(users!).toEqual({ + total: 100, + items: [ createUser(12), createUser(13), - ])); + ], + canCreate: false, + }); })); it('should make get request to get single user', diff --git a/frontend/src/app/features/administration/services/users.service.ts b/frontend/src/app/features/administration/services/users.service.ts index 38f9dcfe1..d565f5b10 100644 --- a/frontend/src/app/features/administration/services/users.service.ts +++ b/frontend/src/app/features/administration/services/users.service.ts @@ -9,21 +9,15 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks, ResultSet } from '@app/shared'; +import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks } from '@app/shared'; -export class UsersDto extends ResultSet { - public get canCreate() { - return hasAnyLink(this._links, 'create'); - } -} - -export class UserDto { +export class UserDto implements Resource { public readonly _links: ResourceLinks; + public readonly canDelete: boolean; public readonly canLock: boolean; public readonly canUnlock: boolean; public readonly canUpdate: boolean; - public readonly canDelete: boolean; constructor(links: ResourceLinks, public readonly id: string, @@ -34,20 +28,37 @@ export class UserDto { ) { this._links = links; + this.canDelete = hasAnyLink(links, 'delete'); this.canLock = hasAnyLink(links, 'lock'); this.canUnlock = hasAnyLink(links, 'unlock'); this.canUpdate = hasAnyLink(links, 'update'); - this.canDelete = hasAnyLink(links, 'delete'); } } -type Permissions = readonly string[]; +export type UsersDto = Readonly<{ + // The list of users. + items: ReadonlyArray; + + // The number of users. + total: number; + + // True, if the user has permissions to create a user. + canCreate?: boolean; +}>; -export type CreateUserDto = - Readonly<{ email: string; displayName: string; permissions: Permissions; password: string }>; +export type UpsertUserDto = Readonly<{ + // The email address of the user. + email: string; -export type UpdateUserDto = - Partial; + // The display name. + displayName?: string; + + // The permissions as in the dot-notation. + permissions?: ReadonlyArray; + + // The password (confirm is only used in the UI). + password?: string; +}>; @Injectable() export class UsersService { @@ -77,7 +88,7 @@ export class UsersService { pretifyError('i18n:users.loadUserFailed')); } - public postUser(dto: CreateUserDto): Observable { + public postUser(dto: UpsertUserDto): Observable { const url = this.apiUrl.buildUrl('api/user-management'); return this.http.post(url, dto).pipe( @@ -87,7 +98,7 @@ export class UsersService { pretifyError('i18n:users.createFailed')); } - public putUser(user: Resource, dto: UpdateUserDto): Observable { + public putUser(user: Resource, dto: Partial): Observable { const link = user._links['update']; const url = this.apiUrl.buildUrl(link.href); @@ -133,10 +144,13 @@ export class UsersService { } } -function parseUsers(response: { items: any[]; total: number } & Resource) { - const items = response.items.map(parseUser); +function parseUsers(response: { items: any[]; total: number } & Resource): UsersDto { + const { items: list, total, _links } = response; + const items = list.map(parseUser); + + const canCreate = hasAnyLink(_links, 'create'); - return new UsersDto(response.total, items, response._links); + return { items, total, canCreate }; } function parseUser(response: any) { diff --git a/frontend/src/app/features/administration/state/event-consumers.state.spec.ts b/frontend/src/app/features/administration/state/event-consumers.state.spec.ts index f2cef1330..8cd472089 100644 --- a/frontend/src/app/features/administration/state/event-consumers.state.spec.ts +++ b/frontend/src/app/features/administration/state/event-consumers.state.spec.ts @@ -8,7 +8,7 @@ import { of, throwError } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; -import { EventConsumersDto, EventConsumersService } from '@app/features/administration/internal'; +import { EventConsumersService } from '@app/features/administration/internal'; import { DialogService } from '@app/framework'; import { createEventConsumer } from './../services/event-consumers.service.spec'; import { EventConsumersState } from './event-consumers.state'; @@ -35,7 +35,7 @@ describe('EventConsumersState', () => { describe('Loading', () => { it('should load event consumers', () => { eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => of(new EventConsumersDto(2, [eventConsumer1, eventConsumer2], {}))).verifiable(); + .returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable(); eventConsumersState.load().subscribe(); @@ -57,7 +57,7 @@ describe('EventConsumersState', () => { it('should show notification on load if reload is true', () => { eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => of(new EventConsumersDto(2, [eventConsumer1, eventConsumer2], {}))).verifiable(); + .returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable(); eventConsumersState.load(true).subscribe(); @@ -81,7 +81,7 @@ describe('EventConsumersState', () => { describe('Updates', () => { beforeEach(() => { eventConsumersService.setup(x => x.getEventConsumers()) - .returns(() => of(new EventConsumersDto(2, [eventConsumer1, eventConsumer2], {}))).verifiable(); + .returns(() => of({ items: [eventConsumer1, eventConsumer2] })).verifiable(); eventConsumersState.load().subscribe(); }); diff --git a/frontend/src/app/features/administration/state/users.forms.ts b/frontend/src/app/features/administration/state/users.forms.ts index 151275ab4..aa14eccba 100644 --- a/frontend/src/app/features/administration/state/users.forms.ts +++ b/frontend/src/app/features/administration/state/users.forms.ts @@ -7,9 +7,9 @@ import { FormControl, Validators } from '@angular/forms'; import { ExtendedFormGroup, Form, ValidatorsEx } from '@app/shared'; -import { UpdateUserDto, UserDto } from './../services/users.service'; +import { UpsertUserDto, UserDto } from './../services/users.service'; -export class UserForm extends Form { +export class UserForm extends Form { constructor() { super(new ExtendedFormGroup({ email: new FormControl('', [ diff --git a/frontend/src/app/features/administration/state/users.state.spec.ts b/frontend/src/app/features/administration/state/users.state.spec.ts index 59b839d96..e8cd798d9 100644 --- a/frontend/src/app/features/administration/state/users.state.spec.ts +++ b/frontend/src/app/features/administration/state/users.state.spec.ts @@ -8,7 +8,7 @@ import { firstValueFrom, of, throwError } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; -import { UsersDto, UsersService } from '@app/features/administration/internal'; +import { UpsertUserDto, UsersService } from '@app/features/administration/internal'; import { DialogService } from '@app/shared'; import { createUser } from './../services/users.service.spec'; import { UsersState } from './users.state'; @@ -17,8 +17,6 @@ describe('UsersState', () => { const user1 = createUser(1); const user2 = createUser(2); - const oldUsers = new UsersDto(200, [user1, user2]); - const newUser = createUser(3); let dialogs: IMock; @@ -39,7 +37,7 @@ describe('UsersState', () => { describe('Loading', () => { it('should load users', () => { usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of(oldUsers)).verifiable(); + .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(); usersState.load().subscribe(); @@ -62,7 +60,7 @@ describe('UsersState', () => { it('should show notification on load if reload is true', () => { usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of(oldUsers)).verifiable(); + .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(); usersState.load(true).subscribe(); @@ -73,7 +71,7 @@ describe('UsersState', () => { it('should load with new pagination if paging', () => { usersService.setup(x => x.getUsers(10, 10, undefined)) - .returns(() => of(new UsersDto(200, []))).verifiable(); + .returns(() => of({ items: [], total: 200 })).verifiable(); usersState.page({ page: 1, pageSize: 10 }).subscribe(); @@ -82,7 +80,7 @@ describe('UsersState', () => { it('should load with query if searching', () => { usersService.setup(x => x.getUsers(10, 0, 'my-query')) - .returns(() => of(new UsersDto(0, []))).verifiable(); + .returns(() => of({ items: [], total: 0 })).verifiable(); usersState.search('my-query').subscribe(); @@ -93,7 +91,7 @@ describe('UsersState', () => { describe('Updates', () => { beforeEach(() => { usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of(oldUsers)).verifiable(); + .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(); usersState.load().subscribe(); }); @@ -133,7 +131,7 @@ describe('UsersState', () => { }); it('should add user to snapshot if created', () => { - const request = { ...newUser, password: 'password' }; + const request: UpsertUserDto = { ...newUser, password: 'password' } as any; usersService.setup(x => x.postUser(request)) .returns(() => of(newUser)).verifiable(); @@ -145,7 +143,7 @@ describe('UsersState', () => { }); it('should update user if updated', () => { - const request = {}; + const request: Partial = {}; const updated = createUser(2, '_new'); @@ -190,10 +188,10 @@ describe('UsersState', () => { }); it('should truncate users if page size reached', () => { - const request = { ...newUser, password: 'password' }; + const request: UpsertUserDto = { ...newUser, password: 'password' } as any; usersService.setup(x => x.getUsers(2, 0, undefined)) - .returns(() => of(new UsersDto(200, [user1, user2]))).verifiable(); + .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(); usersService.setup(x => x.postUser(request)) .returns(() => of(newUser)).verifiable(); @@ -209,7 +207,7 @@ describe('UsersState', () => { describe('Selection', () => { beforeEach(() => { usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of(oldUsers)).verifiable(Times.atLeastOnce()); + .returns(() => of({ items: [user1, user2], total: 200 })).verifiable(Times.atLeastOnce()); usersState.load().subscribe(); usersState.select(user2.id).subscribe(); @@ -222,7 +220,7 @@ describe('UsersState', () => { ]; usersService.setup(x => x.getUsers(10, 0, undefined)) - .returns(() => of(new UsersDto(2, newUsers))); + .returns(() => of({ items: newUsers, total: 200 })); usersState.load().subscribe(); diff --git a/frontend/src/app/features/administration/state/users.state.ts b/frontend/src/app/features/administration/state/users.state.ts index e45e79ec6..99bd830e2 100644 --- a/frontend/src/app/features/administration/state/users.state.ts +++ b/frontend/src/app/features/administration/state/users.state.ts @@ -12,7 +12,7 @@ import '@app/framework/utils/rxjs-extensions'; import { EMPTY, Observable, of } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; import { DialogService, getPagingInfo, ListState, shareSubscribed, State } from '@app/shared'; -import { CreateUserDto, UpdateUserDto, UserDto, UsersService } from './../services/users.service'; +import { UpsertUserDto, UserDto, UsersService } from './../services/users.service'; interface Snapshot extends ListState { // The current users. @@ -133,7 +133,7 @@ export class UsersState extends State { shareSubscribed(this.dialogs)); } - public create(request: CreateUserDto): Observable { + public create(request: UpsertUserDto): Observable { return this.usersService.postUser(request).pipe( tap(created => { this.next(s => { @@ -145,7 +145,7 @@ export class UsersState extends State { shareSubscribed(this.dialogs, { silent: true })); } - public update(user: UserDto, request: UpdateUserDto): Observable { + public update(user: UserDto, request: Partial): Observable { return this.usersService.putUser(user, request).pipe( tap(updated => { this.replaceUser(updated); diff --git a/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts b/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts index 799db89ff..0c1272f89 100644 --- a/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts +++ b/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts @@ -31,7 +31,7 @@ export class GraphQLPageComponent implements AfterViewInit { public ngAfterViewInit() { const url = this.apiUrl.buildUrl(`api/content/${this.appsState.appName}/graphql`); - const subscriptionUrl = + const subscriptionUrl = url .replace('http://', 'ws://') .replace('https://', 'wss://') + diff --git a/frontend/src/app/features/content/pages/content/content-page.component.ts b/frontend/src/app/features/content/pages/content/content-page.component.ts index fd46780d8..3aa6cb175 100644 --- a/frontend/src/app/features/content/pages/content/content-page.component.ts +++ b/frontend/src/app/features/content/pages/content/content-page.component.ts @@ -78,7 +78,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD this.own( this.languagesState.isoLanguages - .subscribe(languages => { + .subscribe(languages => { this.languages = languages; })); diff --git a/frontend/src/app/features/content/pages/content/editor/content-field.component.ts b/frontend/src/app/features/content/pages/content/editor/content-field.component.ts index 521c60f27..dd87fb53c 100644 --- a/frontend/src/app/features/content/pages/content/editor/content-field.component.ts +++ b/frontend/src/app/features/content/pages/content/editor/content-field.component.ts @@ -113,7 +113,7 @@ export class ContentFieldComponent implements OnChanges { if (!master) { return; } - + const masterCode = master.iso2Code; const masterValue = this.formModel.get(masterCode)!.form.value; diff --git a/frontend/src/app/features/content/pages/contents/contents-page.component.ts b/frontend/src/app/features/content/pages/contents/contents-page.component.ts index 2e8200954..1a036109c 100644 --- a/frontend/src/app/features/content/pages/contents/contents-page.component.ts +++ b/frontend/src/app/features/content/pages/contents/contents-page.component.ts @@ -10,7 +10,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators'; -import { AppLanguageDto, AppsState, contentsTranslationStatus, ContentDto, ContentsState, ContributorsState, defined, LanguagesState, LocalStoreService, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, Settings, switchSafe, TableSettings, TempService, TranslationStatus, UIState } from '@app/shared'; +import { AppLanguageDto, AppsState, ContentDto, ContentsState, contentsTranslationStatus, ContributorsState, defined, LanguagesState, LocalStoreService, ModalModel, Queries, Query, QuerySynchronizer, ResourceOwner, Router2State, SchemaDto, SchemasService, SchemasState, Settings, switchSafe, TableSettings, TempService, TranslationStatus, UIState } from '@app/shared'; import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component'; @Component({ @@ -119,7 +119,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit { this.contentsState.contents .subscribe(contents => { this.updateSelectionSummary(); - + this.translationStatus = contentsTranslationStatus(contents.map(x => x.data), this.schema, this.languages); })); } diff --git a/frontend/src/app/features/content/shared/forms/array-editor.component.ts b/frontend/src/app/features/content/shared/forms/array-editor.component.ts index f677c71aa..aabbb8a4d 100644 --- a/frontend/src/app/features/content/shared/forms/array-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/array-editor.component.ts @@ -49,7 +49,7 @@ export class ArrayEditorComponent implements OnChanges { @ViewChildren(ArrayItemComponent) public children!: QueryList; - + @ViewChildren(VirtualScrollerComponent) public scroller?: QueryList; diff --git a/frontend/src/app/features/content/shared/forms/array-item.component.ts b/frontend/src/app/features/content/shared/forms/array-item.component.ts index 0c4fbd466..3bd4b14d7 100644 --- a/frontend/src/app/features/content/shared/forms/array-item.component.ts +++ b/frontend/src/app/features/content/shared/forms/array-item.component.ts @@ -143,7 +143,7 @@ export class ArrayItemComponent implements OnChanges { function getTitle(formModel: ObjectFormBase) { const value = formModel.form.value; const values: string[] = []; - + let valueLength = 0; if (Types.is(formModel, ComponentForm) && formModel.schema) { @@ -163,7 +163,7 @@ function getTitle(formModel: ObjectFormBase) { if (formatted) { values.push(formatted); valueLength += formatted.length; - + if (valueLength > 30) { break; } diff --git a/frontend/src/app/features/content/shared/forms/component.component.ts b/frontend/src/app/features/content/shared/forms/component.component.ts index 0a1160ded..49a919a17 100644 --- a/frontend/src/app/features/content/shared/forms/component.component.ts +++ b/frontend/src/app/features/content/shared/forms/component.component.ts @@ -58,7 +58,7 @@ export class ComponentComponent extends ResourceOwner implements OnChanges { public ngOnChanges(changes: SimpleChanges) { if (changes['formModel']) { this.unsubscribeAll(); - + this.isDisabled = disabled$(this.formModel.form); this.own( diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.html b/frontend/src/app/features/content/shared/forms/field-editor.component.html index 61484eb80..36a05dfe5 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.html +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.html @@ -34,7 +34,7 @@ [formValue]="form.valueChanges | async" [formIndex]="index" [formField]="formModel.field.name" - [language]="language?.iso2Code"> + [language]="language.iso2Code"> diff --git a/frontend/src/app/features/content/shared/references/reference-dropdown.component.ts b/frontend/src/app/features/content/shared/references/reference-dropdown.component.ts index c1fc1267f..4ca9cf1cf 100644 --- a/frontend/src/app/features/content/shared/references/reference-dropdown.component.ts +++ b/frontend/src/app/features/content/shared/references/reference-dropdown.component.ts @@ -201,7 +201,7 @@ export class ReferenceDropdownComponent extends StatefulControlComponent x !== MetaFields.empty); +const META_FIELD_NAMES = Object.values(META_FIELDS).filter(x => x !== META_FIELDS.empty); @Component({ selector: 'sqx-field-list[fieldNames][schema]', diff --git a/frontend/src/app/features/settings/pages/backups/backup.component.ts b/frontend/src/app/features/settings/pages/backups/backup.component.ts index 3e035c1d4..d2e509e72 100644 --- a/frontend/src/app/features/settings/pages/backups/backup.component.ts +++ b/frontend/src/app/features/settings/pages/backups/backup.component.ts @@ -21,7 +21,7 @@ export class BackupComponent implements OnChanges { public duration = ''; constructor( - public readonly apiUrl: ApiUrlConfig, + public readonly apiUrl: ApiUrlConfig, private readonly backupsState: BackupsState, ) { } diff --git a/frontend/src/app/features/settings/pages/templates/template.component.ts b/frontend/src/app/features/settings/pages/templates/template.component.ts index 6b5f5ee3a..46d4d0600 100644 --- a/frontend/src/app/features/settings/pages/templates/template.component.ts +++ b/frontend/src/app/features/settings/pages/templates/template.component.ts @@ -20,7 +20,7 @@ export class TemplateComponent implements OnChanges { public template!: TemplateDto; public isExpanded = false; - + public details?: Observable; constructor( @@ -44,14 +44,14 @@ export class TemplateComponent implements OnChanges { let details = dto.details.replace(//g, app); const client = this.clientsState.snapshot.clients[0]; - + if (client) { const clientId = `${app}:${client.id}`; - + details = details.replace(/\/g, clientId); details = details.replace(/\/g, client.secret); } - + return details; } } diff --git a/frontend/src/app/features/settings/pages/workflows/workflow.component.ts b/frontend/src/app/features/settings/pages/workflows/workflow.component.ts index 4318d65fa..f07e27b9f 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow.component.ts +++ b/frontend/src/app/features/settings/pages/workflows/workflow.component.ts @@ -58,7 +58,7 @@ export class WorkflowComponent implements OnChanges { .subscribe({ next: () => { this.error = null; - }, + }, error: (error: ErrorDto) => { this.error = error; }, @@ -79,7 +79,7 @@ export class WorkflowComponent implements OnChanges { } public rename(name: string) { - this.workflow = this.workflow.rename(name); + this.workflow = this.workflow.changeName(name); } public changeSchemaIds(schemaIds: string[]) { diff --git a/frontend/src/app/framework/angular/compensate-scrollbar.directive.ts b/frontend/src/app/framework/angular/compensate-scrollbar.directive.ts index c6c3b2b13..3a8fab029 100644 --- a/frontend/src/app/framework/angular/compensate-scrollbar.directive.ts +++ b/frontend/src/app/framework/angular/compensate-scrollbar.directive.ts @@ -16,7 +16,7 @@ export class CompensateScrollbarDirective extends ResourceOwner implements Resiz @Input('sqxCompensateScrollbar') public enabled?: string | boolean | null = true; - + constructor( private readonly renderer: Renderer2, private readonly element: ElementRef, diff --git a/frontend/src/app/framework/angular/forms/editors/code-editor.component.ts b/frontend/src/app/framework/angular/forms/editors/code-editor.component.ts index 1101bebac..23b578851 100644 --- a/frontend/src/app/framework/angular/forms/editors/code-editor.component.ts +++ b/frontend/src/app/framework/angular/forms/editors/code-editor.component.ts @@ -107,7 +107,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im } catch { this.value = ''; } - + if (this.aceEditor) { this.setValue(this.value); } diff --git a/frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts b/frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts index c8d0a7497..8510eb9c8 100644 --- a/frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts +++ b/frontend/src/app/framework/angular/forms/editors/tag-editor.stories.ts @@ -110,7 +110,7 @@ Suggestions.args = { export const SuggestionsEmpty = Template.bind({}); SuggestionsEmpty.args = { - suggestions: [], + suggestions: [], allowOpen: true, }; diff --git a/frontend/src/app/framework/angular/if-once.directive.ts b/frontend/src/app/framework/angular/if-once.directive.ts index 5659a048e..bd7bd418f 100644 --- a/frontend/src/app/framework/angular/if-once.directive.ts +++ b/frontend/src/app/framework/angular/if-once.directive.ts @@ -17,7 +17,7 @@ export class IfOnceDirective { public set condition(value: boolean) { if (value && !this.hasView) { this.viewContainer.createEmbeddedView(this.templateRef); - + this.hasView = true; } } diff --git a/frontend/src/app/framework/internal.ts b/frontend/src/app/framework/internal.ts index 37992b92b..166b4700d 100644 --- a/frontend/src/app/framework/internal.ts +++ b/frontend/src/app/framework/internal.ts @@ -10,7 +10,6 @@ export * from './angular/drag-helper'; export * from './angular/routers/router-utils'; export * from './angular/stateful.component'; export * from './configurations'; -export * from './services/analytics.service'; export * from './services/clipboard.service'; export * from './services/dialog.service'; export * from './services/loading.service'; diff --git a/frontend/src/app/framework/module.ts b/frontend/src/app/framework/module.ts index 424231312..63ee87095 100644 --- a/frontend/src/app/framework/module.ts +++ b/frontend/src/app/framework/module.ts @@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { ColorPickerModule } from 'ngx-color-picker'; -import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, CompensateScrollbarDirective, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, IfOnceDirective, ImageSourceDirective, ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, JoinPipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoaderComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, RadioGroupComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations'; +import { AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, CompensateScrollbarDirective, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, IfOnceDirective, ImageSourceDirective, ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, JoinPipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoaderComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, RadioGroupComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations'; @NgModule({ imports: [ @@ -205,7 +205,6 @@ export class SqxFrameworkModule { return { ngModule: SqxFrameworkModule, providers: [ - AnalyticsService, CanDeactivateGuard, ClipboardService, DialogService, diff --git a/frontend/src/app/framework/services/analytics.service.ts b/frontend/src/app/framework/services/analytics.service.ts deleted file mode 100644 index 61094be24..000000000 --- a/frontend/src/app/framework/services/analytics.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Injectable } from '@angular/core'; -import { NavigationEnd, Router } from '@angular/router'; -import { filter } from 'rxjs'; -import { UIOptions } from './../configurations'; -import { Types } from './../utils/types'; -import { ResourceLoaderService } from './resource-loader.service'; - -@Injectable() -export class AnalyticsService { - private readonly gtag: any; - private readonly analyticsId?: string; - - constructor( - private readonly uiOptions?: UIOptions, - private readonly router?: Router, - private readonly resourceLoader?: ResourceLoaderService, - ) { - window['dataLayer'] = window['dataLayer'] || []; - - // eslint-disable-next-line func-names - this.gtag = function () { - // eslint-disable-next-line prefer-rest-params - window['dataLayer'].push(arguments); - }; - - if (this.uiOptions) { - this.analyticsId = this.uiOptions.get('google.analyticsId'); - } - - this.configureGtag(); - } - - public trackEvent(category: string, action: string, label?: string, value?: number) { - this.gtag('event', 'user-action', { - event_category: category, - event_action: action, - event_label: label, - value, - }); - } - - private configureGtag() { - if (this.analyticsId && this.router && this.resourceLoader && window.location.hostname !== 'localhost') { - this.gtag('config', this.analyticsId, { anonymize_ip: true }); - - this.router.events.pipe( - filter(event => Types.is(event, NavigationEnd))) - .subscribe(() => { - this.gtag('config', this.analyticsId, { page_path: window.location.pathname, anonymize_ip: true }); - }); - - this.loadScript(); - } - } - - private loadScript() { - if (document.cookie.indexOf('ga-disable') < 0 && this.resourceLoader) { - this.resourceLoader.loadScript(`https://www.googletagmanager.com/gtag/js?id=${this.analyticsId}`); - } - } -} diff --git a/frontend/src/app/framework/services/dialog.service.ts b/frontend/src/app/framework/services/dialog.service.ts index 14a0991f9..2dc611208 100644 --- a/frontend/src/app/framework/services/dialog.service.ts +++ b/frontend/src/app/framework/services/dialog.service.ts @@ -65,7 +65,7 @@ export class Tooltip { public get offsetX() { return this.isHorizontal ? 6 : 0; } - + public get offsetY() { return this.isHorizontal ? 0 : 6; } diff --git a/frontend/src/app/framework/state.ts b/frontend/src/app/framework/state.ts index 85d11c264..b0935bc7b 100644 --- a/frontend/src/app/framework/state.ts +++ b/frontend/src/app/framework/state.ts @@ -7,7 +7,6 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; -import { ResourceLinks } from './utils/hateos'; import { Types } from './utils/types'; export type Mutable = { @@ -44,18 +43,6 @@ export class Model { } } -export class ResultSet { - public readonly _links: ResourceLinks; - - constructor( - public readonly total: number, - public readonly items: ReadonlyArray, - links?: ResourceLinks, - ) { - this._links = links || {}; - } -} - export interface PagingInfo { // The current page. page: number; @@ -101,7 +88,7 @@ const devToolsExtension = window['__REDUX_DEVTOOLS_EXTENSION__']; export class State { private readonly state: BehaviorSubject>; private readonly devTools?: any; - + public get changes(): Observable> { return this.state; } @@ -135,8 +122,8 @@ export class State { const name = `[Squidex] ${debugName}`; this.devTools = devToolsExtension.connect({ name, features: { jump: true } }); - this.devTools.init(initialState); - + this.devTools.init(initialState); + this.devTools.subscribe((message: any) => { if (message.type === 'DISPATCH' && message.payload.type === 'JUMP_TO_ACTION') { this.state.next(JSON.parse(message.state)); diff --git a/frontend/src/app/framework/utils/modal-positioner.ts b/frontend/src/app/framework/utils/modal-positioner.ts index 9f962cfdd..6c52f14f7 100644 --- a/frontend/src/app/framework/utils/modal-positioner.ts +++ b/frontend/src/app/framework/utils/modal-positioner.ts @@ -13,7 +13,7 @@ export type AnchorX = 'left-to-left' | 'right-to-left' | 'right-to-right'; - + export type AnchorY = 'bottom-to-bottom' | 'bottom-to-top' | diff --git a/frontend/src/app/framework/utils/text-measurer.ts b/frontend/src/app/framework/utils/text-measurer.ts index 60c5e1afb..74653d57b 100644 --- a/frontend/src/app/framework/utils/text-measurer.ts +++ b/frontend/src/app/framework/utils/text-measurer.ts @@ -35,7 +35,7 @@ export class TextMeasurer { } const style = window.getComputedStyle(currentElement); - + const fontSize = style.getPropertyValue('font-size'); const fontFamily = style.getPropertyValue('font-family'); @@ -45,7 +45,7 @@ export class TextMeasurer { this.font = `${fontSize} ${fontFamily}`; } - + if (!this.font) { return -1000; } diff --git a/frontend/src/app/shared/components/app-form.component.ts b/frontend/src/app/shared/components/app-form.component.ts index 0802a8972..8cc6feffe 100644 --- a/frontend/src/app/shared/components/app-form.component.ts +++ b/frontend/src/app/shared/components/app-form.component.ts @@ -38,7 +38,7 @@ export class AppFormComponent { if (value) { const request = { ...value, template: this.template?.name }; - + this.appsStore.create(request) .subscribe({ next: () => { diff --git a/frontend/src/app/shared/components/contents/content-list-cell.directive.ts b/frontend/src/app/shared/components/contents/content-list-cell.directive.ts index b8404d696..70a7ed9e5 100644 --- a/frontend/src/app/shared/components/contents/content-list-cell.directive.ts +++ b/frontend/src/app/shared/components/contents/content-list-cell.directive.ts @@ -7,7 +7,7 @@ import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, Pipe, PipeTransform, Renderer2 } from '@angular/core'; import { ResourceOwner } from '@app/framework'; -import { ContentDto, FieldSizes, MetaFields, TableField, TableSettings } from '@app/shared/internal'; +import { ContentDto, FieldSizes, META_FIELDS, TableField, TableSettings } from '@app/shared/internal'; export function getCellWidth(field: TableField, sizes: FieldSizes | undefined | null) { const size = sizes?.[field.name] || 0; @@ -17,27 +17,27 @@ export function getCellWidth(field: TableField, sizes: FieldSizes | undefined | } switch (field) { - case MetaFields.id: + case META_FIELDS.id: return 280; - case MetaFields.created: + case META_FIELDS.created: return 150; - case MetaFields.createdByAvatar: + case META_FIELDS.createdByAvatar: return 55; - case MetaFields.createdByName: + case META_FIELDS.createdByName: return 150; - case MetaFields.lastModified: + case META_FIELDS.lastModified: return 150; - case MetaFields.lastModifiedByAvatar: + case META_FIELDS.lastModifiedByAvatar: return 55; - case MetaFields.lastModifiedByName: + case META_FIELDS.lastModifiedByName: return 150; - case MetaFields.status: + case META_FIELDS.status: return 200; - case MetaFields.statusNext: + case META_FIELDS.statusNext: return 240; - case MetaFields.statusColor: + case META_FIELDS.statusColor: return 50; - case MetaFields.version: + case META_FIELDS.version: return 80; default: return 200; @@ -96,7 +96,7 @@ export class ContentListWidthDirective extends ResourceOwner implements OnChange if (!this.fields) { return; } - + let size = 100; for (const field of this.fields) { @@ -106,7 +106,7 @@ export class ContentListWidthDirective extends ResourceOwner implements OnChange if (size === this.size) { return; } - + const width = `${size}px`; this.renderer.setStyle(this.element.nativeElement, 'min-width', width); @@ -151,13 +151,13 @@ export class ContentListCellDirective extends ResourceOwner implements OnChanges if (!this.field.name) { return; } - + const size = getCellWidth(this.field, this.sizes); if (size === this.size) { return; } - + const width = `${size}px`; this.renderer.setStyle(this.element.nativeElement, 'min-width', width); @@ -204,11 +204,11 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy { this.mouseDown = undefined; this.mouseBlur?.(); this.mouseBlur = undefined; - + this.resetMovement(); } - - public ngOnInit() { + + public ngOnInit() { if (!this.tableFields || !this.fieldName) { return; } @@ -221,7 +221,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy { this.mouseDown = this.renderer.listen(this.resizer, 'mousedown', this.onMouseDown); this.mouseBlur = this.renderer.listen(this.resizer, 'blur', this.onMouseUp); } - + private onMouseDown = (event: MouseEvent) => { if (!this.tableFields || !this.fieldName) { return; @@ -236,7 +236,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy { this.startOffset = event.pageX; this.startWidth = this.element.nativeElement.offsetWidth; }; - + private onMouseMove = (event: MouseEvent) => { if (!this.mouseMove || !this.tableFields || !this.fieldName) { return; @@ -248,7 +248,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy { this.resetMovement(); } }; - + private onMouseUp = (event: MouseEvent) => { if (!this.mouseMove || !this.tableFields || !this.fieldName) { return; @@ -271,7 +271,7 @@ export class ContentListCellResizeDirective implements OnInit, OnDestroy { if (width < this.minimumWidth) { width = this.minimumWidth; } - + this.tableFields!.updateSize(this.fieldName!, width, save); } diff --git a/frontend/src/app/shared/components/contents/content-list-field.component.ts b/frontend/src/app/shared/components/contents/content-list-field.component.ts index f35e572be..14ab66b36 100644 --- a/frontend/src/app/shared/components/contents/content-list-field.component.ts +++ b/frontend/src/app/shared/components/contents/content-list-field.component.ts @@ -7,7 +7,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { ContentDto, FieldValue, getContentValue, LanguageDto, MetaFields, StatefulComponent, TableField, TableSettings } from '@app/shared/internal'; +import { ContentDto, FieldValue, getContentValue, LanguageDto, META_FIELDS, StatefulComponent, TableField, TableSettings } from '@app/shared/internal'; interface State { // The formatted value. @@ -21,7 +21,7 @@ interface State { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ContentListFieldComponent extends StatefulComponent implements OnChanges { - public readonly metaFields = MetaFields; + public readonly metaFields = META_FIELDS; @Input() public field!: TableField; diff --git a/frontend/src/app/shared/components/contents/content-list-header.component.ts b/frontend/src/app/shared/components/contents/content-list-header.component.ts index 7e23476db..9f3f47335 100644 --- a/frontend/src/app/shared/components/contents/content-list-header.component.ts +++ b/frontend/src/app/shared/components/contents/content-list-header.component.ts @@ -6,7 +6,7 @@ */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; -import { LanguageDto, MetaFields, Query, SortMode, TableField } from '@app/shared/internal'; +import { LanguageDto, META_FIELDS, Query, SortMode, TableField } from '@app/shared/internal'; @Component({ selector: 'sqx-content-list-header[field][language]', @@ -15,7 +15,7 @@ import { LanguageDto, MetaFields, Query, SortMode, TableField } from '@app/share changeDetection: ChangeDetectionStrategy.OnPush, }) export class ContentListHeaderComponent implements OnChanges { - public readonly metaFields = MetaFields; + public readonly metaFields = META_FIELDS; @Input() public field!: TableField; @@ -35,9 +35,9 @@ export class ContentListHeaderComponent implements OnChanges { public ngOnChanges() { const { field, language } = this; - if (field === MetaFields.created) { + if (field === META_FIELDS.created) { this.sortPath = 'created'; - } else if (field === MetaFields.lastModified) { + } else if (field === META_FIELDS.lastModified) { this.sortPath = 'lastModified'; } else if (field.rootField?.properties.isSortable !== true) { this.sortPath = undefined; @@ -47,7 +47,7 @@ export class ContentListHeaderComponent implements OnChanges { this.sortPath = `data.${field.name}.iv`; } - if (field === MetaFields.lastModified) { + if (field === META_FIELDS.lastModified) { this.sortDefault = 'descending'; } } diff --git a/frontend/src/app/shared/components/forms/markdown-editor.component.ts b/frontend/src/app/shared/components/forms/markdown-editor.component.ts index 6c8ecc84b..2f2df9bfc 100644 --- a/frontend/src/app/shared/components/forms/markdown-editor.component.ts +++ b/frontend/src/app/shared/components/forms/markdown-editor.component.ts @@ -347,7 +347,7 @@ export class MarkdownEditorComponent extends StatefulControlComponent im .defined() .join(', ') || 'content'; - + return `${name}`; } diff --git a/frontend/src/app/shared/components/pipes.ts b/frontend/src/app/shared/components/pipes.ts index 79f2b3b26..e69652007 100644 --- a/frontend/src/app/shared/components/pipes.ts +++ b/frontend/src/app/shared/components/pipes.ts @@ -72,7 +72,7 @@ export class UserNamePipe extends UserAsyncPipe implements OnDestroy, PipeTransf } public transform(userId: string | undefined | null, placeholder = 'Me') { - return super.transformInternal(userId, (users, userId) => + return super.transformInternal(userId, (users, userId) => users.getUser(userId, placeholder).pipe(map(u => u.displayName))); } } diff --git a/frontend/src/app/shared/components/references/content-selector-item.component.ts b/frontend/src/app/shared/components/references/content-selector-item.component.ts index b5709f9be..0ecd36a9e 100644 --- a/frontend/src/app/shared/components/references/content-selector-item.component.ts +++ b/frontend/src/app/shared/components/references/content-selector-item.component.ts @@ -8,7 +8,7 @@ /* tslint:disable: component-selector */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { ContentDto, LanguageDto, MetaFields, SchemaDto } from '@app/shared/internal'; +import { ContentDto, LanguageDto, META_FIELDS, SchemaDto } from '@app/shared/internal'; @Component({ selector: '[sqxContentSelectorItem][language][schema]', @@ -17,7 +17,7 @@ import { ContentDto, LanguageDto, MetaFields, SchemaDto } from '@app/shared/inte changeDetection: ChangeDetectionStrategy.OnPush, }) export class ContentSelectorItemComponent { - public readonly metaFields = MetaFields; + public readonly metaFields = META_FIELDS; @Output() public selectedChange = new EventEmitter(); diff --git a/frontend/src/app/shared/components/references/content-selector.component.ts b/frontend/src/app/shared/components/references/content-selector.component.ts index 69b23b5f2..8bc596c2d 100644 --- a/frontend/src/app/shared/components/references/content-selector.component.ts +++ b/frontend/src/app/shared/components/references/content-selector.component.ts @@ -8,7 +8,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { BehaviorSubject, of } from 'rxjs'; import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; -import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDto, MetaFields, Query, ResourceOwner, SchemaDto, SchemasService, SchemasState } from '@app/shared/internal'; +import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDto, META_FIELDS, Query, ResourceOwner, SchemaDto, SchemasService, SchemasState } from '@app/shared/internal'; @Component({ selector: 'sqx-content-selector[language][languages]', @@ -19,8 +19,8 @@ import { ApiUrlConfig, AppsState, ComponentContentsState, ContentDto, LanguageDt ], }) export class ContentSelectorComponent extends ResourceOwner implements OnInit { - public readonly metaFields = MetaFields; - + public readonly metaFields = META_FIELDS; + @Output() public select = new EventEmitter>(); diff --git a/frontend/src/app/shared/components/references/reference-input.component.ts b/frontend/src/app/shared/components/references/reference-input.component.ts index 992ea2b14..16b63d2d6 100644 --- a/frontend/src/app/shared/components/references/reference-input.component.ts +++ b/frontend/src/app/shared/components/references/reference-input.component.ts @@ -106,7 +106,7 @@ export class ReferenceInputComponent extends StatefulControlComponent { const version = new Version('1'); @@ -20,7 +20,6 @@ describe('AppLanguagesService', () => { providers: [ AppLanguagesService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -155,9 +154,6 @@ describe('AppLanguagesService', () => { export function createLanguages(...codes: ReadonlyArray): AppLanguagesPayload { return { items: codes.map((code, i) => createLanguage(code, codes, i)), - _links: { - create: { method: 'POST', href: '/languages' }, - }, canCreate: true, }; } diff --git a/frontend/src/app/shared/services/app-languages.service.ts b/frontend/src/app/shared/services/app-languages.service.ts index 73dbf02db..dd36f0ac8 100644 --- a/frontend/src/app/shared/services/app-languages.service.ts +++ b/frontend/src/app/shared/services/app-languages.service.ts @@ -8,17 +8,15 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; +import { ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; export class AppLanguageDto { public readonly _links: ResourceLinks; - public readonly canUpdate: boolean; public readonly canDelete: boolean; + public readonly canUpdate: boolean; - constructor( - links: ResourceLinks, + constructor(links: ResourceLinks, public readonly iso2Code: string, public readonly englishName: string, public readonly isMaster: boolean, @@ -27,29 +25,42 @@ export class AppLanguageDto { ) { this._links = links; - this.canUpdate = hasAnyLink(links, 'update'); this.canDelete = hasAnyLink(links, 'delete'); + this.canUpdate = hasAnyLink(links, 'update'); } } -export type AppLanguagesDto = - Versioned; +export type AppLanguagesDto = Versioned; + +export type AppLanguagesPayload = Readonly<{ + // The app languages. + items: ReadonlyArray; + + // The if the user can create a new language. + canCreate?: boolean; +}>; -export type AppLanguagesPayload = - Readonly<{ items: ReadonlyArray; canCreate: boolean } & Resource>; +export type AddAppLanguageDto = Readonly<{ + // The language code to add. + language: string; +}>; -export type AddAppLanguageDto = - Readonly<{ language: string }>; +export type UpdateAppLanguageDto = Readonly<{ + // Indicates if the language is the master language. + isMaster?: boolean; -export type UpdateAppLanguageDto = - Readonly<{ isMaster?: boolean; isOptional?: boolean; falback?: ReadonlyArray }>; + // Indicates if the langauge is optional (cannot be master language). + isOptional?: boolean; + + // The fallback language codes. + falback?: ReadonlyArray; +}>; @Injectable() export class AppLanguagesService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -70,9 +81,6 @@ export class AppLanguagesService { mapVersioned(({ body }) => { return parseLanguages(body); }), - tap(() => { - this.analytics.trackEvent('Language', 'Added', appName); - }), pretifyError('i18n:languages.addFailed')); } @@ -85,9 +93,6 @@ export class AppLanguagesService { mapVersioned(({ body }) => { return parseLanguages(body); }), - tap(() => { - this.analytics.trackEvent('Language', 'Updated', appName); - }), pretifyError('i18n:languages.updateFailed')); } @@ -100,23 +105,24 @@ export class AppLanguagesService { mapVersioned(({ body }) => { return parseLanguages(body); }), - tap(() => { - this.analytics.trackEvent('Language', 'Deleted', appName); - }), pretifyError('i18n:languages.deleteFailed')); } } -function parseLanguages(response: { items: any[] } & Resource) { - const items = response.items.map(item => - new AppLanguageDto(item._links, - item.iso2Code, - item.englishName, - item.isMaster, - item.isOptional, - item.fallback || [])); +function parseLanguages(response: { items: any[] } & Resource): AppLanguagesPayload { + const { items: list, _links } = response; + const items = list.map(parseLanguage); + + const canCreate = hasAnyLink(_links, 'create'); - const { _links } = response; + return { items, canCreate }; +} - return { items, _links, canCreate: hasAnyLink(_links, 'create') }; +function parseLanguage(response: any) { + return new AppLanguageDto(response._links, + response.iso2Code, + response.englishName, + response.isMaster, + response.isOptional, + response.fallback || []); } diff --git a/frontend/src/app/shared/services/apps.service.spec.ts b/frontend/src/app/shared/services/apps.service.spec.ts index 4dc62307d..a22c55350 100644 --- a/frontend/src/app/shared/services/apps.service.spec.ts +++ b/frontend/src/app/shared/services/apps.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, AppDto, AppsService, DateTime, ErrorDto, Resource, ResourceLinks, Version } from '@app/shared/internal'; +import { ApiUrlConfig, AppDto, AppsService, DateTime, ErrorDto, Resource, ResourceLinks, Version } from '@app/shared/internal'; import { AppSettingsDto, AssetScriptsDto, AssetScriptsPayload, EditorDto, PatternDto } from './apps.service'; describe('AppsService', () => { @@ -21,7 +21,6 @@ describe('AppsService', () => { providers: [ AppsService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -326,11 +325,11 @@ describe('AppsService', () => { lastModifiedBy: `modifier${id}`, version: key, name: `app-name${key}`, - label: `app-label${key}`, - description: `app-description${key}`, - permissions: ['Owner'], canAccessApi: id % 2 === 0, canAccessContent: id % 2 === 0, + description: `app-description${key}`, + label: `app-label${key}`, + permissions: ['Owner'], roleName: `Role${id}`, roleProperties: createProperties(id), _links: { @@ -418,12 +417,8 @@ export function createAppSettings(id: number, suffix = '') { } export function createAssetScripts(id: number, suffix = ''): AssetScriptsPayload { - const key = `${id}${suffix}`; - return { - scripts: { - update: key, - }, + scripts: { update: `${id}${suffix}` }, _links: { update: { method: 'PUT', href: `apps/${id}/assets/scripts` }, }, diff --git a/frontend/src/app/shared/services/apps.service.ts b/frontend/src/app/shared/services/apps.service.ts index fceb9ed6d..92d365848 100644 --- a/frontend/src/app/shared/services/apps.service.ts +++ b/frontend/src/app/shared/services/apps.service.ts @@ -8,8 +8,8 @@ import { HttpClient, HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; -import { catchError, filter, map, tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, DateTime, ErrorDto, getLinkUrl, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, StringHelper, Types, Version, Versioned } from '@app/framework'; +import { catchError, filter, map } from 'rxjs/operators'; +import { ApiUrlConfig, DateTime, ErrorDto, getLinkUrl, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, StringHelper, Types, Version, Versioned } from '@app/framework'; export class AppDto { public readonly _links: ResourceLinks; @@ -31,6 +31,7 @@ export class AppDto { public readonly canUpdateGeneral: boolean; public readonly canUpdateImage: boolean; public readonly canUploadAssets: boolean; + public readonly image: string; public readonly displayName: string; @@ -111,30 +112,47 @@ export class EditorDto { } } -export type AssetScriptsDto = - Versioned; +export type AssetScripts = Readonly<{ [name: string]: string | null }>; + +export type AssetScriptsDto = Versioned; + +export type AssetScriptsPayload = Readonly<{ + // The actual asset scripts. + scripts: AssetScripts; + + // True, if the user has permissions to update the scripts. + canUpdate?: boolean; +}> & Resource; -export type AssetScriptsPayload = - Readonly<{ scripts: AssetScripts; canUpdate: boolean } & Resource>; +export type UpdateAppSettingsDto = Readonly<{ + // The regex patterns for scehams. + patterns: ReadonlyArray; -export type UpdateAppSettingsDto = - Readonly<{ patterns: ReadonlyArray; editors: ReadonlyArray; hideScheduler?: boolean }>; + // The registered editors for schemas. + editors: ReadonlyArray; -export type AssetScripts = - Readonly<{ [name: string]: string | null }>; + // True if the scheduler should be hidden. + hideScheduler?: boolean; +}>; -export type CreateAppDto = - Readonly<{ name: string }>; +export type CreateAppDto = Readonly<{ + // The new name of the app. Must be unique. + name: string; +}>; -export type UpdateAppDto = - Readonly<{ label?: string; description?: string }>; +export type UpdateAppDto = Readonly<{ + // The label, which is like a display name. + label?: string; + + // The description of the app. + description?: string; +}>; @Injectable() export class AppsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -169,9 +187,6 @@ export class AppsService { map(body => { return parseApp(body); }), - tap(() => { - this.analytics.trackEvent('App', 'Created', dto.name); - }), pretifyError('i18n:apps.createFailed')); } @@ -184,9 +199,6 @@ export class AppsService { map(({ payload }) => { return parseApp(payload.body); }), - tap(() => { - this.analytics.trackEvent('App', 'Updated', appName); - }), pretifyError('i18n:apps.updateFailed')); } @@ -209,9 +221,6 @@ export class AppsService { map(({ payload }) => { return parseAppSettings(payload.body); }), - tap(() => { - this.analytics.trackEvent('App', 'Updated', appName); - }), pretifyError('i18n:apps.updateSettingsFailed')); } @@ -234,9 +243,6 @@ export class AppsService { mapVersioned(({ body }) => { return parseAssetScripts(body); }), - tap(() => { - this.analytics.trackEvent('App', 'Updated', appName); - }), pretifyError('i18n:apps.updateAssetScriptsFailed')); } @@ -267,11 +273,6 @@ export class AppsService { return throwError(() => error); } }), - tap(value => { - if (!Types.isNumber(value)) { - this.analytics.trackEvent('AppImage', 'Uploaded', appName); - } - }), pretifyError('i18n:apps.uploadImageFailed')); } @@ -284,9 +285,6 @@ export class AppsService { map(({ payload }) => { return parseApp(payload.body); }), - tap(() => { - this.analytics.trackEvent('AppImage', 'Removed', appName); - }), pretifyError('i18n:apps.removeImageFailed')); } @@ -296,9 +294,6 @@ export class AppsService { const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url).pipe( - tap(() => { - this.analytics.trackEvent('App', 'Left', appName); - }), pretifyError('i18n:apps.leaveFailed')); } @@ -308,9 +303,6 @@ export class AppsService { const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url).pipe( - tap(() => { - this.analytics.trackEvent('App', 'Archived', appName); - }), pretifyError('i18n:apps.archiveFailed')); } } @@ -343,8 +335,10 @@ function parseAppSettings(response: any & Resource) { new Version(response.version.toString())); } -function parseAssetScripts(response: any) { +function parseAssetScripts(response: any): AssetScriptsPayload { const { _links, ...scripts } = response; - return { scripts, _links, canUpdate: hasAnyLink(_links, 'update') }; + const canUpdate = hasAnyLink(_links, 'update'); + + return { scripts, canUpdate, _links }; } diff --git a/frontend/src/app/shared/services/assets.service.spec.ts b/frontend/src/app/shared/services/assets.service.spec.ts index 8f221b334..5ccef9cbe 100644 --- a/frontend/src/app/shared/services/assets.service.spec.ts +++ b/frontend/src/app/shared/services/assets.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, AssetCompletions, AssetDto, AssetFolderDto, AssetFoldersDto, AssetsDto, AssetsService, DateTime, ErrorDto, MathHelper, Resource, ResourceLinks, sanitize, Version } from '@app/shared/internal'; +import { ApiUrlConfig, AssetCompletions, AssetDto, AssetFolderDto, AssetFoldersDto, AssetsDto, AssetsService, DateTime, ErrorDto, MathHelper, Resource, ResourceLinks, sanitize, Version } from '@app/shared/internal'; describe('AssetsService', () => { const version = new Version('1'); @@ -20,7 +20,6 @@ describe('AssetsService', () => { providers: [ AssetsService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -110,11 +109,15 @@ describe('AssetsService', () => { ], }); - expect(assets!).toEqual( - new AssetsDto(10, [ + expect(assets!).toEqual({ + items: [ createAsset(12), createAsset(13), - ])); + ], + total: 10, + canCreate: false, + canRenameTag: false, + }); })); it('should make get request to get asset folders', @@ -141,13 +144,16 @@ describe('AssetsService', () => { ], }); - expect(assetFolders!).toEqual( - new AssetFoldersDto(10, [ + expect(assetFolders!).toEqual({ + items: [ createAssetFolder(22), createAssetFolder(23), - ], [ + ], + path: [ createAssetFolder(44), - ])); + ], + canCreate: false, + }); })); it('should make get request to get asset', diff --git a/frontend/src/app/shared/services/assets.service.ts b/frontend/src/app/shared/services/assets.service.ts index e88acdd72..78f1983a2 100644 --- a/frontend/src/app/shared/services/assets.service.ts +++ b/frontend/src/app/shared/services/assets.service.ts @@ -8,8 +8,8 @@ import { HttpClient, HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; -import { catchError, filter, map, tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, DateTime, ErrorDto, getLinkUrl, hasAnyLink, HTTP, Metadata, pretifyError, Resource, ResourceLinks, ResultSet, StringHelper, Types, Version, Versioned } from '@app/framework'; +import { catchError, filter, map } from 'rxjs/operators'; +import { ApiUrlConfig, DateTime, ErrorDto, getLinkUrl, hasAnyLink, HTTP, Metadata, pretifyError, Resource, ResourceLinks, StringHelper, Types, Version, Versioned } from '@app/framework'; import { AuthService } from './auth.service'; import { Query, sanitize } from './query'; @@ -18,15 +18,7 @@ const SVG_PREVIEW_LIMIT = 10 * 1024; const MIME_TIFF = 'image/tiff'; const MIME_SVG = 'image/svg+xml'; -export class AssetsDto extends ResultSet { - public get canCreate() { - return hasAnyLink(this._links, 'create'); - } - - public get canRenameTag() { - return hasAnyLink(this._links, 'tags/rename'); - } -} +type AssetFolderScope = 'PathAndItems' | 'Path' | 'Items'; export class AssetDto { public readonly _meta: Metadata = {}; @@ -103,19 +95,6 @@ export class AssetDto { } } -export class AssetFoldersDto extends ResultSet { - constructor(total: number, items: ReadonlyArray, - public readonly path: ReadonlyArray, - links?: ResourceLinks, - ) { - super(total, items, links); - } - - public get canCreate() { - return hasAnyLink(this._links, 'create'); - } -} - export class AssetFolderDto { public readonly _links: ResourceLinks; @@ -137,47 +116,119 @@ export class AssetFolderDto { } } -type Tags = readonly string[]; +export type AssetsDto = Readonly<{ + // The list of assets. + items: ReadonlyArray; -type AssetFolderScope = 'PathAndItems' | 'Path' | 'Items'; -type AssetMetadata = { [key: string]: any }; + // The total number of assets. + total: number; + + // True, if the user has permissions to create an asset. + canCreate?: boolean; + + // True, if the user has permissions to rename a tag. + canRenameTag?: boolean; +}>; + +export type AssetFoldersDto = Readonly<{ + // The list of asset folders. + items: ReadonlyArray; + + // The path to the asset folders. + path: ReadonlyArray; + + // True, if the user has permissions to create an asset folder. + canCreate?: boolean; +}>; + +export type AssetCompletions = ReadonlyArray<{ + // The autocompletion path. + path: string; + + // The description of the autocompletion field. + description: string; + + // The type of the autocompletion field. + type: string; + }>; -export type AssetCompletions = - ReadonlyArray<{ path: string; description: string; type: string }>; +export type AnnotateAssetDto = Readonly<{ + // The optional file name. + fileName?: string; -export type AnnotateAssetDto = - Readonly<{ fileName?: string; isProtected?: boolean; slug?: string; tags?: Tags; metadata?: AssetMetadata }>; + // The optional flag, if an asset is protected. + isProtected?: boolean; -export type CreateAssetFolderDto = - Readonly<{ folderName: string } & MoveAssetItemDto>; + // The optiona slug. + slug?: string; -export type RenameAssetFolderDto = - Readonly<{ folderName: string }>; + // The optional tags. + tags?: ReadonlyArray; -export type RenameAssetTagDto = - Readonly<{ tagName: string }>; + // The optional metadata. + metadata?: { [key: string]: any }; +}>; -export type MoveAssetItemDto = - Readonly<{ parentId?: string }>; +export type CreateAssetFolderDto = Readonly<{ + // The name of the folder. + folderName: string; +} & MoveAssetItemDto>; -export type AssetsQuery = - Readonly<{ noTotal?: boolean; noSlowTotal?: boolean }>; +export type RenameAssetFolderDto = Readonly<{ + // The name of the folder. + folderName: string; +}>; -export type AssetsByRef = - Readonly<{ ref: string }>; +export type RenameAssetTagDto = Readonly<{ + // The name of the tag. + tagName: string; +}>; -export type AssetsByIds = - Readonly<{ ids: ReadonlyArray }>; +export type MoveAssetItemDto = Readonly<{ + // The new ID to the asset folder. + parentId?: string; +}>; -export type AssetsByQuery = - Readonly<{ query?: Query; skip?: number; tags?: Tags; take?: number; parentId?: string }>; +export type AssetsQuery = Readonly<{ + // True, to not return the total number of items. + noTotal?: boolean; + + // True, to not return the total number of items, if the query would be slow. + noSlowTotal?: boolean; +}>; + +export type AssetsByRef = Readonly<{ + // The reference. + ref: string; +}>; + +export type AssetsByIds = Readonly<{ + // The IDs of the assets. + ids: ReadonlyArray; +}>; + +export type AssetsByQuery = Readonly<{ + // The JSON query. + query?: Query; + + // The number of items to skip. + skip?: number; + + // The number of items to take. + take?: number; + + // The tags to filter. + tags?: ReadonlyArray; + + // The ID of the asset folder. + parentId?: string; +}>; @Injectable() export class AssetsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -256,11 +307,6 @@ export class AssetsService { return throwError(() => error); } }), - tap(value => { - if (!Types.isNumber(value)) { - this.analytics.trackEvent('Asset', 'Uploaded', appName); - } - }), pretifyError('i18n:assets.uploadFailed')); } @@ -291,11 +337,6 @@ export class AssetsService { return throwError(() => error); } }), - tap(value => { - if (!Types.isNumber(value)) { - this.analytics.trackEvent('Asset', 'Replaced', appName); - } - }), pretifyError('i18n:assets.replaceFailed')); } @@ -306,9 +347,6 @@ export class AssetsService { map(({ payload }) => { return parseAssetFolder(payload.body); }), - tap(() => { - this.analytics.trackEvent('AssetFolder', 'Updated', appName); - }), pretifyError('i18n:assets.createFolderFailed')); } @@ -321,9 +359,6 @@ export class AssetsService { map(({ payload }) => { return parseAsset(payload.body); }), - tap(() => { - this.analytics.trackEvent('Asset', 'Updated', appName); - }), pretifyError('i18n:assets.updateFailed')); } @@ -336,9 +371,6 @@ export class AssetsService { map(({ payload }) => { return parseAssetFolder(payload.body); }), - tap(() => { - this.analytics.trackEvent('AssetFolder', 'Updated', appName); - }), pretifyError('i18n:assets.updateFolderFailed')); } @@ -348,9 +380,6 @@ export class AssetsService { const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( - tap(() => { - this.analytics.trackEvent('Asset', 'Moved', appName); - }), pretifyError('i18n:assets.moveFailed')); } @@ -360,9 +389,6 @@ export class AssetsService { const url = `${this.apiUrl.buildUrl(link.href)}?checkReferrers=${checkReferrers}`; return HTTP.requestVersioned(this.http, link.method, url, version).pipe( - tap(() => { - this.analytics.trackEvent('Asset', 'Deleted', appName); - }), pretifyError('i18n:assets.deleteFailed')); } @@ -452,10 +478,14 @@ function buildQuery(q?: AssetsQuery & AssetsByQuery & AssetsByIds & AssetsByRef) return body; } -function parseAssets(response: { items: any[]; total: number } & Resource) { - const items = response.items.map(parseAsset); +function parseAssets(response: { items: any[]; total: number } & Resource): AssetsDto { + const { items: list, total, _links } = response; + const items = list.map(parseAsset); - return new AssetsDto(response.total, items, response._links); + const canCreate = hasAnyLink(_links, 'create'); + const canRenameTag = hasAnyLink(_links, 'tags/rename'); + + return { items, total, canCreate, canRenameTag }; } function parseAsset(response: any) { @@ -479,11 +509,13 @@ function parseAsset(response: any) { response.tags || []); } -function parseAssetFolders(response: { items: any[]; path: any[]; total: number } & Resource) { - const assetFolders = response.items.map(parseAssetFolder); - const assetPath = response.path.map(parseAssetFolder); +function parseAssetFolders(response: { items: any[]; path: any[]; total: number } & Resource): AssetFoldersDto { + const { items: list, _links } = response; + const items = list.map(parseAssetFolder); + + const canCreate = hasAnyLink(_links, 'create'); - return new AssetFoldersDto(response.total, assetFolders, assetPath, response._links); + return { items, canCreate, path: response.path.map(parseAssetFolder) }; } function parseAssetFolder(response: any) { diff --git a/frontend/src/app/shared/services/backups.service.spec.ts b/frontend/src/app/shared/services/backups.service.spec.ts index 9aa4727a8..54d7e38b2 100644 --- a/frontend/src/app/shared/services/backups.service.spec.ts +++ b/frontend/src/app/shared/services/backups.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, BackupDto, BackupsDto, BackupsService, DateTime, Resource, ResourceLinks, RestoreDto } from '@app/shared/internal'; +import { ApiUrlConfig, BackupDto, BackupsDto, BackupsService, DateTime, Resource, ResourceLinks, RestoreDto } from '@app/shared/internal'; describe('BackupsService', () => { beforeEach(() => { @@ -18,7 +18,6 @@ describe('BackupsService', () => { providers: [ BackupsService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -47,11 +46,13 @@ describe('BackupsService', () => { ], }); - expect(backups!).toEqual( - new BackupsDto(2, [ + expect(backups!).toEqual({ + items: [ createBackup(12), createBackup(13), - ], {})); + ], + canCreate: false, + }); })); it('should make get request to get restore', diff --git a/frontend/src/app/shared/services/backups.service.ts b/frontend/src/app/shared/services/backups.service.ts index 2aa7f7f77..83d6c10e1 100644 --- a/frontend/src/app/shared/services/backups.service.ts +++ b/frontend/src/app/shared/services/backups.service.ts @@ -8,14 +8,8 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of, throwError } from 'rxjs'; -import { catchError, map, tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, DateTime, hasAnyLink, pretifyError, Resource, ResourceLinks, ResultSet, Types } from '@app/framework'; - -export class BackupsDto extends ResultSet { - public get canCreate() { - return hasAnyLink(this._links, 'create'); - } -} +import { catchError, map } from 'rxjs/operators'; +import { ApiUrlConfig, DateTime, hasAnyLink, pretifyError, Resource, ResourceLinks, Types } from '@app/framework'; export class BackupDto { public readonly _links: ResourceLinks; @@ -29,8 +23,7 @@ export class BackupDto { return this.status === 'Failed'; } - constructor( - links: ResourceLinks, + constructor(links: ResourceLinks, public readonly id: string, public readonly started: DateTime, public readonly stopped: DateTime | null, @@ -58,15 +51,27 @@ export class RestoreDto { } } -export type StartRestoreDto = - Readonly<{ url: string; newAppName?: string }>; +export type BackupsDto = Readonly<{ + // The list of backups. + items: ReadonlyArray; + + // True, if the user has permissions to create a backup. + canCreate?: boolean; +}>; + +export type StartRestoreDto = Readonly<{ + // The url of the backup file. + url: string; + + // The optional app name tro use. + newAppName?: string; +}>; @Injectable() export class BackupsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -103,9 +108,6 @@ export class BackupsService { const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`); return this.http.post(url, {}).pipe( - tap(() => { - this.analytics.trackEvent('Backup', 'Started', appName); - }), pretifyError('i18n:backups.startFailed')); } @@ -113,9 +115,6 @@ export class BackupsService { const url = this.apiUrl.buildUrl('api/apps/restore'); return this.http.post(url, dto).pipe( - tap(() => { - this.analytics.trackEvent('Restore', 'Started'); - }), pretifyError('i18n:backups.restoreFailed')); } @@ -125,17 +124,17 @@ export class BackupsService { const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url).pipe( - tap(() => { - this.analytics.trackEvent('Backup', 'Deleted', appName); - }), pretifyError('i18n:backups.deleteFailed')); } } -function parseBackups(response: { items: any[] } & Resource) { - const items = response.items.map(parseBackup); +function parseBackups(response: { items: any[] } & Resource): BackupsDto { + const { items: list, _links } = response; + const items = list.map(parseBackup); + + const canCreate = hasAnyLink(_links, 'create'); - return new BackupsDto(items.length, items, response._links); + return { items, canCreate }; } function parseRestore(response: any) { diff --git a/frontend/src/app/shared/services/clients.service.spec.ts b/frontend/src/app/shared/services/clients.service.spec.ts index 58ab97a96..1ca97b139 100644 --- a/frontend/src/app/shared/services/clients.service.spec.ts +++ b/frontend/src/app/shared/services/clients.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AccessTokenDto, AnalyticsService, ApiUrlConfig, ClientDto, ClientsDto, ClientsPayload, ClientsService, Resource, ResourceLinks, Version } from '@app/shared/internal'; +import { AccessTokenDto, ApiUrlConfig, ClientDto, ClientsDto, ClientsPayload, ClientsService, Resource, ResourceLinks, Version } from '@app/shared/internal'; describe('ClientsService', () => { const version = new Version('1'); @@ -20,7 +20,6 @@ describe('ClientsService', () => { providers: [ ClientsService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -177,9 +176,6 @@ describe('ClientsService', () => { export function createClients(...ids: ReadonlyArray): ClientsPayload { return { items: ids.map(createClient), - _links: { - create: { method: 'POST', href: '/clients' }, - }, canCreate: true, }; } diff --git a/frontend/src/app/shared/services/clients.service.ts b/frontend/src/app/shared/services/clients.service.ts index 953c71673..ab2eb335b 100644 --- a/frontend/src/app/shared/services/clients.service.ts +++ b/frontend/src/app/shared/services/clients.service.ts @@ -8,17 +8,16 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; +import { map } from 'rxjs/operators'; +import { ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; export class ClientDto { public readonly _links: ResourceLinks; - public readonly canUpdate: boolean; public readonly canRevoke: boolean; + public readonly canUpdate: boolean; - constructor( - links: ResourceLinks, + constructor(links: ResourceLinks, public readonly id: string, public readonly name: string, public readonly secret: string, @@ -29,8 +28,8 @@ export class ClientDto { ) { this._links = links; - this.canUpdate = hasAnyLink(links, 'update'); this.canRevoke = hasAnyLink(links, 'delete'); + this.canUpdate = hasAnyLink(links, 'update'); } } @@ -42,24 +41,40 @@ export class AccessTokenDto { } } -export type ClientsDto = - Versioned; +export type ClientsDto = Versioned; + +export type ClientsPayload = Readonly<{ + // The list of clients. + items: ReadonlyArray; + + // True if the user has permissions to create a client. + canCreate?: boolean; +}>; + +export type CreateClientDto = Readonly<{ + // The new client ID. + id: string; + }>; -export type ClientsPayload = - Readonly<{ items: ReadonlyArray; canCreate: boolean } & Resource>; +export type UpdateClientDto = Readonly<{ + // The optional client name. + name?: string; -export type CreateClientDto = - Readonly<{ id: string }>; + // The role for the client to define the permissions. + role?: string; -export type UpdateClientDto = - Readonly<{ name?: string; role?: string; allowAnonymous?: boolean; apiCallsLimit?: number }>; + // True if the client can be used for anonymous access. + allowAnonymous?: boolean; + + // The allowed api calls. + apiCallsLimit?: number; +}>; @Injectable() export class ClientsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -80,9 +95,6 @@ export class ClientsService { mapVersioned(({ body }) => { return parseClients(body); }), - tap(() => { - this.analytics.trackEvent('Client', 'Created', appName); - }), pretifyError('i18n:clients.addFailed')); } @@ -95,9 +107,6 @@ export class ClientsService { mapVersioned(({ body }) => { return parseClients(body); }), - tap(() => { - this.analytics.trackEvent('Client', 'Updated', appName); - }), pretifyError('i18n:clients.revokeFailed')); } @@ -110,9 +119,6 @@ export class ClientsService { mapVersioned(({ body }) => { return parseClients(body); }), - tap(() => { - this.analytics.trackEvent('Client', 'Deleted', appName); - }), pretifyError('i18n:clients.revokeFailed')); } @@ -136,17 +142,21 @@ export class ClientsService { } function parseClients(response: { items: any[] } & Resource): ClientsPayload { - const items = response.items.map(item => - new ClientDto(item._links, - item.id, - item.name || item.id, - item.secret, - item.role, - item.apiCallsLimit, - item.apiTrafficLimit, - item.allowAnonymous)); - - const { _links } = response; - - return { items, _links, canCreate: hasAnyLink(_links, 'create') }; + const { items: list, _links } = response; + const items = list.map(parseClient); + + const canCreate = hasAnyLink(_links, 'create'); + + return { items, canCreate }; +} + +function parseClient(response: any) { + return new ClientDto(response._links, + response.id, + response.name || response.id, + response.secret, + response.role, + response.apiCallsLimit, + response.apiTrafficLimit, + response.allowAnonymous); } diff --git a/frontend/src/app/shared/services/comments.service.ts b/frontend/src/app/shared/services/comments.service.ts index ee33244a0..a894d1d87 100644 --- a/frontend/src/app/shared/services/comments.service.ts +++ b/frontend/src/app/shared/services/comments.service.ts @@ -34,8 +34,13 @@ export class CommentDto extends Model { } } -export type UpsertCommentDto = - Readonly<{ text: string; url?: string }>; +export type UpsertCommentDto = Readonly<{ + // The text to comment. + text: string; + + // The url to the comment. + url?: string; +}>; @Injectable() export class CommentsService { diff --git a/frontend/src/app/shared/services/contents.service.spec.ts b/frontend/src/app/shared/services/contents.service.spec.ts index 7f54512cb..509d8b93c 100644 --- a/frontend/src/app/shared/services/contents.service.spec.ts +++ b/frontend/src/app/shared/services/contents.service.spec.ts @@ -8,7 +8,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; import { ErrorDto } from '@app/framework'; -import { AnalyticsService, ApiUrlConfig, ContentDto, ContentsDto, ContentsService, DateTime, Resource, ResourceLinks, ScheduleDto, Version, Versioned } from '@app/shared/internal'; +import { ApiUrlConfig, ContentDto, ContentsDto, ContentsService, DateTime, Resource, ResourceLinks, ScheduleDto, Version, Versioned } from '@app/shared/internal'; import { BulkResultDto, BulkUpdateDto } from './contents.service'; import { sanitize } from './query'; @@ -23,7 +23,6 @@ describe('ContentsService', () => { providers: [ ContentsService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -63,11 +62,18 @@ describe('ContentsService', () => { }], }); - expect(contents!).toEqual( - new ContentsDto([{ status: 'Draft', color: 'Gray' }], 10, [ + expect(contents!).toEqual({ + items: [ createContent(12), createContent(13), - ])); + ], + total: 10, + statuses: [ + { status: 'Draft', color: 'Gray' }, + ], + canCreate: false, + canCreateAndPublish: false, + }); })); it('should make post request to get contents with odata filter', diff --git a/frontend/src/app/shared/services/contents.service.ts b/frontend/src/app/shared/services/contents.service.ts index 9e1966eba..cd56d584b 100644 --- a/frontend/src/app/shared/services/contents.service.ts +++ b/frontend/src/app/shared/services/contents.service.ts @@ -8,8 +8,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, DateTime, ErrorDto, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, ResultSet, Version, Versioned } from '@app/framework'; +import { map } from 'rxjs/operators'; +import { ApiUrlConfig, DateTime, ErrorDto, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; import { StatusInfo } from './../state/contents.state'; import { Query, sanitize } from './query'; import { parseField, RootFieldDto } from './schemas.service'; @@ -24,25 +24,6 @@ export class ScheduleDto { } } -export class ContentsDto extends ResultSet { - constructor( - public readonly statuses: ReadonlyArray, - total: number, - items: ReadonlyArray, - links?: ResourceLinks, - ) { - super(total, items, links); - } - - public get canCreate() { - return hasAnyLink(this._links, 'create'); - } - - public get canCreateAndPublish() { - return hasAnyLink(this._links, 'create/publish'); - } -} - export class ContentDto { public readonly _links: ResourceLinks; @@ -104,47 +85,114 @@ export class BulkResultDto { } } -export type BulkUpdateType = 'Upsert' | 'ChangeStatus' | 'Delete' | 'Validate'; +export type ContentsDto = Readonly<{ + // The list of content items. + items: ReadonlyArray; + + // The total number of content items. + total: number; + + // The statuses. + statuses: ReadonlyArray; + + // True, if the user has permissions to create a content item. + canCreate?: boolean; + + // True, if the user has permissions to create and publish a content item. + canCreateAndPublish?: boolean; +}>; + +export type ContentReferencesValue = Readonly<{ + // The references by partition. + [partition: string]: string; +}> | string; + +export type ContentReferences = Readonly<{ + // The reference values by field name. + [fieldName: string ]: ContentFieldData; +}>; + +export type ContentFieldData = Readonly<{ + // The data by partition. + [partition: string]: T; +}>; -export type ContentReferencesValue = - Readonly<{ [partition: string]: string }> | string; +export type ContentData = Readonly<{ + // The content data by field name. + [fieldName: string ]: ContentFieldData; +}>; -export type ContentReferences = - Readonly<{ [fieldName: string ]: ContentFieldData }>; +export type BulkStatusDto = Readonly<{ +}>; -export type ContentFieldData = - Readonly<{ [partition: string]: T }>; +export type BulkUpdateDto = Readonly<{ + // The list of bulk update jobs. + jobs: ReadonlyArray; -export type ContentData = - Readonly<{ [fieldName: string ]: ContentFieldData }>; + // True, if scripts should not be executed. + doNotScript?: boolean; -export type BulkStatusDto = - Readonly<{ status?: string; dueTime?: string | null }>; + // True, if referrers should be checked. + checkReferrers?: boolean; +}>; -export type BulkUpdateDto = - Readonly<{ jobs: ReadonlyArray; doNotScript?: boolean; checkReferrers?: boolean }>; +export type BulkUpdateJobDto = Readonly<{ + // The ID of the content to update. + id: string; -export type BulkUpdateJobDto = - Readonly<{ id: string; type: BulkUpdateType; schema?: string; expectedVersion?: number }> & BulkStatusDto; + // The type of the bulk update job. + type: 'Upsert' | 'ChangeStatus' | 'Delete' | 'Validate'; -export type ContentsQuery = - Readonly<{ noTotal?: boolean; noSlowTotal?: boolean }>; + // The schema of the content item. + schema?: string; -export type ContentsByIds = - Readonly<{ ids: ReadonlyArray }> & ContentsQuery; + // The new status. + status?: string; -export type ContentsBySchedule = - Readonly<{ scheduledFrom: string | null; scheduledTo: string | null }> & ContentsQuery; + // The due time of the new status. + dueTime?: string | null; -type ContentsByQuery = - Readonly<{ query?: Query; skip?: number; take?: number }> & ContentsQuery; + // The expected version of the content. + expectedVersion?: number; +}>; + +export type ContentsQuery = Readonly<{ + // True, to not return the total number of items. + noTotal?: boolean; + + // True, to not return the total number of items, if the query would be slow. + noSlowTotal?: boolean; +}>; + +export type ContentsByIds = Readonly<{ + // The Ids of the contents to query. + ids: ReadonlyArray; +}> & ContentsQuery; + +export type ContentsBySchedule = Readonly<{ + // The start of the time frame for scheduled content items. + scheduledFrom: string | null; + + // The end of the time frame for scheduled content items. + scheduledTo: string | null; +}> & ContentsQuery; + +export type ContentsByQuery = Readonly<{ + // The JSON query. + query?: Query; + + // The number of items to skip. + skip?: number; + + // The number of items to take. + take?: number; +}> & ContentsQuery; @Injectable() export class ContentsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -230,9 +278,6 @@ export class ContentsService { map(({ payload }) => { return parseContent(payload.body); }), - tap(() => { - this.analytics.trackEvent('Content', 'Created', appName); - }), pretifyError('i18n:contents.createFailed')); } @@ -245,9 +290,6 @@ export class ContentsService { map(({ payload }) => { return parseContent(payload.body); }), - tap(() => { - this.analytics.trackEvent('Content', 'Updated', appName); - }), pretifyError('i18n:contents.updateFailed')); } @@ -260,9 +302,6 @@ export class ContentsService { map(({ payload }) => { return parseContent(payload.body); }), - tap(() => { - this.analytics.trackEvent('Content', 'Updated', appName); - }), pretifyError('i18n:contents.updateFailed')); } @@ -275,9 +314,6 @@ export class ContentsService { map(({ payload }) => { return parseContent(payload.body); }), - tap(() => { - this.analytics.trackEvent('Content', 'VersioNCreated', appName); - }), pretifyError('i18n:contents.loadVersionFailed')); } @@ -290,9 +326,6 @@ export class ContentsService { map(({ payload }) => { return parseContent(payload.body); }), - tap(() => { - this.analytics.trackEvent('Content', 'Cancelled', appName); - }), pretifyError('i18n:contents.updateFailed')); } @@ -305,9 +338,6 @@ export class ContentsService { map(({ payload }) => { return parseContent(payload.body); }), - tap(() => { - this.analytics.trackEvent('Content', 'VersionDeleted', appName); - }), pretifyError('i18n:contents.deleteVersionFailed')); } @@ -318,9 +348,6 @@ export class ContentsService { map(body => { return body.map(x => new BulkResultDto(x.contentId, parseError(x.error))); }), - tap(() => { - this.analytics.trackEvent('Content', 'Deleted', appName); - }), pretifyError('i18n:contents.bulkFailed')); } } @@ -377,13 +404,17 @@ function buildQuery(q?: ContentsByQuery) { return body; } -function parseContents(response: { items: any[]; total: number; statuses: any } & Resource) { - const items = response.items.map(parseContent); +function parseContents(response: { items: any[]; total: number; statuses: any } & Resource): ContentsDto { + const { items: list, total, statuses, _links } = response; + const items = list.map(parseContent); + + const canCreate = hasAnyLink(_links, 'create'); + const canCreateAndPublish = hasAnyLink(_links, 'create/publish'); - return new ContentsDto(response.statuses, response.total, items, response._links); + return { items, total, statuses, canCreate, canCreateAndPublish }; } -function parseContent(response: any & Resource) { +function parseContent(response: any) { return new ContentDto(response._links, response.id, DateTime.parseISO(response.created), response.createdBy, diff --git a/frontend/src/app/shared/services/contributors.service.spec.ts b/frontend/src/app/shared/services/contributors.service.spec.ts index c529e21b9..a17cefc6c 100644 --- a/frontend/src/app/shared/services/contributors.service.spec.ts +++ b/frontend/src/app/shared/services/contributors.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, ContributorDto, ContributorsDto, ContributorsPayload, ContributorsService, Resource, ResourceLinks, Version } from '@app/shared/internal'; +import { ApiUrlConfig, ContributorDto, ContributorsDto, ContributorsPayload, ContributorsService, Resource, ResourceLinks, Version } from '@app/shared/internal'; describe('ContributorsService', () => { const version = new Version('1'); @@ -20,7 +20,6 @@ describe('ContributorsService', () => { providers: [ ContributorsService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -127,14 +126,9 @@ describe('ContributorsService', () => { export function createContributors(...ids: ReadonlyArray): ContributorsPayload { return { - items: ids.map(createContributor), maxContributors: ids.length * 13, - _links: { - create: { method: 'POST', href: '/contributors' }, - }, - _meta: { - isInvited: 'true', - }, + items: ids.map(createContributor), + isInvited: false, canCreate: true, }; } diff --git a/frontend/src/app/shared/services/contributors.service.ts b/frontend/src/app/shared/services/contributors.service.ts index 9e2c002e2..ac081d37b 100644 --- a/frontend/src/app/shared/services/contributors.service.ts +++ b/frontend/src/app/shared/services/contributors.service.ts @@ -8,21 +8,19 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; +import { ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; export class ContributorDto { public readonly _links: ResourceLinks; - public readonly canUpdate: boolean; public readonly canRevoke: boolean; + public readonly canUpdate: boolean; public get token() { return `subject:${this.contributorId}`; } - constructor( - links: ResourceLinks, + constructor(links: ResourceLinks, public readonly contributorId: string, public readonly contributorName: string, public readonly contributorEmail: string, @@ -30,26 +28,43 @@ export class ContributorDto { ) { this._links = links; - this.canUpdate = hasAnyLink(links, 'update'); this.canRevoke = hasAnyLink(links, 'delete'); + this.canUpdate = hasAnyLink(links, 'update'); } } -export type ContributorsDto = - Versioned; +export type ContributorsDto = Versioned; + +export type ContributorsPayload = Readonly<{ + // The list of contributors. + items: ReadonlyArray; + + // The number of allowed contributors. + maxContributors: number; -export type ContributorsPayload = - Readonly<{ items: ReadonlyArray; maxContributors: number; canCreate: boolean } & Resource>; + // True, if the user has been invited. + isInvited?: boolean; -export type AssignContributorDto = - Readonly<{ contributorId: string; role: string; invite?: boolean }>; + // True, if the user has permission to create a contributor. + canCreate?: boolean; +}>; + +export type AssignContributorDto = Readonly<{ + // The user ID. + contributorId: string; + + // The role for the contributor. + role: string; + + // True, if the user should be invited. + invite?: boolean; +}>; @Injectable() export class ContributorsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -70,9 +85,6 @@ export class ContributorsService { mapVersioned(({ body }) => { return parseContributors(body); }), - tap(() => { - this.analytics.trackEvent('Contributor', 'Configured', appName); - }), pretifyError('i18n:contributors.addFailed')); } @@ -85,22 +97,23 @@ export class ContributorsService { mapVersioned(({ body }) => { return parseContributors(body); }), - tap(() => { - this.analytics.trackEvent('Contributor', 'Deleted', appName); - }), pretifyError('i18n:contributors.deleteFailed')); } } -function parseContributors(response: { items: any[]; maxContributors: number } & Resource) { - const items = response.items.map(item => - new ContributorDto(item._links, - item.contributorId, - item.contributorName, - item.contributorEmail, - item.role)); +function parseContributors(response: { items: any[]; maxContributors: number } & Resource): ContributorsPayload { + const { items: list, maxContributors, _meta, _links } = response; + const items = list.map(parseContributor); - const { maxContributors, _links, _meta } = response; + const canCreate = hasAnyLink(_links, 'create'); - return { items, maxContributors, _links, _meta, canCreate: hasAnyLink(_links, 'create') }; + return { items, maxContributors, canCreate, isInvited: _meta?.['isInvited'] === '1' }; } + +function parseContributor(response: any) { + return new ContributorDto(response._links, + response.contributorId, + response.contributorName, + response.contributorEmail, + response.role); +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/history.service.ts b/frontend/src/app/shared/services/history.service.ts index ffdb674bf..3069ad7dd 100644 --- a/frontend/src/app/shared/services/history.service.ts +++ b/frontend/src/app/shared/services/history.service.ts @@ -82,19 +82,22 @@ export class HistoryService { return this.http.get(url, options).pipe( map(body => { - return parseHistory(body); + return parseHistoryEvents(body); }), pretifyError('i18n:history.loadFailed')); } } -function parseHistory(response: any[]) { - return response.map(item => new HistoryEventDto( - item.eventId, - item.actor, - item.eventType, - item.message, - DateTime.parseISO(item.created), - new Version(item.version.toString()))); +function parseHistoryEvents(response: any[]) { + return response.map(parseHistoryEvent); } +function parseHistoryEvent(response: any) { + return new HistoryEventDto( + response.eventId, + response.actor, + response.eventType, + response.message, + DateTime.parseISO(response.created), + new Version(response.version.toString())); +} diff --git a/frontend/src/app/shared/services/languages.service.ts b/frontend/src/app/shared/services/languages.service.ts index 3f882e399..7ead961e8 100644 --- a/frontend/src/app/shared/services/languages.service.ts +++ b/frontend/src/app/shared/services/languages.service.ts @@ -39,7 +39,11 @@ export class LanguagesService { } function parseLanguages(response: any[]) { - return response.map(item => new LanguageDto( - item.iso2Code, - item.englishName)); + return response.map(parseLanguage); +} + +function parseLanguage(response: any) { + return new LanguageDto( + response.iso2Code, + response.englishName); } diff --git a/frontend/src/app/shared/services/news.service.spec.ts b/frontend/src/app/shared/services/news.service.spec.ts index dfd4583a9..c1ca1ed6a 100644 --- a/frontend/src/app/shared/services/news.service.spec.ts +++ b/frontend/src/app/shared/services/news.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, FeatureDto, FeaturesDto, NewsService } from '@app/shared/internal'; +import { ApiUrlConfig, FeaturesDto, NewsService } from '@app/shared/internal'; describe('NewsService', () => { beforeEach(() => { @@ -40,7 +40,6 @@ describe('NewsService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush({ - version: 13, features: [{ name: 'Feature1', text: 'Feature Text1', @@ -48,12 +47,18 @@ describe('NewsService', () => { name: 'Feature2', text: 'Feature Text2', }], + version: 13, }); - expect(features!).toEqual( - new FeaturesDto([ - new FeatureDto('Feature1', 'Feature Text1'), - new FeatureDto('Feature2', 'Feature Text2'), - ], 13)); + expect(features!).toEqual({ + features: [{ + name: 'Feature1', + text: 'Feature Text1', + }, { + name: 'Feature2', + text: 'Feature Text2', + }], + version: 13, + }); })); }); diff --git a/frontend/src/app/shared/services/news.service.ts b/frontend/src/app/shared/services/news.service.ts index 23a402131..17a26e3c2 100644 --- a/frontend/src/app/shared/services/news.service.ts +++ b/frontend/src/app/shared/services/news.service.ts @@ -8,24 +8,23 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; import { ApiUrlConfig, pretifyError } from '@app/framework'; -export class FeatureDto { - constructor( - public readonly name: string, - public readonly text: string, - ) { - } -} +export type FeatureDto = Readonly<{ + // The name of the feature. + name: string; -export class FeaturesDto { - constructor( - public readonly features: ReadonlyArray, - public readonly version: number, - ) { - } -} + // The feature description. + text: string; +}>; + +export type FeaturesDto = Readonly<{ + // The list of features. + features: ReadonlyArray; + + // The latest version. + version: number; +}>; @Injectable() export class NewsService { @@ -38,20 +37,7 @@ export class NewsService { public getFeatures(version: number): Observable { const url = this.apiUrl.buildUrl(`api/news/features?version=${version}`); - return this.http.get(url).pipe( - map(body => { - return parseFeatures(body); - }), + return this.http.get(url).pipe( pretifyError('i18n:features.loadFailed')); } } - -function parseFeatures(body: { features: any[]; version: number }) { - return new FeaturesDto( - body.features.map(item => - new FeatureDto( - item.name, - item.text), - ), - body.version); -} diff --git a/frontend/src/app/shared/services/plans.service.spec.ts b/frontend/src/app/shared/services/plans.service.spec.ts index bc2e07f2d..e24d62f50 100644 --- a/frontend/src/app/shared/services/plans.service.spec.ts +++ b/frontend/src/app/shared/services/plans.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, PlanChangedDto, PlanDto, PlansDto, PlansService, Version } from '@app/shared/internal'; +import { ApiUrlConfig, PlanChangedDto, PlanDto, PlansDto, PlansService, Version } from '@app/shared/internal'; describe('PlansService', () => { const version = new Version('1'); @@ -20,7 +20,6 @@ describe('PlansService', () => { providers: [ PlansService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); diff --git a/frontend/src/app/shared/services/plans.service.ts b/frontend/src/app/shared/services/plans.service.ts index 55cb113b4..aa826a756 100644 --- a/frontend/src/app/shared/services/plans.service.ts +++ b/frontend/src/app/shared/services/plans.service.ts @@ -8,8 +8,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, HTTP, mapVersioned, pretifyError, Version, Versioned } from '@app/framework'; +import { ApiUrlConfig, HTTP, mapVersioned, pretifyError, Version, Versioned } from '@app/framework'; export class PlanDto { constructor( @@ -28,21 +27,37 @@ export class PlanDto { } } -export type PlansDto = - Versioned }>>; +export type PlansDto = Versioned; -export type PlanChangedDto = - Readonly<{ redirectUri?: string }>; +export type PlansPayload = Readonly<{ + // The ID of the current plan. + currentPlanId: string; -export type ChangePlanDto = - Readonly<{ planId: string }>; + // The user, who owns the plan. + planOwner: string; + + // True, if the installation has a billing portal. + hasPortal: boolean; + + // The actual plans. + plans: ReadonlyArray; +}>; + +export type PlanChangedDto = Readonly<{ + // The redirect URI. + redirectUri?: string; +}>; + +export type ChangePlanDto = Readonly<{ + // The new plan ID. + planId: string; +}>; @Injectable() export class PlansService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -63,32 +78,28 @@ export class PlansService { mapVersioned(({ body }) => { return body; }), - tap(() => { - this.analytics.trackEvent('Plan', 'Changed', appName); - }), pretifyError('i18n:plans.changeFailed')); } } -function parsePlans(body: { plans: any[]; hasPortal: boolean; currentPlanId: string; planOwner: string }) { - const { hasPortal, currentPlanId, planOwner } = body; - - return { - currentPlanId, - planOwner, - plans: body.plans.map(item => new PlanDto( - item.id, - item.name, - item.costs, - item.confirmText, - item.yearlyId, - item.yearlyCosts, - item.yearlyConfirmText, - item.maxApiBytes, - item.maxApiCalls, - item.maxAssetSize, - item.maxContributors)), - hasPortal, - }; +function parsePlans(response: { plans: any[]; hasPortal: boolean; currentPlanId: string; planOwner: string }): PlansPayload { + const { plans: list, currentPlanId, hasPortal, planOwner } = response; + const plans = list.map(parsePlan); + + return { plans, planOwner, currentPlanId, hasPortal }; } +function parsePlan(response: any) { + return new PlanDto( + response.id, + response.name, + response.costs, + response.confirmText, + response.yearlyId, + response.yearlyCosts, + response.yearlyConfirmText, + response.maxApiBytes, + response.maxApiCalls, + response.maxAssetSize, + response.maxContributors); +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/query.ts b/frontend/src/app/shared/services/query.ts index 144242350..a0dc8b5b4 100644 --- a/frontend/src/app/shared/services/query.ts +++ b/frontend/src/app/shared/services/query.ts @@ -31,7 +31,7 @@ export type FilterFieldUI = 'Status' | 'Unsupported' | 'User'; - + export function getFilterUI(comparison: FilterComparison, field: FilterableField): FilterFieldUI { if (!field || !comparison) { return 'None'; diff --git a/frontend/src/app/shared/services/roles.service.spec.ts b/frontend/src/app/shared/services/roles.service.spec.ts index 67e0acf65..4c73debff 100644 --- a/frontend/src/app/shared/services/roles.service.spec.ts +++ b/frontend/src/app/shared/services/roles.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, Resource, ResourceLinks, RoleDto, RolesDto, RolesPayload, RolesService, Version } from '@app/shared/internal'; +import { ApiUrlConfig, Resource, ResourceLinks, RoleDto, RolesDto, RolesPayload, RolesService, Version } from '@app/shared/internal'; describe('RolesService', () => { const version = new Version('1'); @@ -20,7 +20,6 @@ describe('RolesService', () => { providers: [ RolesService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -174,9 +173,6 @@ describe('RolesService', () => { export function createRoles(...ids: ReadonlyArray): RolesPayload { return { items: ids.map(createRole), - _links: { - create: { method: 'POST', href: '/roles' }, - }, canCreate: true, }; } diff --git a/frontend/src/app/shared/services/roles.service.ts b/frontend/src/app/shared/services/roles.service.ts index 0e737e668..08d2584e2 100644 --- a/frontend/src/app/shared/services/roles.service.ts +++ b/frontend/src/app/shared/services/roles.service.ts @@ -8,8 +8,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; +import { ApiUrlConfig, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, Version, Versioned } from '@app/framework'; export class RoleDto { public readonly _links: ResourceLinks; @@ -17,8 +16,7 @@ export class RoleDto { public readonly canDelete: boolean; public readonly canUpdate: boolean; - constructor( - links: ResourceLinks, + constructor(links: ResourceLinks, public readonly name: string, public readonly numClients: number, public readonly numContributors: number, @@ -33,26 +31,34 @@ export class RoleDto { } } -type Permissions = readonly string[]; +export type RolesDto = Versioned; + +export type RolesPayload = Readonly<{ + // The list of roles. + items: ReadonlyArray; -export type RolesDto = - Versioned; + // True, if the user has permissions to create a new role. + canCreate?: boolean; +}>; -export type RolesPayload = - Readonly<{ items: ReadonlyArray; canCreate: boolean } & Resource>; +export type CreateRoleDto = Readonly<{ + // The name of the role, cannot be changed later. + name: string; +}>; -export type CreateRoleDto = - Readonly<{ name: string }>; +export type UpdateRoleDto = Readonly<{ + // The permissions in dot notation. + permissions: ReadonlyArray; -export type UpdateRoleDto = - Readonly<{ permissions: Permissions; properties: {} }>; + // The UI properties. + properties: {}; +}>; @Injectable() export class RolesService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -73,9 +79,6 @@ export class RolesService { mapVersioned(({ body }) => { return parseRoles(body); }), - tap(() => { - this.analytics.trackEvent('Role', 'Created', appName); - }), pretifyError('i18n:roles.addFailed')); } @@ -88,9 +91,6 @@ export class RolesService { mapVersioned(({ body }) => { return parseRoles(body); }), - tap(() => { - this.analytics.trackEvent('Role', 'Updated', appName); - }), pretifyError('i18n:roles.updateFailed')); } @@ -103,9 +103,6 @@ export class RolesService { mapVersioned(({ body }) => { return parseRoles(body); }), - tap(() => { - this.analytics.trackEvent('Role', 'Deleted', appName); - }), pretifyError('i18n:roles.revokeFailed')); } @@ -117,19 +114,21 @@ export class RolesService { } } -export function parseRoles(response: any) { - const raw: any[] = response.items; - - const items = raw.map(item => - new RoleDto(item._links, - item.name, - item.numClients, - item.numContributors, - item.permissions, - item.properties, - item.isDefaultRole)); +function parseRoles(response: { items: any } & Resource): RolesPayload { + const { items: list, _links } = response; + const items = list.map(parseRole); - const { _links } = response; + const canCreate = hasAnyLink(_links, 'create'); - return { items, _links, canCreate: hasAnyLink(_links, 'create') }; + return { items, canCreate }; } + +function parseRole(response: any) { + return new RoleDto(response._links, + response.name, + response.numClients, + response.numContributors, + response.permissions, + response.properties, + response.isDefaultRole); +} \ No newline at end of file diff --git a/frontend/src/app/shared/services/rules.service.spec.ts b/frontend/src/app/shared/services/rules.service.spec.ts index eaa81261f..65d190898 100644 --- a/frontend/src/app/shared/services/rules.service.spec.ts +++ b/frontend/src/app/shared/services/rules.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, DateTime, Resource, ResourceLinks, RuleDto, RuleElementDto, RuleElementPropertyDto, RuleEventDto, RuleEventsDto, RulesDto, RulesService, Version } from '@app/shared/internal'; +import { ApiUrlConfig, DateTime, Resource, ResourceLinks, RuleDto, RuleElementDto, RuleElementPropertyDto, RuleEventDto, RuleEventsDto, RulesDto, RulesService, Version } from '@app/shared/internal'; import { RuleCompletions } from '..'; import { SimulatedRuleEventDto, SimulatedRuleEventsDto } from './rules.service'; @@ -22,7 +22,6 @@ describe('RulesService', () => { providers: [ RulesService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -117,11 +116,16 @@ describe('RulesService', () => { runningRuleId: '12', }); - expect(rules!).toEqual( - new RulesDto([ + expect(rules!).toEqual({ + items: [ createRule(12), createRule(13), - ], {}, '12')); + ], + runningRuleId: '12', + canCancelRun: false, + canCreate: false, + canReadEvents: false, + }); })); it('should make post request to create rule', @@ -290,13 +294,22 @@ describe('RulesService', () => { ruleEventResponse(1), ruleEventResponse(2), ], + _links: { + cancel: { method: 'DELETE', href: '/rules/events' }, + }, }); - expect(rules!).toEqual( - new RuleEventsDto(20, [ + expect(rules!).toEqual({ + items: [ createRuleEvent(1), createRuleEvent(2), - ])); + ], + _links: { + cancel: { method: 'DELETE', href: '/rules/events' }, + }, + total: 20, + canCancelAll: false, + }); })); it('should make get request to get simulated rule events', @@ -319,11 +332,13 @@ describe('RulesService', () => { ], }); - expect(rules!).toEqual( - new SimulatedRuleEventsDto(20, [ + expect(rules!).toEqual({ + items: [ createSimulatedRuleEvent(1), createSimulatedRuleEvent(2), - ])); + ], + total: 20, + }); })); it('should make post request to get simulated rule events with action and trigger', @@ -346,11 +361,13 @@ describe('RulesService', () => { ], }); - expect(rules!).toEqual( - new SimulatedRuleEventsDto(20, [ + expect(rules!).toEqual({ + items: [ createSimulatedRuleEvent(1), createSimulatedRuleEvent(2), - ])); + ], + total: 20, + }); })); it('should make put request to enqueue rule event', diff --git a/frontend/src/app/shared/services/rules.service.ts b/frontend/src/app/shared/services/rules.service.ts index 52ecebfdb..3d423e680 100644 --- a/frontend/src/app/shared/services/rules.service.ts +++ b/frontend/src/app/shared/services/rules.service.ts @@ -8,8 +8,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, DateTime, hasAnyLink, HTTP, Model, pretifyError, Resource, ResourceLinks, ResultSet, Version } from '@app/framework'; +import { map } from 'rxjs/operators'; +import { ApiUrlConfig, DateTime, hasAnyLink, HTTP, Model, pretifyError, Resource, ResourceLinks, Version } from '@app/framework'; export type RuleElementMetadataDto = Readonly<{ description: string; @@ -101,26 +101,6 @@ export class RuleElementPropertyDto { } } -export class RulesDto extends ResultSet { - public get canCreate() { - return hasAnyLink(this._links, 'create'); - } - - public get canReadEvents() { - return hasAnyLink(this._links, 'events'); - } - - public get canCancelRun() { - return hasAnyLink(this._links, 'run/cancel'); - } - - constructor(items: ReadonlyArray, links?: {}, - public readonly runningRuleId?: string, - ) { - super(items.length, items, links); - } -} - export class RuleDto { public readonly _links: ResourceLinks; @@ -133,8 +113,7 @@ export class RuleDto { public readonly canTrigger: boolean; public readonly canUpdate: boolean; - constructor( - links: ResourceLinks, + constructor(links: ResourceLinks, public readonly id: string, public readonly created: DateTime, public readonly createdBy: string, @@ -164,9 +143,6 @@ export class RuleDto { } } -export class RuleEventsDto extends ResultSet { -} - export class RuleEventDto extends Model { public readonly _links: ResourceLinks; @@ -193,9 +169,6 @@ export class RuleEventDto extends Model { } } -export class SimulatedRuleEventsDto extends ResultSet { -} - export class SimulatedRuleEventDto { public readonly _links: ResourceLinks; @@ -213,27 +186,93 @@ export class SimulatedRuleEventDto { } } -export type RuleCompletions = - ReadonlyArray<{ path: string; description: string; type: string }>; +export type RuleCompletions = ReadonlyArray>; + +export type RulesDto = Readonly<{ + // The list of rules. + items: ReadonlyArray; + + // The id of the rule that is currently running. + runningRuleId?: string; + + // True, if the user has permission to create a rule. + canCreate?: boolean; + + // True, if the user has permission to read events. + canReadEvents?: boolean; + + // True, if the user has permission to cancel an event. + canCancelRun?: boolean; +}>; + +export type RuleEventsDto = Readonly<{ + // The list of rule events. + items: ReadonlyArray; + + // The total number of rule events. + total: number; + + // True, if the user has permissions to cancel all rule events. + canCancelAll?: boolean; +}> & Resource; + +export type SimulatedRuleEventsDto = Readonly<{ + // The list of simulated rule events. + items: ReadonlyArray; -export type ActionsDto = - Readonly<{ [name: string]: RuleElementDto }>; + // The total number of simulated rule events. + total: number; +}>; + +export type ActionsDto = Readonly<{ + // The rule elements by name. + [name: string]: RuleElementDto; +}>; -export type UpsertRuleDto = - Readonly<{ trigger?: RuleTrigger; action?: RuleAction; name?: string; isEnabled?: boolean }>; +export type UpsertRuleDto = Readonly<{ + // The optional trigger to update. + trigger?: RuleTrigger; -export type RuleAction = - Readonly<{ actionType: string; [key: string]: any }>; + // The optional action to update. + action?: RuleAction; -export type RuleTrigger = - Readonly<{ triggerType: string; [key: string]: any }>; + // The optional rule name. + name?: string; + + // True, if the rule is enabled. + isEnabled?: boolean; +}>; + +export type RuleAction = Readonly<{ + // The type of the action. + actionType: string; + + // The additional properties. + [key: string]: any; + }>; + +export type RuleTrigger = Readonly<{ + // The type of the trigger. + triggerType: string; + + // The additional properties. + [key: string]: any; +}>; @Injectable() export class RulesService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -264,9 +303,6 @@ export class RulesService { map(({ payload }) => { return parseRule(payload.body); }), - tap(() => { - this.analytics.trackEvent('Rule', 'Created', appName); - }), pretifyError('i18n:rules.createFailed')); } @@ -279,9 +315,6 @@ export class RulesService { map(({ payload }) => { return parseRule(payload.body); }), - tap(() => { - this.analytics.trackEvent('Rule', 'Updated', appName); - }), pretifyError('i18n:rules.updateFailed')); } @@ -291,9 +324,6 @@ export class RulesService { const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( - tap(() => { - this.analytics.trackEvent('Rule', 'Deleted', appName); - }), pretifyError('i18n:rules.deleteFailed')); } @@ -303,9 +333,6 @@ export class RulesService { const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url, {}).pipe( - tap(() => { - this.analytics.trackEvent('Rule', 'Run', appName); - }), pretifyError('i18n:rules.runFailed')); } @@ -315,9 +342,6 @@ export class RulesService { const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url, {}).pipe( - tap(() => { - this.analytics.trackEvent('Rule', 'Run', appName); - }), pretifyError('i18n:rules.runFailed')); } @@ -325,9 +349,6 @@ export class RulesService { const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/run`); return this.http.delete(url).pipe( - tap(() => { - this.analytics.trackEvent('Rule', 'RunCancel', appName); - }), pretifyError('i18n:rules.cancelFailed')); } @@ -337,9 +358,6 @@ export class RulesService { const url = this.apiUrl.buildUrl(link.href); return this.http.request(link.method, url, {}).pipe( - tap(() => { - this.analytics.trackEvent('Rule', 'Triggered', appName); - }), pretifyError('i18n:rules.triggerFailed')); } @@ -379,9 +397,6 @@ export class RulesService { const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url).pipe( - tap(() => { - this.analytics.trackEvent('Rule', 'EventEnqueued', appName); - }), pretifyError('i18n:rules.ruleEvents.enqueueFailed')); } @@ -391,9 +406,6 @@ export class RulesService { const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url).pipe( - tap(() => { - this.analytics.trackEvent('Rule', 'EventsCancelled', appName); - }), pretifyError('i18n:rules.ruleEvents.cancelFailed')); } @@ -404,22 +416,31 @@ export class RulesService { } } -function parseSimulatedEvents(response: { items: any[]; total: number } & Resource) { - const simulatedRuleEvents = response.items.map(parseSimulatedRuleEvent); +function parseSimulatedEvents(response: { items: any[]; total: number } & Resource): SimulatedRuleEventsDto { + const { items: list, total } = response; + const items = list.map(parseSimulatedRuleEvent); - return new SimulatedRuleEventsDto(response.total, simulatedRuleEvents, response._links); + return { items, total }; } -function parseEvents(response: { items: any[]; total: number } & Resource) { - const ruleEvents = response.items.map(parseRuleEvent); +function parseEvents(response: { items: any[]; total: number } & Resource): RuleEventsDto { + const { items: list, total, _links } = response; + const items = list.map(parseRuleEvent); + + const canCancelAll = hasAnyLink(_links, 'create'); - return new RuleEventsDto(response.total, ruleEvents, response._links); + return { items, total, canCancelAll, _links }; } -function parseRules(response: { items: any[]; runningRuleId?: string } & Resource) { - const rules = response.items.map(parseRule); +function parseRules(response: { items: any[]; runningRuleId?: string } & Resource): RulesDto { + const { items: list, runningRuleId, _links } = response; + const items = list.map(parseRule); + + const canCreate = hasAnyLink(_links, 'create'); + const canReadEvents = hasAnyLink(_links, 'events'); + const canCancelRun = hasAnyLink(_links, 'run/cancel'); - return new RulesDto(rules, response._links, response.runningRuleId); + return { items, runningRuleId, canCreate, canCancelRun, canReadEvents }; } function parseActions(response: any) { diff --git a/frontend/src/app/shared/services/schemas.service.spec.ts b/frontend/src/app/shared/services/schemas.service.spec.ts index efd1853ad..e42a19e57 100644 --- a/frontend/src/app/shared/services/schemas.service.spec.ts +++ b/frontend/src/app/shared/services/schemas.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, createProperties, DateTime, FieldRule, NestedFieldDto, Resource, ResourceLinks, RootFieldDto, SchemaDto, SchemaPropertiesDto, SchemasDto, SchemasService, Version } from '@app/shared/internal'; +import { ApiUrlConfig, createProperties, DateTime, FieldRule, NestedFieldDto, Resource, ResourceLinks, RootFieldDto, SchemaDto, SchemaPropertiesDto, SchemasDto, SchemasService, Version } from '@app/shared/internal'; import { SchemaCompletions } from '..'; describe('SchemasService', () => { @@ -21,7 +21,6 @@ describe('SchemasService', () => { providers: [ SchemasService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -60,14 +59,11 @@ describe('SchemasService', () => { }); expect(schemas!).toEqual({ - canCreate: true, items: [ createSchema(12), createSchema(13), ], - _links: { - create: { method: 'POST', href: '/schemas' }, - }, + canCreate: true, }); })); diff --git a/frontend/src/app/shared/services/schemas.service.ts b/frontend/src/app/shared/services/schemas.service.ts index 0f381074e..33d8a7b17 100644 --- a/frontend/src/app/shared/services/schemas.service.ts +++ b/frontend/src/app/shared/services/schemas.service.ts @@ -8,12 +8,17 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, DateTime, hasAnyLink, HTTP, pretifyError, Resource, ResourceLinks, StringHelper, Types, Version, Versioned } from '@app/framework'; +import { map } from 'rxjs/operators'; +import { ApiUrlConfig, DateTime, hasAnyLink, HTTP, pretifyError, Resource, ResourceLinks, StringHelper, Types, Version, Versioned } from '@app/framework'; import { QueryModel } from './query'; import { createProperties, FieldPropertiesDto } from './schemas.types'; -export const MetaFields = { +export type FieldRuleAction = 'Disable' | 'Hide' | 'Require'; +export type SchemaType = 'Default' | 'Singleton' | 'Component'; +export type SchemaScripts = Record; +export type PreviewUrls = Record; + +export const META_FIELDS = { empty: { name: '', label: '', @@ -64,17 +69,19 @@ export const MetaFields = { }, }; -export type SchemaType = 'Default' | 'Singleton' | 'Component'; -export type SchemaScripts = Record; -export type PreviewUrls = Record; +export const FIELD_RULE_ACTIONS: ReadonlyArray = [ + 'Disable', + 'Hide', + 'Require', +]; export class SchemaDto { public readonly _links: ResourceLinks; public readonly canAddField: boolean; - public readonly canContentsRead: boolean; public readonly canContentsCreate: boolean; public readonly canContentsCreateAndPublish: boolean; + public readonly canContentsRead: boolean; public readonly canDelete: boolean; public readonly canOrderFields: boolean; public readonly canPublish: boolean; @@ -83,10 +90,10 @@ export class SchemaDto { public readonly canUnpublish: boolean; public readonly canUpdate: boolean; public readonly canUpdateCategory: boolean; + public readonly canUpdateRules: boolean; public readonly canUpdateScripts: boolean; public readonly canUpdateUIFields: boolean; public readonly canUpdateUrls: boolean; - public readonly canUpdateRules: boolean; public readonly displayName: string; @@ -108,8 +115,8 @@ export class SchemaDto { public readonly isPublished: boolean, public readonly properties: SchemaPropertiesDto, public readonly fields: ReadonlyArray = [], - public readonly fieldsInLists: Tags = [], - public readonly fieldsInReferences: Tags = [], + public readonly fieldsInLists: ReadonlyArray = [], + public readonly fieldsInReferences: ReadonlyArray = [], public readonly fieldRules: ReadonlyArray = [], public readonly previewUrls: PreviewUrls = {}, public readonly scripts: SchemaScripts = {}, @@ -117,9 +124,9 @@ export class SchemaDto { this._links = links; this.canAddField = hasAnyLink(links, 'fields/add'); - this.canContentsRead = hasAnyLink(links, 'contents'); this.canContentsCreate = hasAnyLink(links, 'contents/create'); this.canContentsCreateAndPublish = hasAnyLink(links, 'contents/create/publish'); + this.canContentsRead = hasAnyLink(links, 'contents'); this.canDelete = hasAnyLink(links, 'delete'); this.canOrderFields = hasAnyLink(links, 'fields/order'); this.canPublish = hasAnyLink(links, 'publish'); @@ -128,49 +135,53 @@ export class SchemaDto { this.canUnpublish = hasAnyLink(links, 'unpublish'); this.canUpdate = hasAnyLink(links, 'update'); this.canUpdateCategory = hasAnyLink(links, 'update/category'); + this.canUpdateRules = hasAnyLink(links, 'update/rules'); this.canUpdateScripts = hasAnyLink(links, 'update/scripts'); this.canUpdateUIFields = hasAnyLink(links, 'fields/ui'); this.canUpdateUrls = hasAnyLink(links, 'update/urls'); - this.canUpdateRules = hasAnyLink(links, 'update/rules'); this.displayName = StringHelper.firstNonEmpty(this.properties.label, this.name); + function tableField(rootField: RootFieldDto) { + return { name: rootField.name, label: rootField.displayName, rootField }; + } + if (fields) { this.contentFields = fields.filter(x => x.properties.isContentField).map(tableField); function tableFields(names: ReadonlyArray, fields: ReadonlyArray): TableField[] { const result: TableField[] = []; - + for (const name of names) { - const metaField = MetaFields[name]; - + const metaField = META_FIELDS[name]; + if (metaField) { result.push(metaField); } else { const field = fields.find(x => x.name === name && x.properties.isContentField); - + if (field) { result.push(tableField(field)); } } } - + return result; } const listFields = tableFields(fieldsInLists, fields); if (listFields.length === 0) { - listFields.push(MetaFields.lastModifiedByAvatar); + listFields.push(META_FIELDS.lastModifiedByAvatar); if (fields.length > 0) { listFields.push(tableField(this.fields[0])); } else { - listFields.push(MetaFields.empty); + listFields.push(META_FIELDS.empty); } - listFields.push(MetaFields.statusColor); - listFields.push(MetaFields.lastModified); + listFields.push(META_FIELDS.statusColor); + listFields.push(META_FIELDS.lastModified); } this.defaultListFields = listFields; @@ -181,7 +192,7 @@ export class SchemaDto { if (fields.length > 0) { referenceFields.push(tableField(this.fields[0])); } else { - referenceFields.push(MetaFields.empty); + referenceFields.push(META_FIELDS.empty); } } @@ -246,10 +257,6 @@ export class SchemaDto { } } -export function tableField(rootField: RootFieldDto) { - return { name: rootField.name, label: rootField.displayName, rootField }; -} - export class FieldDto { public readonly _links: ResourceLinks; @@ -341,52 +348,135 @@ export class SchemaPropertiesDto { } } -export const FIELD_RULE_ACTIONS: ReadonlyArray = [ - 'Disable', - 'Hide', - 'Require', -]; +export type TableField = Readonly<{ + // The name of the table field. + name: string; -type Tags = readonly string[]; + // The label for the table header. + label: string; -export type TableField = { name: string; label: string; rootField?: RootFieldDto }; + // The reference to the root field. + rootField?: RootFieldDto; +}>; -export type FieldRuleAction = 'Disable' | 'Hide' | 'Require'; -export type FieldRule = { field: string; action: FieldRuleAction; condition: string }; +export type FieldRule = Readonly<{ + // The path to the field to update when the rule is valid. + field: string; + + // The action to invoke. + action: FieldRuleAction; + + //The condition as javascript expression. + condition: string; +}>; + +export type SchemaCompletions = ReadonlyArray<{ + // The autocompletion path. + path: string; -export type SchemaCompletions = - ReadonlyArray<{ path: string; description: string; type: string }>; + // The description of the autocompletion field. + description: string; -export type SchemasDto = - Readonly<{ items: ReadonlyArray; canCreate: boolean } & Resource>; + // The type of the autocompletion field. + type: string; + }>; -export type AddFieldDto = - Readonly<{ name: string; partitioning?: string; properties: FieldPropertiesDto }>; +export type SchemasDto = Readonly<{ + // The list of schemas. + items: ReadonlyArray; -export type UpdateUIFields = - Readonly<{ fieldsInLists?: Tags; fieldsInReferences?: Tags }>; + // True, if the user has permissions to create a new schema. + canCreate?: boolean; +}>; -export type CreateSchemaDto = - Readonly<{ name: string; fields?: ReadonlyArray; category?: string; type?: string; isPublished?: boolean; properties?: SchemaPropertiesDto }>; +export type AddFieldDto = Readonly<{ + // The name of the field. + name: string; -export type UpdateSchemaCategoryDto = - Readonly<{ name?: string }>; + // The partitioning of the field. + partitioning?: string; -export type UpdateFieldDto = - Readonly<{ properties: FieldPropertiesDto }>; + // The field properties. + properties: FieldPropertiesDto; +}>; -export type SynchronizeSchemaDto = - Readonly<{ noFieldDeletiong?: boolean; noFieldRecreation?: boolean; [key: string]: any }>; +export type UpdateUIFields = Readonly<{ + // The names of all fields that should be shown in the list. + fieldsInLists?: ReadonlyArray; -export type UpdateSchemaDto = - Readonly<{ label?: string; hints?: string; contentsSidebarUrl?: string; contentSidebarUrl?: string; contentEditorUrl?: string; validateOnPublish?: boolean; tags?: Tags }>; + // The names of all fields that should be shown in the reference list. + fieldsInReferences?: ReadonlyArray; +}>; + +export type CreateSchemaDto = Readonly<{ + // The name of the schema. + name: string; + + // The initial fields of the schema. + fields?: ReadonlyArray; + + // The category name. + category?: string; + + // The type of the schema. + type?: string; + + // The initial published state. + isPublished?: boolean; + + // The initial schema properties. + properties?: SchemaPropertiesDto; +}>; + +export type UpdateSchemaCategoryDto = Readonly<{ + // The name of the category. + name?: string; +}>; + +export type UpdateFieldDto = Readonly<{ + // The field properties. + properties: FieldPropertiesDto; +}>; + +export type SynchronizeSchemaDto = Readonly<{ + // True, to not delete fields when synchronizing. + noFieldDeletiong?: boolean; + + // True, to not recreate fields when synchronizing. + noFieldRecreation?: boolean; + + // The additional properties. + [key: string]: any; +}>; + +export type UpdateSchemaDto = Readonly<{ + // The label of the schema. + label?: string; + + // The hints to explain the schema. + hints?: string; + + // The URL to the contents sidebar plugin. + contentsSidebarUrl?: string; + + // The URL to the content sidebar plugin. + contentSidebarUrl?: string; + + // The URL to an editor to replace the editor. + contentEditorUrl?: string; + + // True, if the content should be validated on publishing. + validateOnPublish?: boolean; + + // The tags. + tags?: ReadonlyArray; +}>; @Injectable() export class SchemasService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -417,9 +507,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'Created', appName); - }), pretifyError('i18n:schemas.createFailed')); } @@ -432,9 +519,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'ScriptsConfigured', appName); - }), pretifyError('i18n:schemas.updateScriptsFailed')); } @@ -447,9 +531,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'RulesConfigured', appName); - }), pretifyError('i18n:schemas.updateRulesFailed')); } @@ -462,9 +543,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'Updated', appName); - }), pretifyError('i18n:schemas.synchronizeFailed')); } @@ -477,9 +555,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'Updated', appName); - }), pretifyError('i18n:schemas.updateFailed')); } @@ -492,9 +567,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'CategoryChanged', appName); - }), pretifyError('i18n:schemas.changeCategoryFailed')); } @@ -507,9 +579,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'PreviewUrlsConfigured', appName); - }), pretifyError('i18n:schemas.updatePreviewUrlsFailed')); } @@ -522,9 +591,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'Published', appName); - }), pretifyError('i18n:schemas.publishFailed')); } @@ -537,9 +603,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'Unpublished', appName); - }), pretifyError('i18n:schemas.unpublishFailed')); } @@ -552,9 +615,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldCreated', appName); - }), pretifyError('i18n:schemas.addFieldFailed')); } @@ -567,9 +627,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'UIFieldsConfigured', appName); - }), pretifyError('i18n:schemas.updateUIFieldsFailed')); } @@ -582,9 +639,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldsReordered', appName); - }), pretifyError('i18n:schemas.reorderFieldsFailed')); } @@ -597,9 +651,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldUpdated', appName); - }), pretifyError('i18n:schemas.updateFieldFailed')); } @@ -612,9 +663,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldLocked', appName); - }), pretifyError('i18n:schemas.lockFieldFailed')); } @@ -627,9 +675,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldEnabled', appName); - }), pretifyError('i18n:schemas.enableFieldFailed')); } @@ -642,9 +687,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldDisabled', appName); - }), pretifyError('i18n:schemas.disableFieldFailed')); } @@ -657,9 +699,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldShown', appName); - }), pretifyError('i18n:schemas.showFieldFailed')); } @@ -672,9 +711,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldHidden', appName); - }), pretifyError('i18n:schemas.hideFieldFailed')); } @@ -687,9 +723,6 @@ export class SchemasService { map(({ payload }) => { return parseSchema(payload.body); }), - tap(() => { - this.analytics.trackEvent('Schema', 'FieldDeleted', appName); - }), pretifyError('i18n:schemas.deleteFieldFailed')); } @@ -699,9 +732,6 @@ export class SchemasService { const url = this.apiUrl.buildUrl(link.href); return HTTP.requestVersioned(this.http, link.method, url, version).pipe( - tap(() => { - this.analytics.trackEvent('Schema', 'Deleted', appName); - }), pretifyError('i18n:schemas.deleteFailed')); } @@ -719,11 +749,12 @@ export class SchemasService { } function parseSchemas(response: { items: any[] } & Resource) { - const items = response.items.map(parseSchema); + const { items: list, _links } = response; + const items = list.map(parseSchema); - const _links = response._links; + const canCreate = hasAnyLink(_links, 'create'); - return { items, _links, canCreate: hasAnyLink(_links, 'create') }; + return { items, canCreate }; } function parseSchema(response: any) { diff --git a/frontend/src/app/shared/services/schemas.spec.ts b/frontend/src/app/shared/services/schemas.spec.ts index 5b7d19f69..286c9e690 100644 --- a/frontend/src/app/shared/services/schemas.spec.ts +++ b/frontend/src/app/shared/services/schemas.spec.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { createProperties, MetaFields, SchemaPropertiesDto } from '@app/shared/internal'; +import { createProperties, META_FIELDS, SchemaPropertiesDto } from '@app/shared/internal'; import { TestValues } from './../state/_test-helpers'; const { @@ -58,10 +58,10 @@ describe('SchemaDto', () => { const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); expect(schema.defaultListFields.map(x => x.name)).toEqual([ - MetaFields.lastModifiedByAvatar.name, + META_FIELDS.lastModifiedByAvatar.name, field1.name, - MetaFields.statusColor.name, - MetaFields.lastModified.name, + META_FIELDS.statusColor.name, + META_FIELDS.lastModified.name, ]); }); @@ -69,10 +69,10 @@ describe('SchemaDto', () => { const schema = createSchema({ properties: new SchemaPropertiesDto() }); expect(schema.defaultListFields.map(x => x.name)).toEqual([ - MetaFields.lastModifiedByAvatar.name, - MetaFields.empty.name, - MetaFields.statusColor.name, - MetaFields.lastModified.name, + META_FIELDS.lastModifiedByAvatar.name, + META_FIELDS.empty.name, + META_FIELDS.statusColor.name, + META_FIELDS.lastModified.name, ]); }); @@ -88,7 +88,7 @@ describe('SchemaDto', () => { const schema = createSchema({ properties: new SchemaPropertiesDto() }); expect(schema.defaultReferenceFields.map(x => x.name)).toEqual([ - MetaFields.empty.name, + META_FIELDS.empty.name, ]); }); }); diff --git a/frontend/src/app/shared/services/search.service.ts b/frontend/src/app/shared/services/search.service.ts index 9b427be28..c7dab84c4 100644 --- a/frontend/src/app/shared/services/search.service.ts +++ b/frontend/src/app/shared/services/search.service.ts @@ -46,13 +46,15 @@ export class SearchService { } } -function parseResults(body: any[]) { - const results = body.map(item => new SearchResultDto( - item._links, - item.name, - item.type, - item.label)); - - return results; +function parseResults(response: any[]) { + return response.map(parseResult); +} + +function parseResult(response: any) { + return new SearchResultDto( + response._links, + response.name, + response.type, + response.label); } diff --git a/frontend/src/app/shared/services/stock-photo.service.ts b/frontend/src/app/shared/services/stock-photo.service.ts index 823734692..2a766f5ca 100644 --- a/frontend/src/app/shared/services/stock-photo.service.ts +++ b/frontend/src/app/shared/services/stock-photo.service.ts @@ -37,12 +37,15 @@ export class StockPhotoService { catchError(() => of([]))); } } -function parseImages(body: any[]) { - return body.map(x => new StockPhotoDto( - x.url, - x.thumbUrl, - x.user, - x.userProfileUrl, - )); +function parseImages(response: any[]) { + return response.map(parseImage); +} + +function parseImage(response: any) { + return new StockPhotoDto( + response.url, + response.thumbUrl, + response.user, + response.userProfileUrl); } diff --git a/frontend/src/app/shared/services/templates.service.spec.ts b/frontend/src/app/shared/services/templates.service.spec.ts index e5bc50e64..ccb11a657 100644 --- a/frontend/src/app/shared/services/templates.service.spec.ts +++ b/frontend/src/app/shared/services/templates.service.spec.ts @@ -46,11 +46,12 @@ describe('TemplatesService', () => { ], }); - expect(templates!).toEqual( - new TemplatesDto(2, [ + expect(templates!).toEqual({ + items: [ createTemplate(1), createTemplate(2), - ], {})); + ], + }); })); it('should make get request to get template', diff --git a/frontend/src/app/shared/services/templates.service.ts b/frontend/src/app/shared/services/templates.service.ts index 7faf23713..8009c6ae4 100644 --- a/frontend/src/app/shared/services/templates.service.ts +++ b/frontend/src/app/shared/services/templates.service.ts @@ -9,14 +9,11 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, pretifyError, Resource, ResourceLinks, ResultSet } from '@app/framework'; - -export class TemplatesDto extends ResultSet { -} +import { ApiUrlConfig, pretifyError, Resource, ResourceLinks } from '@app/framework'; export class TemplateDto { public readonly _links: ResourceLinks; - + constructor(links: ResourceLinks, public readonly name: string, public readonly title: string, @@ -37,6 +34,11 @@ export class TemplateDetailsDto { } } +export type TemplatesDto = Readonly<{ + // The list of templates. + items: ReadonlyArray; +}>; + @Injectable() export class TemplatesService { constructor( @@ -68,15 +70,19 @@ export class TemplatesService { } } -function parseTemplates(response: { items: any[] } & Resource) { - const items = response.items.map(item => - new TemplateDto(item._links, - item.name, - item.title, - item.description, - item.isStarter)); +function parseTemplates(response: { items: any[] } & Resource): TemplatesDto { + const { items: list } = response; + const items = list.map(parseTemplate); + + return { items }; +} - return new TemplatesDto(items.length, items, response._links); +function parseTemplate(response: any & Resource) { + return new TemplateDto(response._links, + response.name, + response.title, + response.description, + response.isStarter); } function parseTemplateDetails(response: any & Resource) { diff --git a/frontend/src/app/shared/services/translations.service.ts b/frontend/src/app/shared/services/translations.service.ts index d456aabdb..30b3471f7 100644 --- a/frontend/src/app/shared/services/translations.service.ts +++ b/frontend/src/app/shared/services/translations.service.ts @@ -19,8 +19,16 @@ export class TranslationDto { } } -export type TranslateDto = - Readonly<{ text: string; sourceLanguage: string; targetLanguage: string }>; +export type TranslateDto = Readonly<{ + // The text to translate. + text: string; + + // The source language. + sourceLanguage: string; + + // The target language. + targetLanguage: string; + }>; @Injectable() export class TranslationsService { diff --git a/frontend/src/app/shared/services/ui.service.ts b/frontend/src/app/shared/services/ui.service.ts index 1c5bec011..3247f9a3c 100644 --- a/frontend/src/app/shared/services/ui.service.ts +++ b/frontend/src/app/shared/services/ui.service.ts @@ -11,8 +11,10 @@ import { Observable, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { ApiUrlConfig } from '@app/framework'; -export type UISettingsDto = - Readonly<{ canCreateApps: boolean }>; +export type UISettingsDto = Readonly<{ + // True, if the user has the permissions to create a new app. + canCreateApps?: boolean; +}>; @Injectable() export class UIService { diff --git a/frontend/src/app/shared/services/users.service.spec.ts b/frontend/src/app/shared/services/users.service.spec.ts index ddbf9d328..821172efd 100644 --- a/frontend/src/app/shared/services/users.service.spec.ts +++ b/frontend/src/app/shared/services/users.service.spec.ts @@ -7,7 +7,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { ApiUrlConfig, ResourcesDto, UserDto, UsersService } from '@app/shared/internal'; +import { ApiUrlConfig, Resource, UserDto, UsersService } from '@app/shared/internal'; describe('UsersService', () => { beforeEach(() => { @@ -108,7 +108,7 @@ describe('UsersService', () => { it('should make get request to get resources', inject([UsersService, HttpTestingController], (usersService: UsersService, httpMock: HttpTestingController) => { - let resources: ResourcesDto; + let resources: Resource; usersService.getResources().subscribe(result => { resources = result; @@ -125,10 +125,10 @@ describe('UsersService', () => { }, }); - const expected = new ResourcesDto({ - schemas: { method: 'GET', href: '/api/schemas' }, + expect(resources!).toEqual({ + _links: { + schemas: { method: 'GET', href: '/api/schemas' }, + }, }); - - expect(resources!).toEqual(expected); })); }); diff --git a/frontend/src/app/shared/services/users.service.ts b/frontend/src/app/shared/services/users.service.ts index e08c64ea2..e6927e764 100644 --- a/frontend/src/app/shared/services/users.service.ts +++ b/frontend/src/app/shared/services/users.service.ts @@ -9,7 +9,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiUrlConfig, pretifyError, ResourceLinks } from '@app/framework'; +import { ApiUrlConfig, pretifyError, Resource } from '@app/framework'; export class UserDto { constructor( @@ -19,14 +19,6 @@ export class UserDto { } } -export class ResourcesDto { - public readonly _links: ResourceLinks; - - constructor(links: ResourceLinks) { - this._links = links; - } -} - @Injectable() export class UsersService { constructor( @@ -55,13 +47,10 @@ export class UsersService { pretifyError('i18n:users.loadUserFailed')); } - public getResources(): Observable { + public getResources(): Observable { const url = this.apiUrl.buildUrl('api'); - return this.http.get<{ _links: {} }>(url).pipe( - map(({ _links }) => { - return new ResourcesDto(_links); - }), + return this.http.get(url).pipe( pretifyError('i18n:users.loadUserFailed')); } } diff --git a/frontend/src/app/shared/services/workflows.service.spec.ts b/frontend/src/app/shared/services/workflows.service.spec.ts index f01c77722..0299b67df 100644 --- a/frontend/src/app/shared/services/workflows.service.spec.ts +++ b/frontend/src/app/shared/services/workflows.service.spec.ts @@ -9,7 +9,7 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; -import { AnalyticsService, ApiUrlConfig, Resource, Version, WorkflowDto, WorkflowsDto, WorkflowsPayload, WorkflowsService } from '@app/shared/internal'; +import { ApiUrlConfig, Resource, Version, WorkflowDto, WorkflowsDto, WorkflowsPayload, WorkflowsService } from '@app/shared/internal'; describe('WorkflowsService', () => { const version = new Version('1'); @@ -22,7 +22,6 @@ describe('WorkflowsService', () => { providers: [ WorkflowsService, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, - { provide: AnalyticsService, useValue: new AnalyticsService() }, ], }); }); @@ -178,14 +177,11 @@ describe('WorkflowsService', () => { export function createWorkflows(...names: ReadonlyArray): WorkflowsPayload { return { + items: names.map(createWorkflow), errors: [ 'Error1', 'Error2', ], - items: names.map(createWorkflow), - _links: { - create: { method: 'POST', href: '/workflows' }, - }, canCreate: true, }; } @@ -510,7 +506,7 @@ describe('Workflow', () => { it('should rename workflow', () => { const workflow = new WorkflowDto({}, 'id') - .rename('name'); + .changeName('name'); expect(workflow.serialize()).toEqual({ name: 'name', schemaIds: [], steps: {}, initial: null }); }); diff --git a/frontend/src/app/shared/services/workflows.service.ts b/frontend/src/app/shared/services/workflows.service.ts index 35da27322..20faeb4e5 100644 --- a/frontend/src/app/shared/services/workflows.service.ts +++ b/frontend/src/app/shared/services/workflows.service.ts @@ -8,19 +8,17 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { AnalyticsService, ApiUrlConfig, compareStrings, hasAnyLink, HTTP, mapVersioned, Model, pretifyError, Resource, ResourceLinks, StringHelper, Version, Versioned } from '@app/framework'; +import { ApiUrlConfig, compareStrings, hasAnyLink, HTTP, mapVersioned, pretifyError, Resource, ResourceLinks, StringHelper, Version, Versioned } from '@app/framework'; -export class WorkflowDto extends Model { +export class WorkflowDto { public readonly _links: ResourceLinks; - public readonly canUpdate: boolean; public readonly canDelete: boolean; + public readonly canUpdate: boolean; public readonly displayName: string; - constructor( - links: ResourceLinks, + constructor(links: ResourceLinks, public readonly id: string, public readonly name: string | null = null, public readonly initial: string | null = null, @@ -28,22 +26,22 @@ export class WorkflowDto extends Model { public readonly steps: WorkflowStep[] = [], public readonly transitions: WorkflowTransition[] = [], ) { - super(); - - this.onCloned(); - this._links = links; - this.canUpdate = hasAnyLink(links, 'update'); this.canDelete = hasAnyLink(links, 'delete'); + this.canUpdate = hasAnyLink(links, 'update'); this.displayName = StringHelper.firstNonEmpty(name, 'i18n:workflows.notNamed'); } protected onCloned() { - this.steps.sort((a, b) => compareStrings(a.name, b.name)); + this.steps.sort((a, b) => { + return compareStrings(a.name, b.name); + }); - this.transitions.sort((a, b) => compareStrings(a.to, b.to)); + this.transitions.sort((a, b) => { + return compareStrings(a.to, b.to); + }); } public getOpenSteps(step: WorkflowStep) { @@ -116,7 +114,7 @@ export class WorkflowDto extends Model { return this.with({ schemaIds }); } - public rename(name: string) { + public changeName(name: string) { return this.with({ name }); } @@ -173,7 +171,6 @@ export class WorkflowDto extends Model { const s = { ...values, transitions: {} }; for (const transition of this.getTransitions(step)) { - // eslint-disable-next-line @typescript-eslint/naming-convention const { to, step: _, from: __, ...t } = transition; s.transitions[to] = t; @@ -184,38 +181,85 @@ export class WorkflowDto extends Model { return result; } + + private with(update: Partial) { + const clone = Object.assign(Object.assign(Object.create(Object.getPrototypeOf(this)), this), update); + + clone.onCloned(); + + return clone; + } } -export type WorkflowStepValues = - Readonly<{ color?: string; isLocked?: boolean; validate?: boolean; noUpdate?: boolean; noUpdateExpression?: string; noUpdateRoles?: ReadonlyArray }>; +export type WorkflowsDto = Versioned; -export type WorkflowStep = - Readonly<{ name: string } & WorkflowStepValues>; +export type WorkflowsPayload = Readonly<{ + // The list of workflows. + items: WorkflowDto[]; -export type WorkflowTransitionValues = - Readonly<{ expression?: string; roles?: string[] }>; + // The validations errors. + errors: string[]; -export type WorkflowTransition = - Readonly<{ from: string; to: string } & WorkflowTransitionValues>; + // True, if the user has permissions to create a new workflow. + canCreate?: boolean; +}>; -export type WorkflowTransitionView = - Readonly<{ step: WorkflowStep } & WorkflowTransition>; +export type WorkflowStepValues = Readonly<{ + // The color of the step. + color?: string; -export type WorkflowsDto = - Versioned; + // True, if the step cannot be removed. + isLocked?: boolean; -export type WorkflowsPayload = - Readonly<{ items: WorkflowDto[]; errors: string[]; canCreate: boolean } & Resource>; + // True, if the content should be validated on this step. + validate?: boolean; -export type CreateWorkflowDto = - Readonly<{ name: string }>; + // True, when the step has an update restriction. + noUpdate?: boolean; + + // The expression when updates are not allowed. + noUpdateExpression?: string; + + // The user roles which cannot update a content. + noUpdateRoles?: ReadonlyArray; +}>; + +export type WorkflowStep = Readonly<{ + // The name of the workflow. + name: string; +} & WorkflowStepValues>; + +export type WorkflowTransitionValues = Readonly<{ + // The expression when a transition is possible. + expression?: string; + + // The user roles which can transition to this step. + roles?: string[]; +}>; + +export type WorkflowTransition = Readonly<{ + // The source step name. + from: string; + + // The target step name. + to: string; +} & WorkflowTransitionValues>; + +export type WorkflowTransitionView = Readonly<{ + // The actual workflow step. + step: WorkflowStep; +} & WorkflowTransition>; + +export type CreateWorkflowDto = Readonly<{ + // The name of the workflow. + name: string; +}>; @Injectable() export class WorkflowsService { constructor( private readonly http: HttpClient, private readonly apiUrl: ApiUrlConfig, - private readonly analytics: AnalyticsService, ) { } @@ -236,9 +280,6 @@ export class WorkflowsService { mapVersioned(({ body }) => { return parseWorkflows(body); }), - tap(() => { - this.analytics.trackEvent('Workflow', 'Created', appName); - }), pretifyError('i18n:workflows.createFailed')); } @@ -251,9 +292,6 @@ export class WorkflowsService { mapVersioned(({ body }) => { return parseWorkflows(body); }), - tap(() => { - this.analytics.trackEvent('Workflow', 'Updated', appName); - }), pretifyError('i18n:workflows.updateFailed')); } @@ -266,19 +304,17 @@ export class WorkflowsService { mapVersioned(({ body }) => { return parseWorkflows(body); }), - tap(() => { - this.analytics.trackEvent('Workflow', 'Deleted', appName); - }), pretifyError('i18n:workflows.deleteFailed')); } } function parseWorkflows(response: { items: any[]; errors: string[] } & Resource) { - const items = response.items.map(parseWorkflow); + const { items: list, errors, _links } = response; + const items = list.map(parseWorkflow); - const { errors, _links } = response; + const canCreate = hasAnyLink(_links, 'create'); - return { errors, items, _links, canCreate: hasAnyLink(_links, 'create') }; + return { items, errors, canCreate }; } function parseWorkflow(workflow: any) { diff --git a/frontend/src/app/shared/state/asset-uploader.state.ts b/frontend/src/app/shared/state/asset-uploader.state.ts index 3fc37ee46..6c94348a2 100644 --- a/frontend/src/app/shared/state/asset-uploader.state.ts +++ b/frontend/src/app/shared/state/asset-uploader.state.ts @@ -104,7 +104,7 @@ export class AssetUploaderState extends State { } else { return event; } - }), shareReplay()); + }), shareReplay()); stream.subscribe({ next: event => { diff --git a/frontend/src/app/shared/state/assets.state.spec.ts b/frontend/src/app/shared/state/assets.state.spec.ts index 52b6469b1..ada04e4a7 100644 --- a/frontend/src/app/shared/state/assets.state.spec.ts +++ b/frontend/src/app/shared/state/assets.state.spec.ts @@ -9,7 +9,7 @@ import { of, throwError } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; import { ErrorDto } from '@app/framework'; -import { AssetFoldersDto, AssetsDto, AssetsService, AssetsState, DialogService, MathHelper, versioned } from '@app/shared/internal'; +import { AssetsService, AssetsState, DialogService, MathHelper, versioned } from '@app/shared/internal'; import { createAsset, createAssetFolder } from './../services/assets.service.spec'; import { TestValues } from './_test-helpers'; @@ -49,12 +49,12 @@ describe('AssetsState', () => { describe('Loading', () => { beforeEach(() => { assetsService.setup(x => x.getAssetFolders(app, MathHelper.EMPTY_GUID, 'PathAndItems')) - .returns(() => of(new AssetFoldersDto(2, [assetFolder1, assetFolder2], []))).verifiable(Times.atLeastOnce()); + .returns(() => of({ items: [assetFolder1, assetFolder2], path: [] })).verifiable(Times.atLeastOnce()); }); it('should load assets', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable(); + .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); assetsState.load().subscribe(); @@ -68,7 +68,7 @@ describe('AssetsState', () => { it('should show notification on load if reload is true', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable(); + .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); assetsState.load(true).subscribe(); @@ -79,7 +79,7 @@ describe('AssetsState', () => { it('should load with total', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: false })) - .returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable(); + .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); assetsState.load(true, false).subscribe(); @@ -90,10 +90,10 @@ describe('AssetsState', () => { it('should load without tags if tag untoggled', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1'], noSlowTotal: true })) - .returns(() => of(new AssetsDto(0, []))).verifiable(); + .returns(() => of({ items: [], total: 0 })).verifiable(); assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of(new AssetsDto(0, []))).verifiable(); + .returns(() => of({ items: [], total: 0 })).verifiable(); assetsState.toggleTag('tag1').subscribe(); assetsState.toggleTag('tag1').subscribe(); @@ -103,7 +103,7 @@ describe('AssetsState', () => { it('should load without tags if tags reset', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of(new AssetsDto(0, []))).verifiable(); + .returns(() => of({ items: [], total: 0 })).verifiable(); assetsState.resetTags().subscribe(); @@ -112,7 +112,7 @@ describe('AssetsState', () => { it('should load with new pagination if paging', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 30, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of(new AssetsDto(200, []))).verifiable(); + .returns(() => of({ items: [], total: 200 })).verifiable(); assetsState.page({ page: 1, pageSize: 30 }).subscribe(); @@ -121,10 +121,10 @@ describe('AssetsState', () => { it('should skip page size if loaded before', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable(); + .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); assetsService.setup(x => x.getAssets(app, { take: 30, skip: 30, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true, noTotal: true })) - .returns(() => of(new AssetsDto(200, []))).verifiable(); + .returns(() => of({ items: [], total: 200 })).verifiable(); assetsState.load().subscribe(); assetsState.page({ page: 1, pageSize: 30 }).subscribe(); @@ -136,10 +136,10 @@ describe('AssetsState', () => { describe('Navigating', () => { it('should load with parent id', () => { assetsService.setup(x => x.getAssetFolders(app, '123', 'PathAndItems')) - .returns(() => of(new AssetFoldersDto(2, [assetFolder1, assetFolder2], []))).verifiable(); + .returns(() => of({ items: [assetFolder1, assetFolder2], path: [] })).verifiable(); assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: '123', noSlowTotal: true })) - .returns(() => of(new AssetsDto(200, []))).verifiable(); + .returns(() => of({ items: [], total: 200 })).verifiable(); assetsState.navigate('123').subscribe(); @@ -150,7 +150,7 @@ describe('AssetsState', () => { describe('Searching', () => { it('should load with tags if tag toggled', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1'], noSlowTotal: true })) - .returns(() => of(new AssetsDto(0, []))).verifiable(); + .returns(() => of({ items: [], total: 0 })).verifiable(); assetsState.toggleTag('tag1').subscribe(); @@ -159,7 +159,7 @@ describe('AssetsState', () => { it('should load with tags if tags selected', () => { assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, tags: ['tag1', 'tag2'], noSlowTotal: true })) - .returns(() => of(new AssetsDto(0, []))).verifiable(); + .returns(() => of({ items: [], total: 0 })).verifiable(); assetsState.selectTags(['tag1', 'tag2']).subscribe(); @@ -170,7 +170,7 @@ describe('AssetsState', () => { const query = { fullText: 'my-query' }; assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, query, noSlowTotal: true })) - .returns(() => of(new AssetsDto(0, []))).verifiable(); + .returns(() => of({ items: [], total: 0 })).verifiable(); assetsState.search(query).subscribe(); @@ -181,7 +181,7 @@ describe('AssetsState', () => { const query = { fullText: 'my-query' }; assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, query, noSlowTotal: true })) - .returns(() => of(new AssetsDto(0, []))).verifiable(); + .returns(() => of({ items: [], total: 0 })).verifiable(); assetsState.next({ ref: '1' }); assetsState.search(query).subscribe(); @@ -194,13 +194,13 @@ describe('AssetsState', () => { describe('Updates', () => { beforeEach(() => { assetsService.setup(x => x.getAssetFolders(app, MathHelper.EMPTY_GUID, 'PathAndItems')) - .returns(() => of(new AssetFoldersDto(2, [assetFolder1, assetFolder2], []))); + .returns(() => of({ items: [assetFolder1, assetFolder2], path: [] })); assetsService.setup(x => x.getAssets(app, { take: 30, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of(new AssetsDto(200, [asset1, asset2]))).verifiable(); + .returns(() => of({ items: [asset1, asset2], total: 200 })).verifiable(); assetsService.setup(x => x.getAssets(app, { take: 2, skip: 0, parentId: MathHelper.EMPTY_GUID, noSlowTotal: true })) - .returns(() => of(new AssetsDto(200, [asset1, asset2]))); + .returns(() => of({ items: [asset1, asset2], total: 200 })); assetsState.load(true).subscribe(); }); diff --git a/frontend/src/app/shared/state/backups.state.spec.ts b/frontend/src/app/shared/state/backups.state.spec.ts index af6ac2693..c0a708bf0 100644 --- a/frontend/src/app/shared/state/backups.state.spec.ts +++ b/frontend/src/app/shared/state/backups.state.spec.ts @@ -8,7 +8,7 @@ import { of, throwError } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; -import { BackupsDto, BackupsService, BackupsState, DialogService } from '@app/shared/internal'; +import { BackupsService, BackupsState, DialogService } from '@app/shared/internal'; import { createBackup } from './../services/backups.service.spec'; import { TestValues } from './_test-helpers'; @@ -39,7 +39,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({ items: [backup1, backup2] } as any)).verifiable(); backupsState.load().subscribe(); @@ -61,7 +61,7 @@ describe('BackupsState', () => { it('should show notification on load if reload is true', () => { backupsService.setup(x => x.getBackups(app)) - .returns(() => of(new BackupsDto(2, [backup1, backup2], {}))).verifiable(); + .returns(() => of({ items: [backup1, backup2] } as any)).verifiable(); backupsState.load(true, false).subscribe(); @@ -96,7 +96,7 @@ describe('BackupsState', () => { describe('Updates', () => { beforeEach(() => { backupsService.setup(x => x.getBackups(app)) - .returns(() => of(new BackupsDto(2, [backup1, backup2], {}))).verifiable(); + .returns(() => of({ items: [backup1, backup2] } as any)).verifiable(); backupsState.load().subscribe(); }); diff --git a/frontend/src/app/shared/state/comments.state.ts b/frontend/src/app/shared/state/comments.state.ts index e58609ce8..a57b8ce51 100644 --- a/frontend/src/app/shared/state/comments.state.ts +++ b/frontend/src/app/shared/state/comments.state.ts @@ -99,7 +99,7 @@ export class CommentsState extends State { public update(comment: CommentDto, text: string, now?: DateTime): Observable { return this.commentsService.putComment(this.commentsUrl, comment.id, { text }).pipe( - map(() => update(comment, text, now || DateTime.now())), + map(() => update(comment, text, now)), tap(updated => { this.next(s => { const comments = s.comments.replacedBy('id', updated); @@ -115,5 +115,10 @@ export class CommentsState extends State { } } -const update = (comment: CommentDto, text: string, time: DateTime) => - comment.with({ text, time }); +const update = (comment: CommentDto, text: string, time?: DateTime) => + new CommentDto( + comment.id, + time || DateTime.now(), + text, + comment.url, + comment.user); diff --git a/frontend/src/app/shared/state/contents.forms-helpers.ts b/frontend/src/app/shared/state/contents.forms-helpers.ts index 3fbe3ec65..245ed28ec 100644 --- a/frontend/src/app/shared/state/contents.forms-helpers.ts +++ b/frontend/src/app/shared/state/contents.forms-helpers.ts @@ -73,7 +73,7 @@ export function fieldTranslationStatus(data: any) { for (const [key, value] of Object.entries(data)) { result[key] = isValidValue(value); } - + return result; } diff --git a/frontend/src/app/shared/state/contents.forms.spec.ts b/frontend/src/app/shared/state/contents.forms.spec.ts index 5704efd8c..44d3dcb25 100644 --- a/frontend/src/app/shared/state/contents.forms.spec.ts +++ b/frontend/src/app/shared/state/contents.forms.spec.ts @@ -109,14 +109,14 @@ describe('TranslationStatus', () => { field1: { en: 'en', de: 'de', - }, + }, field2: { en: 'en', de: 'de', - }, + }, field3: { en: 'en', - }, + }, }; const result = contentTranslationStatus(data, schema, languages as any); @@ -145,23 +145,23 @@ describe('TranslationStatus', () => { field1: { en: 'en', de: 'de', - }, + }, field2: { en: 'en', de: 'de', - }, + }, field3: { en: 'en', - }, + }, }; const data2 = { field1: { de: 'de', - }, + }, field3: { en: 'en', - }, + }, }; const result = contentsTranslationStatus([data1, data2], schema, languages as any); diff --git a/frontend/src/app/shared/state/contents.state.ts b/frontend/src/app/shared/state/contents.state.ts index 3d8fc6f01..d8c538eee 100644 --- a/frontend/src/app/shared/state/contents.state.ts +++ b/frontend/src/app/shared/state/contents.state.ts @@ -352,7 +352,7 @@ export abstract class ContentsStateBase extends State { s.selectedContent : content; } - + return { ...s, contents, selectedContent }; }); }), diff --git a/frontend/src/app/shared/state/contributors.state.ts b/frontend/src/app/shared/state/contributors.state.ts index 28be99ac6..c7f79854c 100644 --- a/frontend/src/app/shared/state/contributors.state.ts +++ b/frontend/src/app/shared/state/contributors.state.ts @@ -139,7 +139,7 @@ export class ContributorsState extends State { tap(({ version, payload }) => { this.replaceContributors(version, payload); }), - shareMapSubscribed(this.dialogs, x => x.payload._meta && x.payload._meta['isInvited'] === '1', options)); + shareMapSubscribed(this.dialogs, x => x.payload.isInvited, options)); } private replaceContributors(version: Version, { canCreate, items, maxContributors }: ContributorsPayload) { diff --git a/frontend/src/app/shared/state/resolvers.spec.ts b/frontend/src/app/shared/state/resolvers.spec.ts index 341339b6a..8e0ce5a5e 100644 --- a/frontend/src/app/shared/state/resolvers.spec.ts +++ b/frontend/src/app/shared/state/resolvers.spec.ts @@ -8,7 +8,7 @@ import { firstValueFrom, of, throwError } from 'rxjs'; import { IMock, Mock, Times } from 'typemoq'; import { UIOptions } from '@app/framework'; -import { ContentsDto, ContentsService } from '../services/contents.service'; +import { ContentsService } from '../services/contents.service'; import { createContent } from '../services/contents.service.spec'; import { TestValues } from './_test-helpers'; import { ResolveContents } from './resolvers'; @@ -42,7 +42,7 @@ describe('ResolveContents', () => { const ids = ['id1', 'id2']; contentsService.setup(x => x.getAllContents(app, { ids })) - .returns(() => of(new ContentsDto([], 2, [contents[0], contents[1]]))); + .returns(() => of({ items: [contents[0], contents[1]] } as any)); return expectAsync(firstValueFrom(contentsResolver.resolveMany(ids))).toBePending(); }); @@ -51,7 +51,7 @@ describe('ResolveContents', () => { const ids = ['id1', 'id2']; contentsService.setup(x => x.getAllContents(app, { ids })) - .returns(() => of(new ContentsDto([], 2, [contents[0], contents[1]]))); + .returns(() => of({ items: [contents[0], contents[1]] } as any)); const result = await firstValueFrom(contentsResolver.resolveMany(ids)); @@ -65,7 +65,7 @@ describe('ResolveContents', () => { const ids = ['id1', 'id2']; contentsService.setup(x => x.getAllContents(app, { ids })) - .returns(() => of(new ContentsDto([], 2, [contents[0]]))); + .returns(() => of({ items: [contents[0]] } as any)); const result = await firstValueFrom(contentsResolver.resolveMany(ids)); @@ -90,7 +90,7 @@ describe('ResolveContents', () => { const ids = ['id1', 'id2', 'id3']; contentsService.setup(x => x.getAllContents(app, { ids })) - .returns(() => of(new ContentsDto([], 2, [contents[0], contents[1], contents[2]]))); + .returns(() => of({ items: [contents[0], contents[1], contents[2]] } as any)); const result1Promise = firstValueFrom(contentsResolver.resolveMany(ids1)); const result2Promise = firstValueFrom(contentsResolver.resolveMany(ids2)); @@ -114,7 +114,7 @@ describe('ResolveContents', () => { const ids = ['id1', 'id2']; contentsService.setup(x => x.getAllContents(app, { ids })) - .returns(() => of(new ContentsDto([], 2, [contents[0], contents[1]]))); + .returns(() => of({ items: [contents[0], contents[1]] } as any)); const result1Promise = firstValueFrom(contentsResolver.resolveMany(ids)); const result2Promise = firstValueFrom(contentsResolver.resolveMany(ids)); @@ -138,7 +138,7 @@ describe('ResolveContents', () => { const ids = ['id1', 'id2']; contentsService.setup(x => x.getAllContents(app, { ids })) - .returns(() => of(new ContentsDto([], 2, [contents[0], contents[1]]))); + .returns(() => of({ items: [contents[0], contents[1]] } as any)); const result1 = await firstValueFrom(contentsResolver.resolveMany(ids)); const result2 = await firstValueFrom(contentsResolver.resolveMany(ids)); @@ -160,7 +160,7 @@ describe('ResolveContents', () => { const schema = 'schema1'; contentsService.setup(x => x.getContents(app, schema, { take: 100 })) - .returns(() => of(new ContentsDto([], 2, [contents[0]]))); + .returns(() => of({ items: [contents[0]] } as any)); const result = await firstValueFrom(contentsResolver.resolveAll('schema1')); @@ -173,7 +173,7 @@ describe('ResolveContents', () => { const schema = 'schema1'; contentsService.setup(x => x.getContents(app, schema, { take: 100 })) - .returns(() => of(new ContentsDto([], 2, [contents[0]]))); + .returns(() => of({ items: [contents[0]] } as any)); const result1Promise = await firstValueFrom(contentsResolver.resolveAll('schema1')); const result2Promise = await firstValueFrom(contentsResolver.resolveAll('schema1')); @@ -193,7 +193,7 @@ describe('ResolveContents', () => { const schema = 'schema1'; contentsService.setup(x => x.getContents(app, schema, { take: 100 })) - .returns(() => of(new ContentsDto([], 2, [contents[0]]))); + .returns(() => of({ items: [contents[0]] } as any)); const result1 = await firstValueFrom(contentsResolver.resolveAll('schema1')); const result2 = await firstValueFrom(contentsResolver.resolveAll('schema1')); diff --git a/frontend/src/app/shared/state/resolvers.ts b/frontend/src/app/shared/state/resolvers.ts index b5b83a7c0..959eeba56 100644 --- a/frontend/src/app/shared/state/resolvers.ts +++ b/frontend/src/app/shared/state/resolvers.ts @@ -129,8 +129,8 @@ export class ResolveContents extends ResolverBase { return result; } - protected createResult(items: ContentDto[]) { - return new ContentsDto([], items.length, items); + protected createResult(items: ContentDto[]): ContentsDto { + return { items, total: items.length } as any; } protected loadMany(ids: string[]) { @@ -151,8 +151,8 @@ export class ResolveAssets extends ResolverBase { super(); } - protected createResult(items: AssetDto[]) { - return new AssetsDto(items.length, items); + protected createResult(items: AssetDto[]): AssetsDto { + return { items, total: items.length } as any; } protected loadMany(ids: string[]) { diff --git a/frontend/src/app/shared/state/rule-events.state.spec.ts b/frontend/src/app/shared/state/rule-events.state.spec.ts index e27e2471f..8ee203d82 100644 --- a/frontend/src/app/shared/state/rule-events.state.spec.ts +++ b/frontend/src/app/shared/state/rule-events.state.spec.ts @@ -8,7 +8,7 @@ import { of, throwError } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, RuleEventsDto, RuleEventsState, RulesService } from '@app/shared/internal'; +import { DialogService, RuleEventsState, RulesService } from '@app/shared/internal'; import { createRuleEvent } from './../services/rules.service.spec'; import { TestValues } from './_test-helpers'; @@ -32,7 +32,7 @@ describe('RuleEventsState', () => { rulesService = Mock.ofType(); rulesService.setup(x => x.getEvents(app, 30, 0, undefined)) - .returns(() => of(new RuleEventsDto(200, oldRuleEvents))); + .returns(() => of({ items: oldRuleEvents, total: 200 } as any)); ruleEventsState = new RuleEventsState(appsState.object, dialogs.object, rulesService.object); ruleEventsState.load().subscribe(); @@ -66,7 +66,7 @@ describe('RuleEventsState', () => { it('should load with new pagination if paging', () => { rulesService.setup(x => x.getEvents(app, 30, 30, undefined)) - .returns(() => of(new RuleEventsDto(200, []))); + .returns(() => of({ items: [], total: 0, _links: {} })); ruleEventsState.page({ page: 1, pageSize: 30 }).subscribe(); @@ -78,7 +78,7 @@ describe('RuleEventsState', () => { it('should load with rule id if filtered', () => { rulesService.setup(x => x.getEvents(app, 30, 0, '12')) - .returns(() => of(new RuleEventsDto(200, []))); + .returns(() => of({ items: [], total: 200, _links: {} })); ruleEventsState.filterByRule('12').subscribe(); @@ -89,7 +89,7 @@ describe('RuleEventsState', () => { it('should not load again if rule id has not changed', () => { rulesService.setup(x => x.getEvents(app, 30, 0, '12')) - .returns(() => of(new RuleEventsDto(200, []))); + .returns(() => of({ items: [], total: 200, _links: {} })); ruleEventsState.filterByRule('12').subscribe(); ruleEventsState.filterByRule('12').subscribe(); diff --git a/frontend/src/app/shared/state/rule-events.state.ts b/frontend/src/app/shared/state/rule-events.state.ts index 4922f2c16..64005d5b7 100644 --- a/frontend/src/app/shared/state/rule-events.state.ts +++ b/frontend/src/app/shared/state/rule-events.state.ts @@ -8,7 +8,7 @@ import { Injectable } from '@angular/core'; import { EMPTY, Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; -import { DialogService, getPagingInfo, hasAnyLink, ListState, ResourceLinks, shareSubscribed, State } from '@app/framework'; +import { DialogService, getPagingInfo, ListState, Resource, shareSubscribed, State } from '@app/framework'; import { RuleEventDto, RulesService } from './../services/rules.service'; import { AppsState } from './apps.state'; @@ -19,8 +19,11 @@ interface Snapshot extends ListState { // The current rule id. ruleId?: string; - // The resource links. - links: ResourceLinks; + // True, if the user has permissions to cancel all rule events. + canCancelAll?: boolean; + + // The resource. + resource: Resource; } @Injectable() @@ -41,7 +44,7 @@ export class RuleEventsState extends State { this.project(x => x.isLoading === true); public canCancelAll = - this.project(x => hasAnyLink(x.links, 'cancel')); + this.project(x => x.canCancelAll === true); public get appId() { return this.appsState.appId; @@ -57,7 +60,7 @@ export class RuleEventsState extends State { private readonly rulesService: RulesService, ) { super({ - links: {}, + resource: { _links: {} }, ruleEvents: [], page: 0, pageSize: 30, @@ -84,15 +87,18 @@ export class RuleEventsState extends State { pageSize, pageSize * page, ruleId).pipe( - tap(({ total, items: ruleEvents, _links: links }) => { + tap(payload => { + const { total, items: ruleEvents, canCancelAll } = payload; + if (isReload) { this.dialogs.notifyInfo('i18n:rules.ruleEvents.reloaded'); } return this.next({ + canCancelAll, isLoaded: true, isLoading: false, - links, + resource: payload, ruleEvents, total, }, 'Loading Success'); @@ -112,7 +118,7 @@ export class RuleEventsState extends State { } public cancelAll(): Observable { - return this.rulesService.cancelEvents(this.appsState.appName, { _links: this.snapshot.links }).pipe( + return this.rulesService.cancelEvents(this.appsState.appName, this.snapshot.resource).pipe( tap(() => { return this.next(s => { const ruleEvents = s.ruleEvents.map(x => setCancelled(x)); @@ -153,4 +159,14 @@ export class RuleEventsState extends State { } const setCancelled = (event: RuleEventDto) => - event.with({ nextAttempt: null, jobResult: 'Cancelled' }); + new RuleEventDto( + event._links, + event.id, + event.created, + null, + event.eventName, + event.description, + event.lastDump, + event.result, + 'Cancelled', + event.numCalls); \ No newline at end of file diff --git a/frontend/src/app/shared/state/rule-simulator.state.spec.ts b/frontend/src/app/shared/state/rule-simulator.state.spec.ts index 9cdcdc36a..cfa45672d 100644 --- a/frontend/src/app/shared/state/rule-simulator.state.spec.ts +++ b/frontend/src/app/shared/state/rule-simulator.state.spec.ts @@ -9,7 +9,6 @@ import { of, throwError } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; import { DialogService, RulesService } from '@app/shared/internal'; -import { SimulatedRuleEventsDto } from '../services/rules.service'; import { createSimulatedRuleEvent } from './../services/rules.service.spec'; import { TestValues } from './_test-helpers'; import { RuleSimulatorState } from './rule-simulator.state'; @@ -39,7 +38,7 @@ describe('RuleSimulatorState', () => { it('should load simulated rule events', () => { rulesService.setup(x => x.getSimulatedEvents(app, '12')) - .returns(() => of(new SimulatedRuleEventsDto(200, oldSimulatedRuleEvents))); + .returns(() => of({ items: oldSimulatedRuleEvents, total: 200 })); ruleSimulatorState.selectRule('12'); ruleSimulatorState.load().subscribe(); @@ -54,7 +53,7 @@ describe('RuleSimulatorState', () => { it('should load simulated rule events by action and trigger', () => { rulesService.setup(x => x.postSimulatedEvents(app, It.isAny(), It.isAny())) - .returns(() => of(new SimulatedRuleEventsDto(200, oldSimulatedRuleEvents))); + .returns(() => of({ items: oldSimulatedRuleEvents, total: 200 })); ruleSimulatorState.setRule({}, {}); ruleSimulatorState.load().subscribe(); diff --git a/frontend/src/app/shared/state/rules.state.spec.ts b/frontend/src/app/shared/state/rules.state.spec.ts index 3d0555eca..1923f78c7 100644 --- a/frontend/src/app/shared/state/rules.state.spec.ts +++ b/frontend/src/app/shared/state/rules.state.spec.ts @@ -8,7 +8,7 @@ import { firstValueFrom, of, throwError } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, RulesDto, RulesService, versioned } from '@app/shared/internal'; +import { DialogService, RulesService, versioned } from '@app/shared/internal'; import { RuleDto } from './../services/rules.service'; import { createRule } from './../services/rules.service.spec'; import { TestValues } from './_test-helpers'; @@ -44,7 +44,7 @@ describe('RulesState', () => { describe('Loading', () => { it('should load rules', () => { rulesService.setup(x => x.getRules(app)) - .returns(() => of(new RulesDto([rule1, rule2], {}, rule1.id))).verifiable(); + .returns(() => of({ items: [rule1, rule2], runningRuleId: rule1.id })).verifiable(); rulesState.load().subscribe(); @@ -75,7 +75,7 @@ describe('RulesState', () => { it('should show notification on load if reload is true', () => { rulesService.setup(x => x.getRules(app)) - .returns(() => of(new RulesDto([rule1, rule2]))).verifiable(); + .returns(() => of({ items: [rule1, rule2] })).verifiable(); rulesState.load(true).subscribe(); @@ -88,7 +88,7 @@ describe('RulesState', () => { describe('Updates', () => { beforeEach(() => { rulesService.setup(x => x.getRules(app)) - .returns(() => of(new RulesDto([rule1, rule2]))).verifiable(); + .returns(() => of({ items: [rule1, rule2] })).verifiable(); rulesState.load().subscribe(); }); @@ -180,7 +180,7 @@ describe('RulesState', () => { describe('Selection', () => { beforeEach(() => { rulesService.setup(x => x.getRules(app)) - .returns(() => of(new RulesDto([rule1, rule2]))).verifiable(Times.atLeastOnce()); + .returns(() => of({ items: [rule1, rule2] })).verifiable(Times.atLeastOnce()); rulesState.load().subscribe(); rulesState.select(rule2.id).subscribe(); @@ -193,7 +193,7 @@ describe('RulesState', () => { ]; rulesService.setup(x => x.getRules(app)) - .returns(() => of(new RulesDto(newRules))); + .returns(() => of({ items: newRules })); rulesState.load().subscribe(); diff --git a/frontend/src/app/shared/state/table-settings.spec.ts b/frontend/src/app/shared/state/table-settings.spec.ts index b9f8150cd..415146d1d 100644 --- a/frontend/src/app/shared/state/table-settings.spec.ts +++ b/frontend/src/app/shared/state/table-settings.spec.ts @@ -8,7 +8,7 @@ import { of } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; import { DateTime, Version } from '@app/framework'; -import { createProperties, FieldSizes, MetaFields, RootFieldDto, SchemaDto, TableSettings, UIState } from '@app/shared/internal'; +import { createProperties, FieldSizes, META_FIELDS, RootFieldDto, SchemaDto, TableSettings, UIState } from '@app/shared/internal'; import { FieldWrappings } from '..'; describe('TableSettings', () => { @@ -77,18 +77,18 @@ describe('TableSettings', () => { }); expect(listFields!).toEqual([ - MetaFields.lastModifiedByAvatar.name, + META_FIELDS.lastModifiedByAvatar.name, schema.fields[0].name, - MetaFields.statusColor.name, - MetaFields.lastModified.name, + META_FIELDS.statusColor.name, + META_FIELDS.lastModified.name, ]); - expect(fieldSizes!).toEqual({ + expect(fieldSizes!).toEqual({ field1: 100, field2: 200, }); - - expect(fieldWrappings!).toEqual({ + + expect(fieldWrappings!).toEqual({ field3: true, field4: false, }); @@ -114,7 +114,7 @@ describe('TableSettings', () => { let listFields: ReadonlyArray; uiState.setup(x => x.getUser('schemas.my-schema.config', {})) - .returns(() => of(({ fields: ['invalid', MetaFields.version.name] }))); + .returns(() => of(({ fields: ['invalid', META_FIELDS.version.name] }))); const tableSettings = new TableSettings(uiState.object, schema); @@ -123,7 +123,7 @@ describe('TableSettings', () => { }); expect(listFields!).toEqual([ - MetaFields.version.name, + META_FIELDS.version.name, ]); }); @@ -133,11 +133,11 @@ describe('TableSettings', () => { const tableSettings = new TableSettings(uiState.object, schema); - const config = [INVALID_FIELD, MetaFields.version]; + const config = [INVALID_FIELD, META_FIELDS.version]; tableSettings.updateFields(config, true); - uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, fields: [MetaFields.version.name] }, true), Times.once()); + uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, fields: [META_FIELDS.version.name] }, true), Times.once()); expect().nothing(); }); @@ -161,7 +161,7 @@ describe('TableSettings', () => { const tableSettings = new TableSettings(uiState.object, schema); - const config = [INVALID_FIELD, MetaFields.version]; + const config = [INVALID_FIELD, META_FIELDS.version]; tableSettings.updateFields(config, false); @@ -182,11 +182,11 @@ describe('TableSettings', () => { fieldSizes = result; }); - tableSettings.updateSize(MetaFields.version.name, 100, true); + tableSettings.updateSize(META_FIELDS.version.name, 100, true); - uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, sizes: { [MetaFields.version.name]: 100 } }, true), Times.once()); + uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, sizes: { [META_FIELDS.version.name]: 100 } }, true), Times.once()); - expect(fieldSizes!).toEqual({ [MetaFields.version.name]: 100 }); + expect(fieldSizes!).toEqual({ [META_FIELDS.version.name]: 100 }); }); it('should update config if sizes are only updated', () => { @@ -201,11 +201,11 @@ describe('TableSettings', () => { fieldSizes = result; }); - tableSettings.updateSize(MetaFields.version.name, 100, false); + tableSettings.updateSize(META_FIELDS.version.name, 100, false); uiState.verify(x => x.set('schemas.my-schema.config', It.isAny(), true), Times.never()); - expect(fieldSizes!).toEqual({ [MetaFields.version.name]: 100 }); + expect(fieldSizes!).toEqual({ [META_FIELDS.version.name]: 100 }); }); it('should update config if wrapping is toggled', () => { @@ -220,11 +220,11 @@ describe('TableSettings', () => { fieldWrappings = result; }); - tableSettings.toggleWrapping(MetaFields.version.name, true); + tableSettings.toggleWrapping(META_FIELDS.version.name, true); - uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, wrappings: { [MetaFields.version.name]: true } }, true), Times.once()); + uiState.verify(x => x.set('schemas.my-schema.config', { ...EMPTY, wrappings: { [META_FIELDS.version.name]: true } }, true), Times.once()); - expect(fieldWrappings!).toEqual({ [MetaFields.version.name]: true }); + expect(fieldWrappings!).toEqual({ [META_FIELDS.version.name]: true }); }); it('should update config if wrapping is toggled and only updated', () => { @@ -239,11 +239,11 @@ describe('TableSettings', () => { fieldWrappings = result; }); - tableSettings.toggleWrapping(MetaFields.version.name, false); + tableSettings.toggleWrapping(META_FIELDS.version.name, false); uiState.verify(x => x.set('schemas.my-schema.config', It.isAny(), true), Times.never()); - expect(fieldWrappings!).toEqual({ [MetaFields.version.name]: true }); + expect(fieldWrappings!).toEqual({ [META_FIELDS.version.name]: true }); }); it('should provide default fields if reset', () => { @@ -253,7 +253,7 @@ describe('TableSettings', () => { const config = { fields: [ - MetaFields.version.name, + META_FIELDS.version.name, ], sizes: { field1: 100, @@ -285,10 +285,10 @@ describe('TableSettings', () => { tableSettings.reset(); expect(listFields!).toEqual([ - MetaFields.lastModifiedByAvatar.name, + META_FIELDS.lastModifiedByAvatar.name, schema.fields[0].name, - MetaFields.statusColor.name, - MetaFields.lastModified.name, + META_FIELDS.statusColor.name, + META_FIELDS.lastModified.name, ]); expect(fieldSizes!).toEqual({}); diff --git a/frontend/src/app/shared/state/table-settings.ts b/frontend/src/app/shared/state/table-settings.ts index a4e619046..232c85a38 100644 --- a/frontend/src/app/shared/state/table-settings.ts +++ b/frontend/src/app/shared/state/table-settings.ts @@ -7,10 +7,10 @@ import { take } from 'rxjs/operators'; import { State, Types } from '@app/framework'; -import { MetaFields, SchemaDto, TableField } from './../services/schemas.service'; +import { META_FIELDS, SchemaDto, TableField } from './../services/schemas.service'; import { UIState } from './ui.state'; -const META_FIELD_NAMES = Object.values(MetaFields).filter(x => x !== MetaFields.empty); +const META_FIELD_NAMES = Object.values(META_FIELDS).filter(x => x !== META_FIELDS.empty); export type FieldSizes = { [name: string]: number }; export type FieldWrappings = { [name: string]: boolean }; @@ -37,7 +37,7 @@ export class TableSettings extends State { public fieldWrappings = this.project(x => x.wrappings); - + public fields = this.project(x => x.fields); @@ -82,10 +82,10 @@ export class TableSettings extends State { } public updateSize(field: string, size: number, save = true) { - this.next(s => ({ + this.next(s => ({ ...s, - sizes: { - ...s.sizes, + sizes: { + ...s.sizes, [field]: size, }, })); @@ -96,10 +96,10 @@ export class TableSettings extends State { } public toggleWrapping(field: string, save = true) { - this.next(s => ({ + this.next(s => ({ ...s, - wrappings: { - ...s.wrappings, + wrappings: { + ...s.wrappings, [field]: !s.wrappings[field], }, })); @@ -118,11 +118,11 @@ export class TableSettings extends State { } private publishSizes(sizes: FieldSizes) { - this.next({ sizes }); + this.next({ sizes }); } private publishWrappings(wrappings: FieldWrappings) { - this.next({ wrappings }); + this.next({ wrappings }); } private publishFields(names: ReadonlyArray) { @@ -135,7 +135,7 @@ export class TableSettings extends State { const { sizes, fields, wrappings } = this.snapshot; if (Object.keys(sizes).length === 0 && Object.keys(wrappings).length === 0 && fields.length === 0) { - this.uiState.removeUser(this.settingsKey); + this.uiState.removeUser(this.settingsKey); } else { const update = { sizes, wrappings, fields: fields.map(x => x.name) }; diff --git a/frontend/src/app/shared/state/template.state.spec.ts b/frontend/src/app/shared/state/template.state.spec.ts index 732dc0c4f..751d79133 100644 --- a/frontend/src/app/shared/state/template.state.spec.ts +++ b/frontend/src/app/shared/state/template.state.spec.ts @@ -8,7 +8,7 @@ import { of, throwError } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; -import { DialogService, TemplatesDto, TemplatesService, TemplatesState } from '@app/shared/internal'; +import { DialogService, TemplatesService, TemplatesState } from '@app/shared/internal'; import { createTemplate } from './../services/templates.service.spec'; describe('TemplatesState', () => { @@ -33,7 +33,7 @@ describe('TemplatesState', () => { describe('Loading', () => { it('should load templates', () => { templatesService.setup(x => x.getTemplates()) - .returns(() => of(new TemplatesDto(2, [template1, template2], {}))).verifiable(); + .returns(() => of({ items: [template1, template2] })).verifiable(); templatesState.load().subscribe(); @@ -55,7 +55,7 @@ describe('TemplatesState', () => { it('should show notification on load if reload is true', () => { templatesService.setup(x => x.getTemplates()) - .returns(() => of(new TemplatesDto(2, [template1, template2], {}))).verifiable(); + .returns(() => of({ items: [template1, template2] })).verifiable(); templatesState.load(true).subscribe(); diff --git a/frontend/src/app/shared/state/ui.state.spec.ts b/frontend/src/app/shared/state/ui.state.spec.ts index d66b61638..22e7598c4 100644 --- a/frontend/src/app/shared/state/ui.state.spec.ts +++ b/frontend/src/app/shared/state/ui.state.spec.ts @@ -7,7 +7,7 @@ import { of } from 'rxjs'; import { IMock, Mock } from 'typemoq'; -import { ResourceLinks, ResourcesDto, UIService, UIState, UsersService } from '@app/shared/internal'; +import { ResourceLinks, UIService, UIState, UsersService } from '@app/shared/internal'; import { TestValues } from './_test-helpers'; describe('UIState', () => { @@ -65,7 +65,7 @@ describe('UIState', () => { usersService = Mock.ofType(); usersService.setup(x => x.getResources()) - .returns(() => of(new ResourcesDto(resources))); + .returns(() => of({ _links: resources })); uiState = new UIState(appsState.object, uiService.object, usersService.object); });