Browse Source

Versioning improved (pure immutability!)

pull/118/head
Sebastian Stehle 9 years ago
parent
commit
5b50f27ac5
  1. 5
      src/Squidex/app/features/api/api-area.component.ts
  2. 5
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts
  3. 5
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  4. 5
      src/Squidex/app/features/content/pages/content/content-history.component.ts
  5. 11
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  6. 27
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  7. 5
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts
  8. 5
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  9. 5
      src/Squidex/app/features/content/shared/content-item.component.ts
  10. 5
      src/Squidex/app/features/content/shared/references-editor.component.ts
  11. 5
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  12. 58
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  13. 9
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  14. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  15. 6
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  16. 44
      src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
  17. 4
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  18. 47
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  19. 14
      src/Squidex/app/features/settings/pages/languages/language.component.ts
  20. 6
      src/Squidex/app/features/settings/pages/languages/languages-page.component.html
  21. 76
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  22. 29
      src/Squidex/app/features/settings/pages/plans/plans-page.component.ts
  23. 5
      src/Squidex/app/features/settings/settings-area.component.ts
  24. 7
      src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts
  25. 12
      src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts
  26. 40
      src/Squidex/app/framework/angular/http-extensions-impl.ts
  27. 14
      src/Squidex/app/framework/utils/version.spec.ts
  28. 10
      src/Squidex/app/framework/utils/version.ts
  29. 13
      src/Squidex/app/shared/components/app.component-base.ts
  30. 21
      src/Squidex/app/shared/components/asset.component.ts
  31. 5
      src/Squidex/app/shared/components/history.component.ts
  32. 8
      src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts
  33. 2
      src/Squidex/app/shared/guards/resolve-app-languages.guard.ts
  34. 51
      src/Squidex/app/shared/services/app-clients.service.spec.ts
  35. 59
      src/Squidex/app/shared/services/app-clients.service.ts
  36. 46
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  37. 52
      src/Squidex/app/shared/services/app-contributors.service.ts
  38. 51
      src/Squidex/app/shared/services/app-languages.service.spec.ts
  39. 59
      src/Squidex/app/shared/services/app-languages.service.ts
  40. 12
      src/Squidex/app/shared/services/apps.service.ts
  41. 44
      src/Squidex/app/shared/services/assets.service.spec.ts
  42. 83
      src/Squidex/app/shared/services/assets.service.ts
  43. 47
      src/Squidex/app/shared/services/contents.service.spec.ts
  44. 109
      src/Squidex/app/shared/services/contents.service.ts
  45. 6
      src/Squidex/app/shared/services/event-consumers.service.ts
  46. 6
      src/Squidex/app/shared/services/history.service.ts
  47. 6
      src/Squidex/app/shared/services/languages.service.ts
  48. 13
      src/Squidex/app/shared/services/plans.service.spec.ts
  49. 40
      src/Squidex/app/shared/services/plans.service.ts
  50. 62
      src/Squidex/app/shared/services/schemas.service.spec.ts
  51. 147
      src/Squidex/app/shared/services/schemas.service.ts
  52. 24
      src/Squidex/app/shared/services/usages.service.ts
  53. 48
      src/Squidex/app/shared/services/users.service.ts
  54. 14
      src/Squidex/app/shared/services/webhooks.service.spec.ts
  55. 33
      src/Squidex/app/shared/services/webhooks.service.ts

5
src/Squidex/app/features/api/api-area.component.ts

@ -10,6 +10,7 @@ import { Component } from '@angular/core';
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
DialogService DialogService
} from 'shared'; } from 'shared';
@ -19,8 +20,8 @@ import {
templateUrl: './api-area.component.html' templateUrl: './api-area.component.html'
}) })
export class ApiAreaComponent extends AppComponentBase { export class ApiAreaComponent extends AppComponentBase {
constructor(apps: AppsStoreService, dialogs: DialogService constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
} }

5
src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts

@ -18,6 +18,7 @@ const GraphiQL = require('graphiql');
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
DialogService, DialogService,
GraphQlService, GraphQlService,
LocalStoreService LocalStoreService
@ -33,11 +34,11 @@ export class GraphQLPageComponent extends AppComponentBase implements OnInit {
@ViewChild('graphiQLContainer') @ViewChild('graphiQLContainer')
public graphiQLContainer: ElementRef; public graphiQLContainer: ElementRef;
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly graphQlService: GraphQlService, private readonly graphQlService: GraphQlService,
private readonly localStoreService: LocalStoreService private readonly localStoreService: LocalStoreService
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {

5
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -17,6 +17,7 @@ import {
AssetDto, AssetDto,
AssetsService, AssetsService,
AssetUpdated, AssetUpdated,
AuthService,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
MessageBus, MessageBus,
@ -38,11 +39,11 @@ export class AssetsPageComponent extends AppComponentBase implements OnDestroy,
public assetsFilter = new FormControl(); public assetsFilter = new FormControl();
public assertQuery = ''; public assertQuery = '';
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly messageBus: MessageBus private readonly messageBus: MessageBus
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {

5
src/Squidex/app/features/content/pages/content/content-history.component.ts

@ -13,6 +13,7 @@ import {
allParams, allParams,
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
DialogService, DialogService,
HistoryChannelUpdated, HistoryChannelUpdated,
HistoryEventDto, HistoryEventDto,
@ -55,13 +56,13 @@ export class ContentHistoryComponent extends AppComponentBase {
.switchMap(() => this.appNameOnce()) .switchMap(() => this.appNameOnce())
.switchMap(app => this.historyService.getHistory(app, this.channel).retry(2)); .switchMap(app => this.historyService.getHistory(app, this.channel).retry(2));
constructor(appsStore: AppsStoreService, dialogs: DialogService, constructor(appsStore: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly users: UsersProviderService, private readonly users: UsersProviderService,
private readonly historyService: HistoryService, private readonly historyService: HistoryService,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute private readonly route: ActivatedRoute
) { ) {
super(dialogs, appsStore); super(dialogs, appsStore, authService);
} }
private userName(userId: string): Observable<string> { private userName(userId: string): Observable<string> {

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

@ -55,14 +55,13 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
public languages: AppLanguageDto[] = []; public languages: AppLanguageDto[] = [];
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly authService: AuthService,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router,
private readonly messageBus: MessageBus private readonly messageBus: MessageBus
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -87,7 +86,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
this.messageBus.of(ContentPublished) this.messageBus.of(ContentPublished)
.subscribe(message => { .subscribe(message => {
if (this.content && message.content.id === this.content.id) { if (this.content && message.content.id === this.content.id) {
this.content = this.content.replaceVersion(message.content.version); this.content = this.content.publish(message.content.lastModifiedBy, message.content.version, message.content.lastModified);
} }
}); });
@ -95,7 +94,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
this.messageBus.of(ContentUnpublished) this.messageBus.of(ContentUnpublished)
.subscribe(message => { .subscribe(message => {
if (this.content && message.content.id === this.content.id) { if (this.content && message.content.id === this.content.id) {
this.content = this.content.replaceVersion(message.content.version); this.content = this.content.unpublish(message.content.lastModifiedBy, message.content.version, message.content.lastModified);
} }
}); });
@ -158,7 +157,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.content.id, requestDto, this.content.version)) .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.content.id, requestDto, this.content.version))
.subscribe(dto => { .subscribe(dto => {
this.content = this.content.update(dto, this.authService.user!.token); this.content = this.content.update(dto.payload, this.userToken, dto.version);
this.emitContentUpdated(this.content); this.emitContentUpdated(this.content);
this.notifyInfo('Content saved successfully.'); this.notifyInfo('Content saved successfully.');

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

@ -63,13 +63,12 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public columnWidth: number; public columnWidth: number;
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly authService: AuthService,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly messageBus: MessageBus private readonly messageBus: MessageBus
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -92,7 +91,7 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.contentUpdatedSubscription = this.contentUpdatedSubscription =
this.messageBus.of(ContentUpdated) this.messageBus.of(ContentUpdated)
.subscribe(message => { .subscribe(message => {
this.contentItems = this.contentItems.replaceBy('id', message.content, (o, n) => o.update(n.data, n.lastModifiedBy)); this.contentItems = this.contentItems.replaceBy('id', message.content, (o, n) => o.update(n.data, n.lastModifiedBy, n.version, n.lastModified));
}); });
this.route.params.map(p => <string> p['language']) this.route.params.map(p => <string> p['language'])
@ -118,8 +117,10 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public publishContent(content: ContentDto) { public publishContent(content: ContentDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version)) .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .subscribe(dto => {
this.contentItems = this.contentItems.replaceBy('id', content.publish(this.authService.user!.token)); content = content.publish(this.userToken, dto.version);
this.contentItems = this.contentItems.replaceBy('id', content);
this.emitContentPublished(content); this.emitContentPublished(content);
}, error => { }, error => {
@ -130,8 +131,10 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public unpublishContent(content: ContentDto) { public unpublishContent(content: ContentDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version)) .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .subscribe(dto => {
this.contentItems = this.contentItems.replaceBy('id', content.unpublish(this.authService.user!.token)); content = content.unpublish(this.userToken, dto.version);
this.contentItems = this.contentItems.replaceBy('id', content);
this.emitContentUnpublished(content); this.emitContentUnpublished(content);
}, error => { }, error => {
@ -142,8 +145,8 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public archiveContent(content: ContentDto) { public archiveContent(content: ContentDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.archiveContent(app, this.schema.name, content.id, content.version)) .switchMap(app => this.contentsService.archiveContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .subscribe(dto => {
content = content.archive(this.authService.user!.token); content = content.archive(this.userToken, dto.version);
this.removeContent(content); this.removeContent(content);
}, error => { }, error => {
@ -154,8 +157,8 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
public restoreContent(content: ContentDto) { public restoreContent(content: ContentDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.contentsService.restoreContent(app, this.schema.name, content.id, content.version)) .switchMap(app => this.contentsService.restoreContent(app, this.schema.name, content.id, content.version))
.subscribe(() => { .subscribe(dto => {
content = content.restore(this.authService.user!.token); content = content.restore(this.userToken, dto.version);
this.removeContent(content); this.removeContent(content);
}, error => { }, error => {

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

@ -12,6 +12,7 @@ import { Observable } from 'rxjs';
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
DialogService, DialogService,
SchemaDto, SchemaDto,
SchemasService SchemasService
@ -52,10 +53,10 @@ export class SchemasPageComponent extends AppComponentBase {
}); });
}); });
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly schemasService: SchemasService private readonly schemasService: SchemasService
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
private loadSchemas(): Observable<SchemaDto[]> { private loadSchemas(): Observable<SchemaDto[]> {

5
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -17,6 +17,7 @@ import {
AssetDto, AssetDto,
AssetsService, AssetsService,
AssetUpdated, AssetUpdated,
AuthService,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
MessageBus, MessageBus,
@ -43,11 +44,11 @@ export class AssetsEditorComponent extends AppComponentBase implements ControlVa
public isDisabled = false; public isDisabled = false;
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly messageBus: MessageBus private readonly messageBus: MessageBus
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {

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

@ -10,6 +10,7 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angu
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
ContentDto, ContentDto,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
@ -66,8 +67,8 @@ export class ContentItemComponent extends AppComponentBase implements OnInit, On
public values: any[] = []; public values: any[] = [];
constructor(apps: AppsStoreService, dialogs: DialogService) { constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnChanges() { public ngOnChanges() {

5
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -13,6 +13,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
ContentDto, ContentDto,
ContentsService, ContentsService,
DialogService, DialogService,
@ -53,11 +54,11 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
public isDisabled = false; public isDisabled = false;
public isInvalidSchema = false; public isInvalidSchema = false;
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService private readonly schemasService: SchemasService
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {

5
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -60,11 +60,10 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit {
public callsCurrent = 0; public callsCurrent = 0;
public callsMax = 0; public callsMax = 0;
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly authService: AuthService,
private readonly usagesService: UsagesService private readonly usagesService: UsagesService
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {

58
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -30,7 +30,8 @@ import {
SchemasService, SchemasService,
UpdateFieldDto, UpdateFieldDto,
UpdateSchemaScriptsDto, UpdateSchemaScriptsDto,
ValidatorsEx ValidatorsEx,
Version
} from 'shared'; } from 'shared';
import { import {
@ -82,15 +83,14 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
return this.addFieldForm.controls['name'].value && this.addFieldForm.controls['name'].value.length > 0; return this.addFieldForm.controls['name'].value && this.addFieldForm.controls['name'].value.length > 0;
} }
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly authService: AuthService,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router, private readonly router: Router,
private readonly schemasService: SchemasService private readonly schemasService: SchemasService
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -129,8 +129,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public publish() { public publish() {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.publishSchema(app, this.schema.name, this.schema.version)).retry(2) .switchMap(app => this.schemasService.publishSchema(app, this.schema.name, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.publish(this.authService.user!.token)); this.updateSchema(this.schema.publish(this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -139,8 +139,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public unpublish() { public unpublish() {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.unpublishSchema(app, this.schema.name, this.schema.version)).retry(2) .switchMap(app => this.schemasService.unpublishSchema(app, this.schema.name, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.unpublish(this.authService.user!.token)); this.updateSchema(this.schema.unpublish(this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -149,8 +149,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public enableField(field: FieldDto) { public enableField(field: FieldDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.enableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.enableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.updateField(field.enable(), this.authService.user!.token)); this.updateSchema(this.schema.updateField(field.enable(), this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -159,8 +159,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public disableField(field: FieldDto) { public disableField(field: FieldDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.disableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.disableField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.updateField(field.disable(), this.authService.user!.token)); this.updateSchema(this.schema.updateField(field.disable(), this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -169,8 +169,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public lockField(field: FieldDto) { public lockField(field: FieldDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.lockField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.lockField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.updateField(field.lock(), this.authService.user!.token)); this.updateSchema(this.schema.updateField(field.lock(), this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -179,8 +179,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public showField(field: FieldDto) { public showField(field: FieldDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.showField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.showField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.updateField(field.show(), this.authService.user!.token)); this.updateSchema(this.schema.updateField(field.show(), this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -189,8 +189,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public hideField(field: FieldDto) { public hideField(field: FieldDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.hideField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.hideField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.updateField(field.hide(), this.authService.user!.token)); this.updateSchema(this.schema.updateField(field.hide(), this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -199,8 +199,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public deleteField(field: FieldDto) { public deleteField(field: FieldDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.deleteField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2) .switchMap(app => this.schemasService.deleteField(app, this.schema.name, field.fieldId, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.removeField(field, this.authService.user!.token)); this.updateSchema(this.schema.removeField(field, this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -209,8 +209,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
public sortFields(fields: FieldDto[]) { public sortFields(fields: FieldDto[]) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.putFieldOrdering(app, this.schema.name, fields.map(t => t.fieldId), this.schema.version)).retry(2) .switchMap(app => this.schemasService.putFieldOrdering(app, this.schema.name, fields.map(t => t.fieldId), this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.replaceFields(fields, this.authService.user!.token)); this.updateSchema(this.schema.replaceFields(fields, this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -221,8 +221,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.putField(app, this.schema.name, field.fieldId, requestDto, this.schema.version)).retry(2) .switchMap(app => this.schemasService.putField(app, this.schema.name, field.fieldId, requestDto, this.schema.version)).retry(2)
.subscribe(() => { .subscribe(dto => {
this.updateSchema(this.schema.updateField(field, this.authService.user!.token)); this.updateSchema(this.schema.updateField(field, this.userToken, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -253,7 +253,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.schemasService.postField(app, this.schema.name, requestDto, this.schema.version)) .switchMap(app => this.schemasService.postField(app, this.schema.name, requestDto, this.schema.version))
.subscribe(dto => { .subscribe(dto => {
this.updateSchema(this.schema.addField(dto, this.authService.user!.token)); this.updateSchema(this.schema.addField(dto.payload, this.userToken, dto.version));
this.resetFieldForm(); this.resetFieldForm();
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
@ -266,14 +266,14 @@ export class SchemaPageComponent extends AppComponentBase implements OnDestroy,
this.resetFieldForm(); this.resetFieldForm();
} }
public onSchemaSaved(properties: SchemaPropertiesDto) { public onSchemaSaved(properties: SchemaPropertiesDto, version: Version) {
this.updateSchema(this.schema.update(properties, this.authService.user!.token)); this.updateSchema(this.schema.update(properties, this.userToken, version));
this.editSchemaDialog.hide(); this.editSchemaDialog.hide();
} }
public onSchemaScriptsSaved(scripts: UpdateSchemaScriptsDto) { public onSchemaScriptsSaved(scripts: UpdateSchemaScriptsDto, version: Version) {
this.updateSchema(this.schema.configureScripts(scripts, this.authService.user!.token)); this.updateSchema(this.schema.configureScripts(scripts, this.userToken, version));
this.configureScriptsDialog.hide(); this.configureScriptsDialog.hide();
} }

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

@ -15,8 +15,7 @@ import {
fadeAnimation, fadeAnimation,
SchemaDetailsDto, SchemaDetailsDto,
SchemasService, SchemasService,
ValidatorsEx, ValidatorsEx
Version
} from 'shared'; } from 'shared';
const FALLBACK_NAME = 'my-schema'; const FALLBACK_NAME = 'my-schema';
@ -83,14 +82,12 @@ export class SchemaFormComponent {
if (this.createForm.valid) { if (this.createForm.valid) {
this.createForm.disable(); this.createForm.disable();
const schemaVersion = new Version();
const schemaName = this.createForm.controls['name'].value; const schemaName = this.createForm.controls['name'].value;
const schemaDto = Object.assign(this.createForm.controls['import'].value || {}, { name: schemaName });
const requestDto = Object.assign(this.createForm.controls['import'].value || {}, { name: schemaName });
const me = this.authService.user!.token; const me = this.authService.user!.token;
this.schemas.postSchema(this.appName, requestDto, me, DateTime.now(), schemaVersion) this.schemas.postSchema(this.appName, schemaDto, me, DateTime.now())
.subscribe(dto => { .subscribe(dto => {
this.emitCreated(dto); this.emitCreated(dto);
this.resetCreateForm(); this.resetCreateForm();

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

@ -13,6 +13,7 @@ import { Subscription } from 'rxjs';
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
ImmutableArray, ImmutableArray,
@ -47,12 +48,12 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy,
public schemasFilter = new FormControl(); public schemasFilter = new FormControl();
public schemasFiltered = ImmutableArray.empty<SchemaDto>(); public schemasFiltered = ImmutableArray.empty<SchemaDto>();
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly schemasService: SchemasService, private readonly schemasService: SchemasService,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute private readonly route: ActivatedRoute
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnDestroy() { public ngOnDestroy() {

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

@ -13,18 +13,18 @@
<div class="panel-main"> <div class="panel-main">
<div class="panel-content panel-content-scroll"> <div class="panel-content panel-content-scroll">
<div class="table-items-row table-items-row-empty" *ngIf="appClients && appClients.length === 0"> <div class="table-items-row table-items-row-empty" *ngIf="appClients && appClients.clients.length === 0">
No client created yet. No client created yet.
</div> </div>
<div *ngFor="let client of appClients"> <div *ngFor="let client of appClients?.clients">
<sqx-client [client]="client" [appName]="appName() | async" <sqx-client [client]="client" [appName]="appName() | async"
(changing)="changeClient(client, $event)" (changing)="changeClient(client, $event)"
(renaming)="renameClient(client, $event)" (renaming)="renameClient(client, $event)"
(revoking)="revokeClient(client)"></sqx-client> (revoking)="revokeClient(client)"></sqx-client>
</div> </div>
<div class="table-items-footer"> <div class="table-items-footer" *ngIf="appClients">
<form [formGroup]="addClientForm" (ngSubmit)="attachClient()"> <form [formGroup]="addClientForm" (ngSubmit)="attachClient()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">

44
src/Squidex/app/features/settings/pages/clients/clients-page.component.ts

@ -10,17 +10,17 @@ import { FormBuilder, Validators } from '@angular/forms';
import { import {
AppClientDto, AppClientDto,
AppClientsDto,
AppClientsService, AppClientsService,
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
CreateAppClientDto, CreateAppClientDto,
DialogService, DialogService,
HistoryChannelUpdated, HistoryChannelUpdated,
ImmutableArray,
MessageBus, MessageBus,
UpdateAppClientDto, UpdateAppClientDto,
ValidatorsEx, ValidatorsEx
Version
} from 'shared'; } from 'shared';
@Component({ @Component({
@ -29,9 +29,7 @@ import {
templateUrl: './clients-page.component.html' templateUrl: './clients-page.component.html'
}) })
export class ClientsPageComponent extends AppComponentBase implements OnInit { export class ClientsPageComponent extends AppComponentBase implements OnInit {
private version = new Version(); public appClients: AppClientsDto;
public appClients: ImmutableArray<AppClientDto>;
public addClientFormSubmitted = false; public addClientFormSubmitted = false;
public addClientForm = public addClientForm =
@ -47,12 +45,12 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
return this.addClientForm.controls['name'].value && this.addClientForm.controls['name'].value.length > 0; return this.addClientForm.controls['name'].value && this.addClientForm.controls['name'].value.length > 0;
} }
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly appClientsService: AppClientsService, private readonly appClientsService: AppClientsService,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {
@ -61,9 +59,9 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
public load() { public load() {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appClientsService.getClients(app, this.version).retry(2)) .switchMap(app => this.appClientsService.getClients(app).retry(2))
.subscribe(dtos => { .subscribe(dtos => {
this.updateClients(ImmutableArray.of(dtos)); this.updateClients(dtos);
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -71,9 +69,9 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
public revokeClient(client: AppClientDto) { public revokeClient(client: AppClientDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appClientsService.deleteClient(app, client.id, this.version)) .switchMap(app => this.appClientsService.deleteClient(app, client.id, this.appClients.version))
.subscribe(() => { .subscribe(dto => {
this.updateClients(this.appClients.remove(client)); this.updateClients(this.appClients.removeClient(client, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -83,9 +81,9 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
const requestDto = new UpdateAppClientDto(name); const requestDto = new UpdateAppClientDto(name);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appClientsService.updateClient(app, client.id, requestDto, this.version)) .switchMap(app => this.appClientsService.updateClient(app, client.id, requestDto, this.appClients.version))
.subscribe(() => { .subscribe(dto => {
this.updateClients(this.appClients.replaceBy('id', client.rename(name))); this.updateClients(this.appClients.updateClient(client.rename(name), dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -95,9 +93,9 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
const requestDto = new UpdateAppClientDto(undefined, isReader); const requestDto = new UpdateAppClientDto(undefined, isReader);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appClientsService.updateClient(app, client.id, requestDto, this.version)) .switchMap(app => this.appClientsService.updateClient(app, client.id, requestDto, this.appClients.version))
.subscribe(() => { .subscribe(dto => {
this.updateClients(this.appClients.replaceBy('id', client.change(isReader))); this.updateClients(this.appClients.updateClient(client.change(isReader), dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -112,9 +110,9 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
const requestDto = new CreateAppClientDto(this.addClientForm.controls['name'].value); const requestDto = new CreateAppClientDto(this.addClientForm.controls['name'].value);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appClientsService.postClient(app, requestDto, this.version)) .switchMap(app => this.appClientsService.postClient(app, requestDto, this.appClients.version))
.subscribe(dto => { .subscribe(dto => {
this.updateClients(this.appClients.push(dto)); this.updateClients(this.appClients.addClient(dto.payload, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}, () => { }, () => {
@ -133,8 +131,8 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
this.addClientForm.reset(); this.addClientForm.reset();
} }
private updateClients(clients: ImmutableArray<AppClientDto>) { private updateClients(appClients: AppClientsDto) {
this.appClients = clients; this.appClients = appClients;
this.messageBus.emit(new HistoryChannelUpdated()); this.messageBus.emit(new HistoryChannelUpdated());
} }

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

@ -27,7 +27,7 @@
</colgroup> </colgroup>
<tbody> <tbody>
<ng-template ngFor let-contributor [ngForOf]="appContributors"> <ng-template ngFor let-contributor [ngForOf]="appContributors?.contributors">
<tr> <tr>
<td> <td>
<img class="user-picture" [attr.title]="contributor.contributorId | sqxUserName" [attr.src]="contributor.contributorId | sqxUserPicture" /> <img class="user-picture" [attr.title]="contributor.contributorId | sqxUserName" [attr.src]="contributor.contributorId | sqxUserPicture" />
@ -54,7 +54,7 @@
</tbody> </tbody>
</table> </table>
<div class="table-items-footer"> <div class="table-items-footer" *ngIf="appContributors">
<form [formGroup]="addContributorForm" (ngSubmit)="assignContributor()"> <form [formGroup]="addContributorForm" (ngSubmit)="assignContributor()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">

47
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -19,10 +19,8 @@ import {
AutocompleteSource, AutocompleteSource,
DialogService, DialogService,
HistoryChannelUpdated, HistoryChannelUpdated,
ImmutableArray,
MessageBus, MessageBus,
UsersService, UsersService
Version
} from 'shared'; } from 'shared';
export class UsersDataSource implements AutocompleteSource { export class UsersDataSource implements AutocompleteSource {
@ -38,7 +36,7 @@ export class UsersDataSource implements AutocompleteSource {
const results: any[] = []; const results: any[] = [];
for (let user of users) { for (let user of users) {
if (!this.component.appContributors || !this.component.appContributors.find(t => t.contributorId === user.id)) { if (!this.component.appContributors || !this.component.appContributors.contributors.find(t => t.contributorId === user.id)) {
results.push(user); results.push(user);
} }
} }
@ -53,9 +51,7 @@ export class UsersDataSource implements AutocompleteSource {
templateUrl: './contributors-page.component.html' templateUrl: './contributors-page.component.html'
}) })
export class ContributorsPageComponent extends AppComponentBase implements OnInit { export class ContributorsPageComponent extends AppComponentBase implements OnInit {
private version = new Version(); public appContributors: AppContributorsDto;
public appContributors = ImmutableArray.empty<AppContributorDto>();
public currentUserId: string; public currentUserId: string;
@ -70,7 +66,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
]; ];
public get canAddContributor() { public get canAddContributor() {
return this.addContributorForm.valid && (this.maxContributors <= -1 || this.appContributors.length < this.maxContributors); return this.addContributorForm.valid && (this.maxContributors <= -1 || this.appContributors.contributors.length < this.maxContributors);
} }
public addContributorForm = public addContributorForm =
@ -81,13 +77,12 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
]] ]]
}); });
constructor(apps: AppsStoreService, dialogs: DialogService, usersService: UsersService, constructor(apps: AppsStoreService, dialogs: DialogService, usersService: UsersService, authService: AuthService,
private readonly appContributorsService: AppContributorsService, private readonly appContributorsService: AppContributorsService,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly authService: AuthService,
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
this.usersDataSource = new UsersDataSource(usersService, this); this.usersDataSource = new UsersDataSource(usersService, this);
} }
@ -100,7 +95,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
public load() { public load() {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appContributorsService.getContributors(app, this.version).retry(2)) .switchMap(app => this.appContributorsService.getContributors(app).retry(2))
.subscribe(dto => { .subscribe(dto => {
this.updateContributorsFromDto(dto); this.updateContributorsFromDto(dto);
}, error => { }, error => {
@ -110,9 +105,9 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
public removeContributor(contributor: AppContributorDto) { public removeContributor(contributor: AppContributorDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId, this.version)) .switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId, this.appContributors.version))
.subscribe(() => { .subscribe(dto => {
this.updateContributors(this.appContributors.remove(contributor)); this.updateContributors(this.appContributors.removeContributor(contributor, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -122,9 +117,9 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
const requestDto = contributor.changePermission(permission); const requestDto = contributor.changePermission(permission);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.version)) .switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.appContributors.version))
.subscribe(() => { .subscribe(dto => {
this.updateContributors(this.appContributors.replace(contributor, requestDto)); this.updateContributors(this.appContributors.updateContributor(contributor, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -134,9 +129,9 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
const requestDto = new AppContributorDto(this.addContributorForm.controls['user'].value.id, 'Editor'); const requestDto = new AppContributorDto(this.addContributorForm.controls['user'].value.id, 'Editor');
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.version)) .switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.appContributors.version))
.subscribe(() => { .subscribe(dto => {
this.updateContributors(this.appContributors.push(requestDto)); this.updateContributors(this.appContributors.addContributor(requestDto, dto.version));
this.resetContributorForm(); this.resetContributorForm();
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
@ -148,14 +143,14 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
this.addContributorForm.reset(); this.addContributorForm.reset();
} }
private updateContributorsFromDto(dto: AppContributorsDto) { private updateContributorsFromDto(appContributors: AppContributorsDto) {
this.updateContributors(ImmutableArray.of(dto.contributors)); this.updateContributors(appContributors);
this.maxContributors = dto.maxContributors; this.maxContributors = appContributors.maxContributors;
} }
private updateContributors(contributors: ImmutableArray<AppContributorDto>) { private updateContributors(appContributors: AppContributorsDto) {
this.appContributors = contributors; this.appContributors = appContributors;
this.messageBus.emit(new HistoryChannelUpdated()); this.messageBus.emit(new HistoryChannelUpdated());
} }

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

@ -9,11 +9,7 @@ import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output, OnInit }
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { import { AppLanguageDto, fadeAnimation } from 'shared';
AppLanguageDto,
fadeAnimation,
ImmutableArray
} from 'shared';
@Component({ @Component({
selector: 'sqx-language', selector: 'sqx-language',
@ -30,7 +26,7 @@ export class LanguageComponent implements OnInit, OnChanges, OnDestroy {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public allLanguages: ImmutableArray<AppLanguageDto>; public allLanguages: AppLanguageDto[];
@Output() @Output()
public removing = new EventEmitter<AppLanguageDto>(); public removing = new EventEmitter<AppLanguageDto>();
@ -38,7 +34,7 @@ export class LanguageComponent implements OnInit, OnChanges, OnDestroy {
@Output() @Output()
public saving = new EventEmitter<AppLanguageDto>(); public saving = new EventEmitter<AppLanguageDto>();
public otherLanguages: ImmutableArray<AppLanguageDto>; public otherLanguages: AppLanguageDto[];
public otherLanguage: AppLanguageDto; public otherLanguage: AppLanguageDto;
public fallbackLanguages: AppLanguageDto[] = []; public fallbackLanguages: AppLanguageDto[] = [];
@ -92,7 +88,7 @@ export class LanguageComponent implements OnInit, OnChanges, OnDestroy {
public removeFallbackLanguage(language: AppLanguageDto) { public removeFallbackLanguage(language: AppLanguageDto) {
this.fallbackLanguages.splice(this.fallbackLanguages.indexOf(language), 1); this.fallbackLanguages.splice(this.fallbackLanguages.indexOf(language), 1);
this.otherLanguages = this.otherLanguages.push(language); this.otherLanguages = [...this.otherLanguages, language];
this.otherLanguage = this.otherLanguages.values[0]; this.otherLanguage = this.otherLanguages.values[0];
} }
@ -142,7 +138,7 @@ export class LanguageComponent implements OnInit, OnChanges, OnDestroy {
this.fallbackLanguages = this.fallbackLanguages =
this.allLanguages.filter(l => this.allLanguages.filter(l =>
this.language.fallback.indexOf(l.iso2Code) >= 0).values; this.language.fallback.indexOf(l.iso2Code) >= 0);
} }
} }
} }

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

@ -13,13 +13,13 @@
<div class="panel-main"> <div class="panel-main">
<div class="panel-content panel-content-scroll"> <div class="panel-content panel-content-scroll">
<div *ngFor="let language of appLanguages"> <div *ngFor="let language of appLanguages?.languages">
<sqx-language [language]="language" [allLanguages]="appLanguages" <sqx-language [language]="language" [allLanguages]="appLanguages.languages"
(saving)="updateLanguage($event)" (saving)="updateLanguage($event)"
(removing)="removeLanguage($event)"></sqx-language> (removing)="removeLanguage($event)"></sqx-language>
</div> </div>
<div class="table-items-footer"> <div class="table-items-footer" *ngIf="appLanguages">
<form [formGroup]="addLanguageForm" (ngSubmit)="addLanguage()"> <form [formGroup]="addLanguageForm" (ngSubmit)="addLanguage()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">

76
src/Squidex/app/features/settings/pages/languages/languages-page.component.ts

@ -12,15 +12,16 @@ import {
AddAppLanguageDto, AddAppLanguageDto,
AppComponentBase, AppComponentBase,
AppLanguageDto, AppLanguageDto,
AppLanguagesDto,
AppLanguagesService, AppLanguagesService,
AppsStoreService, AppsStoreService,
AuthService,
DialogService, DialogService,
HistoryChannelUpdated, HistoryChannelUpdated,
ImmutableArray, ImmutableArray,
MessageBus, MessageBus,
LanguageDto, LanguageDto,
LanguagesService, LanguagesService
Version
} from 'shared'; } from 'shared';
@Component({ @Component({
@ -29,11 +30,9 @@ import {
templateUrl: './languages-page.component.html' templateUrl: './languages-page.component.html'
}) })
export class LanguagesPageComponent extends AppComponentBase implements OnInit { export class LanguagesPageComponent extends AppComponentBase implements OnInit {
private version = new Version();
public allLanguages: LanguageDto[] = []; public allLanguages: LanguageDto[] = [];
public newLanguages: LanguageDto[] = []; public newLanguages: LanguageDto[] = [];
public appLanguages = ImmutableArray.empty<AppLanguageDto>(); public appLanguages: AppLanguagesDto;
public addLanguageForm = public addLanguageForm =
this.formBuilder.group({ this.formBuilder.group({
@ -42,13 +41,13 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
] ]
}); });
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly appLanguagesService: AppLanguagesService, private readonly appLanguagesService: AppLanguagesService,
private readonly languagesService: LanguagesService, private readonly languagesService: LanguagesService,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {
@ -58,9 +57,9 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
public load() { public load() {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appLanguagesService.getLanguages(app, this.version).retry(2)) .switchMap(app => this.appLanguagesService.getLanguages(app).retry(2))
.subscribe(dtos => { .subscribe(dto => {
this.updateLanguages(ImmutableArray.of(dtos)); this.updateLanguages(dto);
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -68,9 +67,9 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
public removeLanguage(language: AppLanguageDto) { public removeLanguage(language: AppLanguageDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code, this.version)) .switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code, this.appLanguages.version))
.subscribe(dto => { .subscribe(dto => {
this.updateLanguages(this.appLanguages.remove(language)); this.updateLanguages(this.appLanguages.removeLanguage(language, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -80,9 +79,9 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
const requestDto = new AddAppLanguageDto(this.addLanguageForm.controls['language'].value.iso2Code); const requestDto = new AddAppLanguageDto(this.addLanguageForm.controls['language'].value.iso2Code);
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appLanguagesService.postLanguages(app, requestDto, this.version)) .switchMap(app => this.appLanguagesService.postLanguages(app, requestDto, this.appLanguages.version))
.subscribe(dto => { .subscribe(dto => {
this.updateLanguages(this.appLanguages.push(dto)); this.updateLanguages(this.appLanguages.addLanguage(dto.payload, dto.version));
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -90,13 +89,9 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
public updateLanguage(language: AppLanguageDto) { public updateLanguage(language: AppLanguageDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, language, this.version)) .switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, language, this.appLanguages.version))
.subscribe(dto => { .subscribe(dto => {
this.updateLanguages( this.updateLanguages(this.appLanguages.updateLanguage(language, dto.version));
this.appLanguages.replaceAll(
l => l.iso2Code === language.iso2Code,
l => language),
language.isMaster ? language.iso2Code : undefined);
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);
}); });
@ -113,26 +108,27 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
}); });
} }
private updateLanguages(languages: ImmutableArray<AppLanguageDto>, masterId?: string) { private updateLanguages(appLanguages: AppLanguagesDto, masterId?: string) {
this.addLanguageForm.reset(); this.addLanguageForm.reset();
this.appLanguages = this.appLanguages =
languages.map(l => { new AppLanguagesDto(
const isMaster = masterId ? l.iso2Code === masterId : l.isMaster; appLanguages.languages.map(l => {
const isMaster = masterId ? l.iso2Code === masterId : l.isMaster;
return new AppLanguageDto(
l.iso2Code, return new AppLanguageDto(
l.englishName, isMaster, l.iso2Code,
l.isOptional, l.englishName, isMaster,
l.fallback.filter(f => !!languages.find(l2 => l2.iso2Code === f)) l.isOptional,
); l.fallback.filter(f => !!appLanguages.languages.find(l2 => l2.iso2Code === f))
}).sort((a, b) => { );
if (a.isMaster === b.isMaster) { }).sort((a, b) => {
return a.iso2Code.localeCompare(b.iso2Code); if (a.isMaster === b.isMaster) {
} else { return a.iso2Code.localeCompare(b.iso2Code);
return (a.isMaster ? 0 : 1) - (b.isMaster ? 0 : 1); } else {
} return (a.isMaster ? 0 : 1) - (b.isMaster ? 0 : 1);
}); }
}), appLanguages.version);
this.updateNewLanguages(); this.updateNewLanguages();
@ -140,8 +136,10 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
} }
private updateNewLanguages() { private updateNewLanguages() {
this.newLanguages = this.allLanguages.filter(x => !this.appLanguages.find(l => l.iso2Code === x.iso2Code)); if (this.appLanguages) {
this.addLanguageForm.controls['language'].setValue(this.newLanguages[0]); this.newLanguages = this.allLanguages.filter(x => !this.appLanguages.languages.find(l => l.iso2Code === x.iso2Code));
this.addLanguageForm.controls['language'].setValue(this.newLanguages[0]);
}
} }
} }

29
src/Squidex/app/features/settings/pages/plans/plans-page.component.ts

@ -17,8 +17,7 @@ import {
AuthService, AuthService,
ChangePlanDto, ChangePlanDto,
DialogService, DialogService,
PlansService, PlansService
Version
} from 'shared'; } from 'shared';
@Component({ @Component({
@ -29,7 +28,6 @@ import {
export class PlansPageComponent extends AppComponentBase implements OnDestroy, OnInit { export class PlansPageComponent extends AppComponentBase implements OnDestroy, OnInit {
private queryParamsSubscription: Subscription; private queryParamsSubscription: Subscription;
private overridePlanId: string; private overridePlanId: string;
private version = new Version();
public portalUrl = this.apiUrl.buildUrl('/identity-server/account/portal'); public portalUrl = this.apiUrl.buildUrl('/identity-server/account/portal');
@ -38,13 +36,12 @@ export class PlansPageComponent extends AppComponentBase implements OnDestroy, O
public isDisabled = false; public isDisabled = false;
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly authService: AuthService,
private readonly plansService: PlansService, private readonly plansService: PlansService,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly apiUrl: ApiUrlConfig private readonly apiUrl: ApiUrlConfig
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -62,14 +59,10 @@ export class PlansPageComponent extends AppComponentBase implements OnDestroy, O
public load(showInfo = false) { public load(showInfo = false) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.plansService.getPlans(app, this.version).retry(2)) .switchMap(app => this.plansService.getPlans(app).retry(2))
.subscribe(dto => { .subscribe(dto => {
if (this.overridePlanId) { if (this.overridePlanId) {
this.plans = new AppPlansDto( this.plans = dto.changePlanId(this.overridePlanId);
this.overridePlanId,
dto.planOwner,
dto.hasPortal,
dto.plans);
} else { } else {
this.plans = dto; this.plans = dto;
} }
@ -88,16 +81,12 @@ export class PlansPageComponent extends AppComponentBase implements OnDestroy, O
this.isDisabled = true; this.isDisabled = true;
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.plansService.putPlan(app, new ChangePlanDto(planId), this.version)) .switchMap(app => this.plansService.putPlan(app, new ChangePlanDto(planId), this.plans.version))
.subscribe(dto => { .subscribe(dto => {
if (dto.redirectUri && dto.redirectUri.length > 0) { if (dto.payload.redirectUri && dto.payload.redirectUri.length > 0) {
window.location.href = dto.redirectUri; window.location.href = dto.payload.redirectUri;
} else { } else {
this.plans = this.plans = this.plans.changePlanId(planId, dto.version);
new AppPlansDto(planId,
this.plans.planOwner,
this.plans.hasPortal,
this.plans.plans);
this.isDisabled = false; this.isDisabled = false;
} }
}, error => { }, error => {

5
src/Squidex/app/features/settings/settings-area.component.ts

@ -10,6 +10,7 @@ import { Component } from '@angular/core';
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
DialogService DialogService
} from 'shared'; } from 'shared';
@ -19,8 +20,8 @@ import {
templateUrl: './settings-area.component.html' templateUrl: './settings-area.component.html'
}) })
export class SettingsAreaComponent extends AppComponentBase { export class SettingsAreaComponent extends AppComponentBase {
constructor(apps: AppsStoreService, dialogs: DialogService constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
} }

7
src/Squidex/app/features/webhooks/pages/webhook-events-page.component.ts

@ -10,6 +10,7 @@ import { Component, OnInit } from '@angular/core';
import { import {
AppComponentBase, AppComponentBase,
AppsStoreService, AppsStoreService,
AuthService,
DialogService, DialogService,
ImmutableArray, ImmutableArray,
Pager, Pager,
@ -28,10 +29,10 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn
public selectedEventId: string | null = null; public selectedEventId: string | null = null;
constructor(dialogs: DialogService, appsStore: AppsStoreService, constructor(dialogs: DialogService, appsStore: AppsStoreService, authService: AuthService,
private readonly webhooksService: WebhooksService private readonly webhooksService: WebhooksService
) { ) {
super(dialogs, appsStore); super(dialogs, appsStore, authService);
} }
public ngOnInit() { public ngOnInit() {
@ -56,7 +57,7 @@ export class WebhookEventsPageComponent extends AppComponentBase implements OnIn
public enqueueEvent(event: WebhookEventDto) { public enqueueEvent(event: WebhookEventDto) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.webhooksService.enqueueEvent(app, event.id)) .switchMap(app => this.webhooksService.enqueueEvent(app, event.id))
.subscribe(dtos => { .subscribe(() => {
this.notifyInfo('Events enqueued. Will be send in a few seconds.'); this.notifyInfo('Events enqueued. Will be send in a few seconds.');
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);

12
src/Squidex/app/features/webhooks/pages/webhooks-page.component.ts

@ -18,7 +18,6 @@ import {
ImmutableArray, ImmutableArray,
SchemaDto, SchemaDto,
SchemasService, SchemasService,
Version,
WebhookDto, WebhookDto,
WebhooksService, WebhooksService,
UpdateWebhookDto UpdateWebhookDto
@ -46,13 +45,12 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
return this.addWebhookForm.controls['url'].value && this.addWebhookForm.controls['url'].value.length > 0; return this.addWebhookForm.controls['url'].value && this.addWebhookForm.controls['url'].value.length > 0;
} }
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly authService: AuthService,
private readonly schemasService: SchemasService, private readonly schemasService: SchemasService,
private readonly webhooksService: WebhooksService, private readonly webhooksService: WebhooksService,
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {
@ -91,7 +89,7 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version)) .switchMap(app => this.webhooksService.putWebhook(app, webhook.id, requestDto, webhook.version))
.subscribe(dto => { .subscribe(dto => {
this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.authService.user!.token)); this.webhooks = this.webhooks.replace(webhook, webhook.update(requestDto, this.userToken, dto.version));
this.notifyInfo('Webhook saved.'); this.notifyInfo('Webhook saved.');
}, error => { }, error => {
@ -107,10 +105,10 @@ export class WebhooksPageComponent extends AppComponentBase implements OnInit {
const requestDto = new CreateWebhookDto(this.addWebhookForm.controls['url'].value, []); const requestDto = new CreateWebhookDto(this.addWebhookForm.controls['url'].value, []);
const me = this.authService.user!.token; const me = this.userToken;
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.webhooksService.postWebhook(app, requestDto, me, DateTime.now(), new Version())) .switchMap(app => this.webhooksService.postWebhook(app, requestDto, me, DateTime.now()))
.subscribe(dto => { .subscribe(dto => {
this.webhooks = this.webhooks.push(dto); this.webhooks = this.webhooks.push(dto);

40
src/Squidex/app/framework/angular/http-extensions-impl.ts

@ -10,6 +10,14 @@ import { Observable } from 'rxjs';
import { Version } from './../utils/version'; import { Version } from './../utils/version';
export class Versioned<T> {
constructor(
public readonly version: Version,
public readonly payload: T
) {
}
}
export class ErrorDto { export class ErrorDto {
public get displayMessage(): string { public get displayMessage(): string {
let result = this.message; let result = this.message;
@ -46,28 +54,28 @@ export class ErrorDto {
} }
export module HTTP { export module HTTP {
export function getVersioned(http: HttpClient, url: string, version?: Version): Observable<any> { export function getVersioned<T>(http: HttpClient, url: string, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.get(url, { observe: 'response', headers }), version); return handleVersion(http.get<T>(url, { observe: 'response', headers }), version);
} }
export function postVersioned(http: HttpClient, url: string, body: any, version?: Version): Observable<any> { export function postVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.post(url, body, { observe: 'response', headers }), version); return handleVersion(http.post<T>(url, body, { observe: 'response', headers }), version);
} }
export function putVersioned(http: HttpClient, url: string, body: any, version?: Version): Observable<any> { export function putVersioned<T>(http: HttpClient, url: string, body: any, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.put(url, body, { observe: 'response', headers }), version); return handleVersion(http.put<T>(url, body, { observe: 'response', headers }), version);
} }
export function deleteVersioned(http: HttpClient, url: string, version?: Version): Observable<any> { export function deleteVersioned<T>(http: HttpClient, url: string, version?: Version): Observable<Versioned<HttpResponse<T>>> {
const headers = createHeaders(version); const headers = createHeaders(version);
return handleVersion(http.delete(url, { observe: 'response', headers }), version); return handleVersion(http.delete<T>(url, { observe: 'response', headers }), version);
} }
function createHeaders(version?: Version): HttpHeaders { function createHeaders(version?: Version): HttpHeaders {
@ -78,16 +86,12 @@ export module HTTP {
} }
} }
function handleVersion(httpRequest: Observable<HttpResponse<any>>, version?: Version): Observable<any> { function handleVersion<T>(httpRequest: Observable<HttpResponse<T>>, version?: Version): Observable<Versioned<HttpResponse<T>>> {
return httpRequest.do((response: HttpResponse<any>) => { return httpRequest.map((response: HttpResponse<T>) => {
if (version && response.status.toString().indexOf('2') === 0 && response.headers) { const etag = response.headers.get('etag') || '';
const etag = response.headers.get('etag');
if (etag) { return new Versioned(new Version(etag), response);
version.update(etag); });
}
}
}).map((response: HttpResponse<any>) => response.body);
} }
} }
@ -105,7 +109,7 @@ export function pretifyError(message: string): Observable<any> {
result = new ErrorDto(response.status, errorDto.message, errorDto.details); result = new ErrorDto(response.status, errorDto.message, errorDto.details);
} }
} catch (e) { } catch (e) {
/* Ignore */ result = new ErrorDto(500, 'Failed to make the request.');
} }
} }

14
src/Squidex/app/framework/utils/version.spec.ts

@ -8,23 +8,9 @@
import { Version } from './../'; import { Version } from './../';
describe('Version', () => { describe('Version', () => {
it('should initialize with default value', () => {
const version = new Version();
expect(version.value).toBe('');
});
it('should initialize with init value', () => { it('should initialize with init value', () => {
const version = new Version('1.0'); const version = new Version('1.0');
expect(version.value).toBe('1.0'); expect(version.value).toBe('1.0');
}); });
it('should update to new value', () => {
const version = new Version();
version.update('1.0');
expect(version.value).toBe('1.0');
});
}); });

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

@ -6,16 +6,8 @@
*/ */
export class Version { export class Version {
public get value() {
return this.currentValue;
}
constructor( constructor(
private currentValue: string = '' public readonly value?: string
) { ) {
} }
public update(newValue: string) {
this.currentValue = newValue;
}
} }

13
src/Squidex/app/shared/components/app.component-base.ts

@ -7,15 +7,24 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppsStoreService, DialogService } from './../declarations-base'; import {
AppsStoreService,
AuthService,
DialogService
} from './../declarations-base';
import { ComponentBase } from './component-base'; import { ComponentBase } from './component-base';
export abstract class AppComponentBase extends ComponentBase { export abstract class AppComponentBase extends ComponentBase {
private appName$: Observable<string>; private appName$: Observable<string>;
public get userToken(): string {
return this.authService.user!.token;
}
constructor(dialogs: DialogService, constructor(dialogs: DialogService,
private readonly appsStore: AppsStoreService protected readonly appsStore: AppsStoreService,
protected readonly authService: AuthService
) { ) {
super(dialogs); super(dialogs);

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

@ -13,7 +13,6 @@ import { AppComponentBase } from './app.component-base';
import { import {
AppsStoreService, AppsStoreService,
AssetDto, AssetDto,
AssetReplacedDto,
AssetsService, AssetsService,
AuthService, AuthService,
DateTime, DateTime,
@ -21,7 +20,8 @@ import {
fadeAnimation, fadeAnimation,
ModalView, ModalView,
UpdateAssetDto, UpdateAssetDto,
Version Version,
Versioned
} from './../declarations-base'; } from './../declarations-base';
@Component({ @Component({
@ -71,12 +71,11 @@ export class AssetComponent extends AppComponentBase implements OnInit {
public progress = 0; public progress = 0;
constructor(apps: AppsStoreService, dialogs: DialogService, constructor(apps: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService
private readonly authService: AuthService
) { ) {
super(dialogs, apps); super(dialogs, apps, authService);
} }
public ngOnInit() { public ngOnInit() {
@ -84,7 +83,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
if (initFile) { if (initFile) {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.assetsService.uploadFile(app, initFile, this.authService.user!.token, DateTime.now())) .switchMap(app => this.assetsService.uploadFile(app, initFile, this.userToken, DateTime.now()))
.subscribe(dto => { .subscribe(dto => {
if (dto instanceof AssetDto) { if (dto instanceof AssetDto) {
this.emitLoaded(dto); this.emitLoaded(dto);
@ -104,8 +103,8 @@ export class AssetComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.assetVersion)) .switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.assetVersion))
.subscribe(dto => { .subscribe(dto => {
if (dto instanceof AssetReplacedDto) { if (dto instanceof Versioned) {
this.updateAsset(this.asset.update(dto, this.authService.user!.token), true); this.updateAsset(this.asset.update(dto.payload, this.userToken, dto.version), true);
} else { } else {
this.setProgress(dto); this.setProgress(dto);
} }
@ -126,8 +125,8 @@ export class AssetComponent extends AppComponentBase implements OnInit {
this.appNameOnce() this.appNameOnce()
.switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.assetVersion)) .switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.assetVersion))
.subscribe(() => { .subscribe(dto => {
this.updateAsset(this.asset.rename(requestDto.fileName, this.authService.user!.token), true); this.updateAsset(this.asset.rename(requestDto.fileName, this.userToken, dto.version), true);
this.resetRenameForm(); this.resetRenameForm();
}, error => { }, error => {
this.notifyError(error); this.notifyError(error);

5
src/Squidex/app/shared/components/history.component.ts

@ -14,6 +14,7 @@ import { AppComponentBase } from './app.component-base';
import { import {
allParams, allParams,
AppsStoreService, AppsStoreService,
AuthService,
DialogService, DialogService,
HistoryChannelUpdated, HistoryChannelUpdated,
HistoryEventDto, HistoryEventDto,
@ -54,13 +55,13 @@ export class HistoryComponent extends AppComponentBase {
.switchMap(() => this.appNameOnce()) .switchMap(() => this.appNameOnce())
.switchMap(app => this.historyService.getHistory(app, this.channel).retry(2)); .switchMap(app => this.historyService.getHistory(app, this.channel).retry(2));
constructor(appsStore: AppsStoreService, dialogs: DialogService, constructor(appsStore: AppsStoreService, dialogs: DialogService, authService: AuthService,
private readonly users: UsersProviderService, private readonly users: UsersProviderService,
private readonly historyService: HistoryService, private readonly historyService: HistoryService,
private readonly messageBus: MessageBus, private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute private readonly route: ActivatedRoute
) { ) {
super(dialogs, appsStore); super(dialogs, appsStore, authService);
} }
private userName(userId: string): Observable<string> { private userName(userId: string): Observable<string> {

8
src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts

@ -8,7 +8,11 @@
import { IMock, Mock } from 'typemoq'; import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppLanguageDto, AppLanguagesService } from 'shared'; import {
AppLanguagesDto,
AppLanguagesService,
Version
} from 'shared';
import { ResolveAppLanguagesGuard } from './resolve-app-languages.guard'; import { ResolveAppLanguagesGuard } from './resolve-app-languages.guard';
import { RouterMockup } from './router-mockup'; import { RouterMockup } from './router-mockup';
@ -68,7 +72,7 @@ describe('ResolveAppLanguagesGuard', () => {
}); });
it('should return languages if loading succeeded', (done) => { it('should return languages if loading succeeded', (done) => {
const languages: AppLanguageDto[] = []; const languages = new AppLanguagesDto([], new Version('2'));
appLanguagesService.setup(x => x.getLanguages('my-app')) appLanguagesService.setup(x => x.getLanguages('my-app'))
.returns(() => Observable.of(languages)); .returns(() => Observable.of(languages));

2
src/Squidex/app/shared/guards/resolve-app-languages.guard.ts

@ -31,7 +31,7 @@ export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[] | null
} }
const result = const result =
this.appLanguagesService.getLanguages(appName) this.appLanguagesService.getLanguages(appName).map(d => d.languages)
.do(dto => { .do(dto => {
if (!dto) { if (!dto) {
this.router.navigate(['/404']); this.router.navigate(['/404']);

51
src/Squidex/app/shared/services/app-clients.service.spec.ts

@ -13,12 +13,45 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
AppClientDto, AppClientDto,
AppClientsDto,
AppClientsService, AppClientsService,
CreateAppClientDto, CreateAppClientDto,
UpdateAppClientDto, UpdateAppClientDto,
Version Version
} from './../'; } from './../';
describe('AppClientsDto', () => {
const client1 = new AppClientDto('1', '1', '1', false);
const client2 = new AppClientDto('2', '2', '1', false);
const client2_new = new AppClientDto('2', '2 New', '1 New', false);
const version = new Version('1');
const newVersion = new Version('2');
it('should update clients when adding client', () => {
const clients_1 = new AppClientsDto([client1], version);
const clients_2 = clients_1.addClient(client2, newVersion);
expect(clients_2.clients).toEqual([client1, client2]);
expect(clients_2.version).toEqual(newVersion);
});
it('should update clients when removing client', () => {
const clients_1 = new AppClientsDto([client1, client2], version);
const clients_2 = clients_1.removeClient(client1, newVersion);
expect(clients_2.clients).toEqual([client2]);
expect(clients_2.version).toEqual(newVersion);
});
it('should update clients when updating client', () => {
const clients_1 = new AppClientsDto([client1, client2], version);
const clients_2 = clients_1.updateClient(client2_new, newVersion);
expect(clients_2.clients).toEqual([client1, client2_new]);
expect(clients_2.version).toEqual(newVersion);
});
});
describe('AppClientDto', () => { describe('AppClientDto', () => {
it('should update name property when renaming', () => { it('should update name property when renaming', () => {
const client_1 = new AppClientDto('1', 'old-name', 'secret', false); const client_1 = new AppClientDto('1', 'old-name', 'secret', false);
@ -58,16 +91,16 @@ describe('AppClientsService', () => {
it('should make get request to get app clients', it('should make get request to get app clients',
inject([AppClientsService, HttpTestingController], (appClientsService: AppClientsService, httpMock: HttpTestingController) => { inject([AppClientsService, HttpTestingController], (appClientsService: AppClientsService, httpMock: HttpTestingController) => {
let clients: AppClientDto[] | null = null; let clients: AppClientsDto | null = null;
appClientsService.getClients('my-app', version).subscribe(result => { appClientsService.getClients('my-app').subscribe(result => {
clients = result; clients = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/clients'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/clients');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([ req.flush([
{ {
@ -82,13 +115,17 @@ describe('AppClientsService', () => {
secret: 'secret2', secret: 'secret2',
isReader: true isReader: true
} }
]); ], {
headers: {
etag: '2'
}
});
expect(clients).toEqual( expect(clients).toEqual(
[ new AppClientsDto([
new AppClientDto('client1', 'Client 1', 'secret1', true), new AppClientDto('client1', 'Client 1', 'secret1', true),
new AppClientDto('client2', 'Client 2', 'secret2', true) new AppClientDto('client2', 'Client 2', 'secret2', true)
]); ], new Version('2')));
})); }));
it('should make post request to create client', it('should make post request to create client',
@ -99,7 +136,7 @@ describe('AppClientsService', () => {
let client: AppClientDto | null = null; let client: AppClientDto | null = null;
appClientsService.postClient('my-app', dto, version).subscribe(result => { appClientsService.postClient('my-app', dto, version).subscribe(result => {
client = result; client = result.payload;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/clients'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/clients');

59
src/Squidex/app/shared/services/app-clients.service.ts

@ -15,9 +15,30 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
Version Version,
Versioned
} from 'framework'; } from 'framework';
export class AppClientsDto {
constructor(
public readonly clients: AppClientDto[],
public readonly version: Version
) {
}
public addClient(client: AppClientDto, version: Version) {
return new AppClientsDto([...this.clients, client], version);
}
public updateClient(client: AppClientDto, version: Version) {
return new AppClientsDto(this.clients.map(c => c.id === client.id ? client : c), version);
}
public removeClient(client: AppClientDto, version: Version) {
return new AppClientsDto(this.clients.filter(c => c.id !== client.id), version);
}
}
export class AppClientDto { export class AppClientDto {
constructor( constructor(
public readonly id: string, public readonly id: string,
@ -68,34 +89,42 @@ export class AppClientsService {
) { ) {
} }
public getClients(appName: string, version?: Version): Observable<AppClientDto[]> { public getClients(appName: string): Observable<AppClientsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return HTTP.getVersioned(this.http, url, version) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { const clients = items.map(item => {
return new AppClientDto( return new AppClientDto(
item.id, item.id,
item.name || response.id, item.name || body.id,
item.secret, item.secret,
item.isReader); item.isReader);
}); });
return new AppClientsDto(clients, response.version);
}) })
.pretifyError('Failed to load clients. Please reload.'); .pretifyError('Failed to load clients. Please reload.');
} }
public postClient(appName: string, dto: CreateAppClientDto, version: Version): Observable<AppClientDto> { public postClient(appName: string, dto: CreateAppClientDto, version: Version): Observable<Versioned<AppClientDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`);
return HTTP.postVersioned(this.http, url, dto, version) return HTTP.postVersioned<any>(this.http, url, dto, version)
.map(response => { .map(response => {
return new AppClientDto( const body = response.payload.body;
response.id,
response.name || response.id, const client = new AppClientDto(
response.secret, body.id,
response.isReader); body.name || body.id,
body.secret,
body.isReader);
return new Versioned(response.version, client);
}) })
.do(() => { .do(() => {
this.analytics.trackEvent('Client', 'Created', appName); this.analytics.trackEvent('Client', 'Created', appName);
@ -103,7 +132,7 @@ export class AppClientsService {
.pretifyError('Failed to add client. Please reload.'); .pretifyError('Failed to add client. Please reload.');
} }
public updateClient(appName: string, id: string, dto: UpdateAppClientDto, version: Version): Observable<any> { public updateClient(appName: string, id: string, dto: UpdateAppClientDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
@ -113,7 +142,7 @@ export class AppClientsService {
.pretifyError('Failed to revoke client. Please reload.'); .pretifyError('Failed to revoke client. Please reload.');
} }
public deleteClient(appName: string, id: string, version: Version): Observable<any> { public deleteClient(appName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)

46
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -17,6 +17,38 @@ import {
Version Version
} from './../'; } from './../';
describe('AppContributorsDto', () => {
const contributor1 = new AppContributorDto('1', 'Owner');
const contributor2 = new AppContributorDto('2', 'Developer');
const contributor2_new = new AppContributorDto('2', 'Editor');
const version = new Version('1');
const newVersion = new Version('2');
it('should update contributors when adding contributor', () => {
const contributors_1 = new AppContributorsDto([contributor1], 4, version);
const contributors_2 = contributors_1.addContributor(contributor2, newVersion);
expect(contributors_2.contributors).toEqual([contributor1, contributor2]);
expect(contributors_2.version).toEqual(newVersion);
});
it('should update contributors when removing contributor', () => {
const contributors_1 = new AppContributorsDto([contributor1, contributor2], 4, version);
const contributors_2 = contributors_1.removeContributor(contributor1, newVersion);
expect(contributors_2.contributors).toEqual([contributor2]);
expect(contributors_2.version).toEqual(newVersion);
});
it('should update contributors when updating contributor', () => {
const contributors_1 = new AppContributorsDto([contributor1, contributor2], 4, version);
const contributors_2 = contributors_1.updateContributor(contributor2_new, newVersion);
expect(contributors_2.contributors).toEqual([contributor1, contributor2_new]);
expect(contributors_2.version).toEqual(newVersion);
});
});
describe('AppContributorDto', () => { describe('AppContributorDto', () => {
it('should update permission property when changing', () => { it('should update permission property when changing', () => {
const contributor_1 = new AppContributorDto('1', 'Owner'); const contributor_1 = new AppContributorDto('1', 'Owner');
@ -51,14 +83,14 @@ describe('AppContributorsService', () => {
let contributors: AppContributorsDto | null = null; let contributors: AppContributorsDto | null = null;
appContributorsService.getContributors('my-app', version).subscribe(result => { appContributorsService.getContributors('my-app').subscribe(result => {
contributors = result; contributors = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/contributors'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/contributors');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ req.flush({
contributors: [ contributors: [
@ -72,13 +104,17 @@ describe('AppContributorsService', () => {
} }
], ],
maxContributors: 100 maxContributors: 100
}, {
headers: {
etag: '2'
}
}); });
expect(contributors).toEqual( expect(contributors).toEqual(
new AppContributorsDto([ new AppContributorsDto([
new AppContributorDto('123', 'Owner'), new AppContributorDto('123', 'Owner'),
new AppContributorDto('456', 'Owner') new AppContributorDto('456', 'Owner')
], 100)); ], 100, new Version('2')));
})); }));
it('should make post request to assign contributor', it('should make post request to assign contributor',

52
src/Squidex/app/shared/services/app-contributors.service.ts

@ -15,27 +15,47 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
Version Version,
Versioned
} from 'framework'; } from 'framework';
export class AppContributorDto { export class AppContributorsDto {
constructor( constructor(
public readonly contributorId: string, public readonly contributors: AppContributorDto[],
public readonly permission: string public readonly maxContributors: number,
public readonly version: Version
) { ) {
} }
public changePermission(permission: string): AppContributorDto { public addContributor(contributor: AppContributorDto, version: Version) {
return new AppContributorDto(this.contributorId, permission); return new AppContributorsDto([...this.contributors, contributor], this.maxContributors, version);
}
public updateContributor(contributor: AppContributorDto, version: Version) {
return new AppContributorsDto(
this.contributors.map(c => c.contributorId === contributor.contributorId ? contributor : c),
this.maxContributors,
version);
}
public removeContributor(contributor: AppContributorDto, version: Version) {
return new AppContributorsDto(
this.contributors.filter(c => c.contributorId !== contributor.contributorId),
this.maxContributors,
version);
} }
} }
export class AppContributorsDto { export class AppContributorDto {
constructor( constructor(
public readonly contributors: AppContributorDto[], public readonly contributorId: string,
public readonly maxContributors: number public readonly permission: string
) { ) {
} }
public changePermission(permission: string): AppContributorDto {
return new AppContributorDto(this.contributorId, permission);
}
} }
@Injectable() @Injectable()
@ -47,12 +67,14 @@ export class AppContributorsService {
) { ) {
} }
public getContributors(appName: string, version?: Version): Observable<AppContributorsDto> { public getContributors(appName: string): Observable<AppContributorsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return HTTP.getVersioned(this.http, url, version) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response.contributors; const body = response.payload.body;
const items: any[] = body.contributors;
return new AppContributorsDto( return new AppContributorsDto(
items.map(item => { items.map(item => {
@ -60,12 +82,12 @@ export class AppContributorsService {
item.contributorId, item.contributorId,
item.permission); item.permission);
}), }),
response.maxContributors); body.maxContributors, response.version);
}) })
.pretifyError('Failed to load contributors. Please reload.'); .pretifyError('Failed to load contributors. Please reload.');
} }
public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable<any> { public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`);
return HTTP.postVersioned(this.http, url, dto, version) return HTTP.postVersioned(this.http, url, dto, version)
@ -75,7 +97,7 @@ export class AppContributorsService {
.pretifyError('Failed to add contributors. Please reload.'); .pretifyError('Failed to add contributors. Please reload.');
} }
public deleteContributor(appName: string, contributorId: string, version: Version): Observable<any> { public deleteContributor(appName: string, contributorId: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)

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

@ -13,11 +13,44 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
AppLanguageDto, AppLanguageDto,
AppLanguagesDto,
AppLanguagesService, AppLanguagesService,
UpdateAppLanguageDto, UpdateAppLanguageDto,
Version Version
} from './../'; } from './../';
describe('AppLanguageDto', () => {
const language1 = new AppLanguageDto('de', 'English', false, false, []);
const language2 = new AppLanguageDto('en', 'English', false, false, []);
const language2_new = new AppLanguageDto('en', 'English (United States)', false, false, []);
const version = new Version('1');
const newVersion = new Version('2');
it('should update languages when adding language', () => {
const languages_1 = new AppLanguagesDto([language1], version);
const languages_2 = languages_1.addLanguage(language2, newVersion);
expect(languages_2.languages).toEqual([language1, language2]);
expect(languages_2.version).toEqual(newVersion);
});
it('should update languages when removing language', () => {
const languages_1 = new AppLanguagesDto([language1, language2], version);
const languages_2 = languages_1.removeLanguage(language1, newVersion);
expect(languages_2.languages).toEqual([language2]);
expect(languages_2.version).toEqual(newVersion);
});
it('should update languages when updating language', () => {
const languages_1 = new AppLanguagesDto([language1, language2], version);
const languages_2 = languages_1.updateLanguage(language2_new, newVersion);
expect(languages_2.languages).toEqual([language1, language2_new]);
expect(languages_2.version).toEqual(newVersion);
});
});
describe('AppLanguageDto', () => { describe('AppLanguageDto', () => {
it('should update properties when updating', () => { it('should update properties when updating', () => {
const language_1 = new AppLanguageDto('de', 'English', false, false, []); const language_1 = new AppLanguageDto('de', 'English', false, false, []);
@ -52,16 +85,16 @@ describe('AppLanguagesService', () => {
it('should make get request to get app languages', it('should make get request to get app languages',
inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => { inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => {
let languages: AppLanguageDto[] | null = null; let languages: AppLanguagesDto | null = null;
appLanguagesService.getLanguages('my-app', version).subscribe(result => { appLanguagesService.getLanguages('my-app').subscribe(result => {
languages = result; languages = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/languages'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/languages');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([ req.flush([
{ {
@ -77,13 +110,17 @@ describe('AppLanguagesService', () => {
isOptional: false, isOptional: false,
englishName: 'Italian' englishName: 'Italian'
} }
]); ], {
headers: {
etag: '2'
}
});
expect(languages).toEqual( expect(languages).toEqual(
[ new AppLanguagesDto([
new AppLanguageDto('en', 'English', true, true, ['de', 'en']), new AppLanguageDto('en', 'English', true, true, ['de', 'en']),
new AppLanguageDto('it', 'Italian', false, false, []) new AppLanguageDto('it', 'Italian', false, false, [])
]); ], new Version('2')));
})); }));
it('should make post request to add language', it('should make post request to add language',
@ -94,7 +131,7 @@ describe('AppLanguagesService', () => {
let language: AppLanguageDto | null = null; let language: AppLanguageDto | null = null;
appLanguagesService.postLanguages('my-app', dto, version).subscribe(result => { appLanguagesService.postLanguages('my-app', dto, version).subscribe(result => {
language = result; language = result.payload;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/languages'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/languages');

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

@ -15,9 +15,30 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
Version Version,
Versioned
} from 'framework'; } from 'framework';
export class AppLanguagesDto {
constructor(
public readonly languages: AppLanguageDto[],
public readonly version: Version
) {
}
public addLanguage(language: AppLanguageDto, version: Version) {
return new AppLanguagesDto([...this.languages, language], version);
}
public updateLanguage(language: AppLanguageDto, version: Version) {
return new AppLanguagesDto(this.languages.map(l => l.iso2Code === language.iso2Code ? language : l), version);
}
public removeLanguage(language: AppLanguageDto, version: Version) {
return new AppLanguagesDto(this.languages.filter(l => l.iso2Code !== language.iso2Code), version);
}
}
export class AppLanguageDto { export class AppLanguageDto {
constructor( constructor(
public readonly iso2Code: string, public readonly iso2Code: string,
@ -58,14 +79,16 @@ export class AppLanguagesService {
) { ) {
} }
public getLanguages(appName: string, version?: Version): Observable<AppLanguageDto[]> { public getLanguages(appName: string): Observable<AppLanguagesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return HTTP.getVersioned(this.http, url, version) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { const languages = items.map(item => {
return new AppLanguageDto( return new AppLanguageDto(
item.iso2Code, item.iso2Code,
item.englishName, item.englishName,
@ -73,21 +96,27 @@ export class AppLanguagesService {
item.isOptional === true, item.isOptional === true,
item.fallback || []); item.fallback || []);
}); });
return new AppLanguagesDto(languages, response.version);
}) })
.pretifyError('Failed to load languages. Please reload.'); .pretifyError('Failed to load languages. Please reload.');
} }
public postLanguages(appName: string, dto: AddAppLanguageDto, version: Version): Observable<AppLanguageDto> { public postLanguages(appName: string, dto: AddAppLanguageDto, version: Version): Observable<Versioned<AppLanguageDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`);
return HTTP.postVersioned(this.http, url, dto, version) return HTTP.postVersioned<any>(this.http, url, dto, version)
.map(response => { .map(response => {
return new AppLanguageDto( const body = response.payload.body;
response.iso2Code,
response.englishName, const language = new AppLanguageDto(
response.isMaster === true, body.iso2Code,
response.isOptional === true, body.englishName,
response.fallback || []); body.isMaster === true,
body.isOptional === true,
body.fallback || []);
return new Versioned(response.version, language);
}) })
.do(() => { .do(() => {
this.analytics.trackEvent('Language', 'Added', appName); this.analytics.trackEvent('Language', 'Added', appName);
@ -95,7 +124,7 @@ export class AppLanguagesService {
.pretifyError('Failed to add language. Please reload.'); .pretifyError('Failed to add language. Please reload.');
} }
public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable<any> { public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
@ -105,7 +134,7 @@ export class AppLanguagesService {
.pretifyError('Failed to change language. Please reload.'); .pretifyError('Failed to change language. Please reload.');
} }
public deleteLanguage(appName: string, languageCode: string, version: Version): Observable<any> { public deleteLanguage(appName: string, languageCode: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)

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

@ -48,9 +48,11 @@ export class AppsService {
public getApps(): Observable<AppDto[]> { public getApps(): Observable<AppDto[]> {
const url = this.apiUrl.buildUrl('/api/apps'); const url = this.apiUrl.buildUrl('/api/apps');
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { return items.map(item => {
return new AppDto( return new AppDto(
@ -67,11 +69,13 @@ export class AppsService {
public postApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> { public postApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> {
const url = this.apiUrl.buildUrl('api/apps'); const url = this.apiUrl.buildUrl('api/apps');
return HTTP.postVersioned(this.http, url, dto) return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => { .map(response => {
const body = response.payload.body;
now = now || DateTime.now(); now = now || DateTime.now();
return new AppDto(response.id, dto.name, 'Owner', now, now); return new AppDto(body.id, dto.name, 'Owner', now, now);
}) })
.do(() => { .do(() => {
this.analytics.trackEvent('App', 'Created', dto.name); this.analytics.trackEvent('App', 'Created', dto.name);

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

@ -18,7 +18,8 @@ import {
DateTime, DateTime,
LocalCacheService, LocalCacheService,
UpdateAssetDto, UpdateAssetDto,
Version Version,
Versioned
} from './../'; } from './../';
describe('AssetDto', () => { describe('AssetDto', () => {
@ -26,22 +27,24 @@ describe('AssetDto', () => {
const creator = 'not-me'; const creator = 'not-me';
const modified = DateTime.now(); const modified = DateTime.now();
const modifier = 'me'; const modifier = 'me';
const version = new Version('2'); const version = new Version('1');
const newVersion = new Version('2');
it('should update name property and user info when renaming', () => { it('should update name property and user info when renaming', () => {
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, version); const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, version);
const asset_2 = asset_1.rename('new-name.png', modifier, modified); const asset_2 = asset_1.rename('new-name.png', modifier, newVersion, modified);
expect(asset_2.fileName).toEqual('new-name.png'); expect(asset_2.fileName).toEqual('new-name.png');
expect(asset_2.lastModified).toEqual(modified); expect(asset_2.lastModified).toEqual(modified);
expect(asset_2.lastModifiedBy).toEqual(modifier); expect(asset_2.lastModifiedBy).toEqual(modifier);
expect(asset_2.version).toEqual(newVersion);
}); });
it('should update file properties when uploading', () => { it('should update file properties when uploading', () => {
const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2, new Version('2')); const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2);
const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, version); const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'name.png', 'png', 1, 1, 'image/png', false, 1, 1, version);
const asset_2 = asset_1.update(update, modifier, modified); const asset_2 = asset_1.update(update, modifier, newVersion, modified);
expect(asset_2.fileSize).toEqual(2); expect(asset_2.fileSize).toEqual(2);
expect(asset_2.fileVersion).toEqual(2); expect(asset_2.fileVersion).toEqual(2);
@ -51,6 +54,7 @@ describe('AssetDto', () => {
expect(asset_2.pixelHeight).toEqual(2); expect(asset_2.pixelHeight).toEqual(2);
expect(asset_2.lastModified).toEqual(modified); expect(asset_2.lastModified).toEqual(modified);
expect(asset_2.lastModifiedBy).toEqual(modifier); expect(asset_2.lastModifiedBy).toEqual(modifier);
expect(asset_2.version).toEqual(newVersion);
}); });
}); });
@ -164,7 +168,7 @@ describe('AssetsService', () => {
let asset: AssetDto | null = null; let asset: AssetDto | null = null;
assetsService.getAsset('my-app', '123', version).subscribe(result => { assetsService.getAsset('my-app', '123').subscribe(result => {
asset = result; asset = result;
}); });
@ -186,8 +190,11 @@ describe('AssetsService', () => {
mimeType: 'image/png', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048
version: 11 }, {
headers: {
etag: '2'
}
}); });
expect(asset).toEqual( expect(asset).toEqual(
@ -203,7 +210,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
new Version('11'))); new Version('2')));
})); }));
it('should provide entry from cache if not found', it('should provide entry from cache if not found',
@ -215,7 +222,7 @@ describe('AssetsService', () => {
let asset: AssetDto | null = null; let asset: AssetDto | null = null;
assetsService.getAsset('my-app', '123', version).subscribe(result => { assetsService.getAsset('my-app', '123').subscribe(result => {
asset = result; asset = result;
}); });
@ -291,8 +298,11 @@ describe('AssetsService', () => {
mimeType: 'image/png', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048
version: 11 }, {
headers: {
etag: '2'
}
}); });
expect(asset).toEqual( expect(asset).toEqual(
@ -309,7 +319,7 @@ describe('AssetsService', () => {
true, true,
1024, 1024,
2048, 2048,
new Version('11'))); new Version('2')));
})); }));
it('should make put request to replace asset content', it('should make put request to replace asset content',
@ -318,7 +328,7 @@ describe('AssetsService', () => {
let asset: AssetReplacedDto | null = null; let asset: AssetReplacedDto | null = null;
assetsService.replaceFile('my-app', '123', null!, version).subscribe(result => { assetsService.replaceFile('my-app', '123', null!, version).subscribe(result => {
asset = <AssetReplacedDto>result; asset = (<Versioned<AssetReplacedDto>>result).payload;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123/content'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123/content');
@ -332,8 +342,7 @@ describe('AssetsService', () => {
mimeType: 'image/png', mimeType: 'image/png',
isImage: true, isImage: true,
pixelWidth: 1024, pixelWidth: 1024,
pixelHeight: 2048, pixelHeight: 2048
version: 11
}); });
expect(asset).toEqual( expect(asset).toEqual(
@ -342,8 +351,7 @@ describe('AssetsService', () => {
'image/png', 'image/png',
true, true,
1024, 1024,
2048, 2048));
new Version('11')));
})); }));
it('should make put request to update asset', it('should make put request to update asset',

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

@ -15,7 +15,8 @@ import {
DateTime, DateTime,
LocalCacheService, LocalCacheService,
HTTP, HTTP,
Version Version,
Versioned
} from 'framework'; } from 'framework';
export class AssetsDto { export class AssetsDto {
@ -45,7 +46,7 @@ export class AssetDto {
) { ) {
} }
public update(update: AssetReplacedDto, user: string, now?: DateTime): AssetDto { public update(update: AssetReplacedDto, user: string, version: Version, now?: DateTime): AssetDto {
return new AssetDto( return new AssetDto(
this.id, this.id,
this.createdBy, user, this.createdBy, user,
@ -58,10 +59,10 @@ export class AssetDto {
update.isImage, update.isImage,
update.pixelWidth, update.pixelWidth,
update.pixelHeight, update.pixelHeight,
update.version); version);
} }
public rename(name: string, user: string, now?: DateTime): AssetDto { public rename(name: string, user: string, version: Version, now?: DateTime): AssetDto {
return new AssetDto( return new AssetDto(
this.id, this.id,
this.createdBy, user, this.createdBy, user,
@ -74,7 +75,7 @@ export class AssetDto {
this.isImage, this.isImage,
this.pixelWidth, this.pixelWidth,
this.pixelHeight, this.pixelHeight,
this.version); version);
} }
} }
@ -92,8 +93,7 @@ export class AssetReplacedDto {
public readonly mimeType: string, public readonly mimeType: string,
public readonly isImage: boolean, public readonly isImage: boolean,
public readonly pixelWidth: number | null, public readonly pixelWidth: number | null,
public readonly pixelHeight: number | null, public readonly pixelHeight: number | null
public readonly version: Version
) { ) {
} }
} }
@ -130,11 +130,13 @@ export class AssetsService {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets?${fullQuery}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response.items; const body = response.payload.body;
const items: any[] = body.items;
return new AssetsDto(response.total, items.map(item => { return new AssetsDto(body.total, items.map(item => {
return new AssetDto( return new AssetDto(
item.id, item.id,
item.createdBy, item.createdBy,
@ -162,7 +164,10 @@ export class AssetsService {
reportProgress: true reportProgress: true
}); });
return this.http.request(req) return this.http.request<any>(req)
.filter(event =>
event.type === HttpEventType.UploadProgress ||
event.type === HttpEventType.Response)
.map(event => { .map(event => {
if (event.type === HttpEventType.UploadProgress) { if (event.type === HttpEventType.UploadProgress) {
const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0; const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
@ -187,7 +192,7 @@ export class AssetsService {
response.isImage, response.isImage,
response.pixelWidth, response.pixelWidth,
response.pixelHeight, response.pixelHeight,
new Version(response.version.toString())); new Version(event.headers.get('etag')));
this.localCache.set(`asset.${dto.id}`, dto, 5000); this.localCache.set(`asset.${dto.id}`, dto, 5000);
@ -196,34 +201,32 @@ export class AssetsService {
}) })
.do(dto => { .do(dto => {
this.analytics.trackEvent('Asset', 'Uploaded', appName); this.analytics.trackEvent('Asset', 'Uploaded', appName);
if (dto instanceof AssetDto) {
this.localCache.set(`asset.${dto.id}`, dto, 5000);
}
}) })
.pretifyError('Failed to upload asset. Please reload.'); .pretifyError('Failed to upload asset. Please reload.');
} }
public getAsset(appName: string, id: string, version?: Version): Observable<AssetDto> { public getAsset(appName: string, id: string): Observable<AssetDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const body = response.payload.body;
return new AssetDto( return new AssetDto(
response.id, body.id,
response.createdBy, body.createdBy,
response.lastModifiedBy, body.lastModifiedBy,
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(body.created),
DateTime.parseISO_UTC(response.lastModified), DateTime.parseISO_UTC(body.lastModified),
response.fileName, body.fileName,
response.fileType, body.fileType,
response.fileSize, body.fileSize,
response.fileVersion, body.fileVersion,
response.mimeType, body.mimeType,
response.isImage, body.isImage,
response.pixelWidth, body.pixelWidth,
response.pixelHeight, body.pixelHeight,
new Version(response.version.toString())); response.version);
}) })
.catch(error => { .catch(error => {
if (error instanceof HttpErrorResponse && error.status === 404) { if (error instanceof HttpErrorResponse && error.status === 404) {
@ -239,7 +242,7 @@ export class AssetsService {
.pretifyError('Failed to load assets. Please reload.'); .pretifyError('Failed to load assets. Please reload.');
} }
public replaceFile(appName: string, id: string, file: File, version: Version): Observable<number | AssetReplacedDto> { public replaceFile(appName: string, id: string, file: File, version: Version): Observable<number | Versioned<AssetReplacedDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}/content`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}/content`);
const req = new HttpRequest('PUT', url, getFormData(file), { const req = new HttpRequest('PUT', url, getFormData(file), {
@ -250,6 +253,9 @@ export class AssetsService {
}); });
return this.http.request(req) return this.http.request(req)
.filter(event =>
event.type === HttpEventType.UploadProgress ||
event.type === HttpEventType.Response)
.map(event => { .map(event => {
if (event.type === HttpEventType.UploadProgress) { if (event.type === HttpEventType.UploadProgress) {
const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0; const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
@ -258,16 +264,15 @@ export class AssetsService {
} else if (event instanceof HttpResponse) { } else if (event instanceof HttpResponse) {
const response: any = event.body; const response: any = event.body;
const dto = new AssetReplacedDto( const replaced = new AssetReplacedDto(
response.fileSize, response.fileSize,
response.fileVersion, response.fileVersion,
response.mimeType, response.mimeType,
response.isImage, response.isImage,
response.pixelWidth, response.pixelWidth,
response.pixelHeight, response.pixelHeight);
new Version(response.version.toString()));
return dto; return new Versioned(new Version(event.headers.get('etag')), replaced);
} }
}) })
.do(() => { .do(() => {
@ -276,7 +281,7 @@ export class AssetsService {
.pretifyError('Failed to replace asset. Please reload.'); .pretifyError('Failed to replace asset. Please reload.');
} }
public deleteAsset(appName: string, id: string, version: Version): Observable<any> { public deleteAsset(appName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)
@ -288,7 +293,7 @@ export class AssetsService {
.pretifyError('Failed to delete asset. Please reload.'); .pretifyError('Failed to delete asset. Please reload.');
} }
public putAsset(appName: string, id: string, dto: UpdateAssetDto, version: Version): Observable<any> { public putAsset(appName: string, id: string, dto: UpdateAssetDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)

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

@ -25,50 +25,56 @@ describe('ContentDto', () => {
const modified = DateTime.now(); const modified = DateTime.now();
const modifier = 'me'; const modifier = 'me';
const version = new Version('1'); const version = new Version('1');
const newVersion = new Version('2');
it('should update data property and user info when updating', () => { it('should update data property and user info when updating', () => {
const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.update({ data: 2 }, modifier, modified); const content_2 = content_1.update({ data: 2 }, modifier, newVersion, modified);
expect(content_2.data).toEqual({ data: 2 }); expect(content_2.data).toEqual({ data: 2 });
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
expect(content_2.version).toEqual(newVersion);
}); });
it('should update status property and user info when publishing', () => { it('should update status property and user info when publishing', () => {
const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.publish(modifier, modified); const content_2 = content_1.publish(modifier, newVersion, modified);
expect(content_2.status).toEqual('Published'); expect(content_2.status).toEqual('Published');
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
expect(content_2.version).toEqual(newVersion);
}); });
it('should update status property and user info when unpublishing', () => { it('should update status property and user info when unpublishing', () => {
const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.unpublish(modifier, modified); const content_2 = content_1.unpublish(modifier, newVersion, modified);
expect(content_2.status).toEqual('Draft'); expect(content_2.status).toEqual('Draft');
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
expect(content_2.version).toEqual(newVersion);
}); });
it('should update status property and user info when archiving', () => { it('should update status property and user info when archiving', () => {
const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Draft', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.archive(modifier, modified); const content_2 = content_1.archive(modifier, newVersion, modified);
expect(content_2.status).toEqual('Archived'); expect(content_2.status).toEqual('Archived');
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
expect(content_2.version).toEqual(newVersion);
}); });
it('should update status property and user info when restoring', () => { it('should update status property and user info when restoring', () => {
const content_1 = new ContentDto('1', 'Archived', creator, creator, creation, creation, { data: 1 }, version); const content_1 = new ContentDto('1', 'Archived', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.restore(modifier, modified); const content_2 = content_1.restore(modifier, newVersion, modified);
expect(content_2.status).toEqual('Draft'); expect(content_2.status).toEqual('Draft');
expect(content_2.lastModified).toEqual(modified); expect(content_2.lastModified).toEqual(modified);
expect(content_2.lastModifiedBy).toEqual(modifier); expect(content_2.lastModifiedBy).toEqual(modifier);
expect(content_2.version).toEqual(newVersion);
}); });
it('should update data property when setting data', () => { it('should update data property when setting data', () => {
@ -79,13 +85,6 @@ describe('ContentDto', () => {
expect(content_2.data).toBe(newData); expect(content_2.data).toBe(newData);
}); });
it('should replace version', () => {
const content_1 = new ContentDto('1', 'Published', creator, creator, creation, creation, { data: 1 }, version);
const content_2 = content_1.replaceVersion(new Version('2'));
expect(content_2.version).toEqual(new Version('2'));
});
}); });
describe('ContentsService', () => { describe('ContentsService', () => {
@ -220,14 +219,14 @@ describe('ContentsService', () => {
let content: ContentDto | null = null; let content: ContentDto | null = null;
contentsService.getContent('my-app', 'my-schema', '1', version).subscribe(result => { contentsService.getContent('my-app', 'my-schema', '1').subscribe(result => {
content = result; content = result;
}); });
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/1'); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/1');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBe(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ req.flush({
id: 'id1', id: 'id1',
@ -236,8 +235,11 @@ describe('ContentsService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
version: 11,
data: {} data: {}
}, {
headers: {
etag: '2'
}
}); });
expect(content).toEqual( expect(content).toEqual(
@ -245,7 +247,7 @@ describe('ContentsService', () => {
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
{}, {},
new Version('11'))); new Version('2')));
})); }));
it('should provide entry from cache if not found', it('should provide entry from cache if not found',
@ -257,14 +259,14 @@ describe('ContentsService', () => {
let content: ContentDto | null = null; let content: ContentDto | null = null;
contentsService.getContent('my-app', 'my-schema', '1', version).subscribe(result => { contentsService.getContent('my-app', 'my-schema', '1').subscribe(result => {
content = result; content = result;
}); });
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/1'); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/1');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBe(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({}, { status: 404, statusText: '404' }); req.flush({}, { status: 404, statusText: '404' });
@ -294,8 +296,11 @@ describe('ContentsService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
version: 11,
data: {} data: {}
}, {
headers: {
etag: '2'
}
}); });
expect(content).toEqual( expect(content).toEqual(
@ -303,7 +308,7 @@ describe('ContentsService', () => {
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
{}, {},
new Version('11'))); new Version('2')));
})); }));
it('should make get request to get versioned content data', it('should make get request to get versioned content data',
@ -320,7 +325,7 @@ describe('ContentsService', () => {
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/1'); const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1/1');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBe(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(response); req.flush(response);

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

@ -17,7 +17,8 @@ import {
DateTime, DateTime,
LocalCacheService, LocalCacheService,
HTTP, HTTP,
Version Version,
Versioned
} from 'framework'; } from 'framework';
export class ContentsDto { export class ContentsDto {
@ -53,50 +54,40 @@ export class ContentDto {
this.version); this.version);
} }
public publish(user: string, now?: DateTime): ContentDto { public publish(user: string, version: Version, now?: DateTime): ContentDto {
return this.changeStatus('Published', user, now); return this.changeStatus('Published', user, version, now);
} }
public unpublish(user: string, now?: DateTime): ContentDto { public unpublish(user: string, version: Version, now?: DateTime): ContentDto {
return this.changeStatus('Draft', user, now); return this.changeStatus('Draft', user, version, now);
} }
public archive(user: string, now?: DateTime): ContentDto { public archive(user: string, version: Version, now?: DateTime): ContentDto {
return this.changeStatus('Archived', user, now); return this.changeStatus('Archived', user, version, now);
} }
public restore(user: string, now?: DateTime): ContentDto { public restore(user: string, version: Version, now?: DateTime): ContentDto {
return this.changeStatus('Draft', user, now); return this.changeStatus('Draft', user, version, now);
} }
public replaceVersion(version: Version): ContentDto { private changeStatus(status: string, user: string, version: Version, now?: DateTime): ContentDto {
return new ContentDto(
this.id,
this.status,
this.createdBy, this.lastModifiedBy,
this.created, this.lastModified,
this.data,
version);
}
private changeStatus(status: string, user: string, now?: DateTime): ContentDto {
return new ContentDto( return new ContentDto(
this.id, this.id,
status, status,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.data, this.data,
this.version); version);
} }
public update(data: any, user: string, now?: DateTime): ContentDto { public update(data: any, user: string, version: Version, now?: DateTime): ContentDto {
return new ContentDto( return new ContentDto(
this.id, this.id,
this.status, this.status,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
data, data,
this.version); version);
} }
} }
@ -143,11 +134,13 @@ export class ContentsService {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?${fullQuery}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response.items; const body = response.payload.body;
const items: any[] = body.items;
return new ContentsDto(response.total, items.map(item => { return new ContentsDto(body.total, items.map(item => {
return new ContentDto( return new ContentDto(
item.id, item.id,
item.status, item.status,
@ -162,20 +155,22 @@ export class ContentsService {
.pretifyError('Failed to load contents. Please reload.'); .pretifyError('Failed to load contents. Please reload.');
} }
public getContent(appName: string, schemaName: string, id: string, version?: Version): Observable<ContentDto> { public getContent(appName: string, schemaName: string, id: string): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return HTTP.getVersioned(this.http, url, version) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const body = response.payload.body;
return new ContentDto( return new ContentDto(
response.id, body.id,
response.status, body.status,
response.createdBy, body.createdBy,
response.lastModifiedBy, body.lastModifiedBy,
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(body.created),
DateTime.parseISO_UTC(response.lastModified), DateTime.parseISO_UTC(body.lastModified),
response.data, body.data,
new Version(response.version.toString())); response.version);
}) })
.catch(error => { .catch(error => {
if (error instanceof HttpErrorResponse && error.status === 404) { if (error instanceof HttpErrorResponse && error.status === 404) {
@ -194,24 +189,29 @@ export class ContentsService {
public getVersionData(appName: string, schemaName: string, id: string, version: Version): Observable<any> { public getVersionData(appName: string, schemaName: string, id: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/${version.value}`);
return HTTP.getVersioned(this.http, url, version) return HTTP.getVersioned<any>(this.http, url)
.map(response => {
return response.payload.body;
})
.pretifyError('Failed to load data. Please reload.'); .pretifyError('Failed to load data. Please reload.');
} }
public postContent(appName: string, schemaName: string, dto: any, publish: boolean): Observable<ContentDto> { public postContent(appName: string, schemaName: string, dto: any, publish: boolean): Observable<ContentDto> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?publish=${publish}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}?publish=${publish}`);
return HTTP.postVersioned(this.http, url, dto) return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => { .map(response => {
const body = response.payload.body;
return new ContentDto( return new ContentDto(
response.id, body.id,
response.status, body.status,
response.createdBy, body.createdBy,
response.lastModifiedBy, body.lastModifiedBy,
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(body.created),
DateTime.parseISO_UTC(response.lastModified), DateTime.parseISO_UTC(body.lastModified),
response.data, body.data,
new Version(response.version.toString())); response.version);
}) })
.do(content => { .do(content => {
this.analytics.trackEvent('Content', 'Created', appName); this.analytics.trackEvent('Content', 'Created', appName);
@ -221,10 +221,15 @@ export class ContentsService {
.pretifyError('Failed to create content. Please reload.'); .pretifyError('Failed to create content. Please reload.');
} }
public putContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable<any> { public putContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
.map(response => {
const body = response.payload.body;
return new Versioned(response.version, body);
})
.do(() => { .do(() => {
this.analytics.trackEvent('Content', 'Updated', appName); this.analytics.trackEvent('Content', 'Updated', appName);
@ -233,7 +238,7 @@ export class ContentsService {
.pretifyError('Failed to update content. Please reload.'); .pretifyError('Failed to update content. Please reload.');
} }
public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> { public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)
@ -245,7 +250,7 @@ export class ContentsService {
.pretifyError('Failed to delete content. Please reload.'); .pretifyError('Failed to delete content. Please reload.');
} }
public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> { public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -255,7 +260,7 @@ export class ContentsService {
.pretifyError('Failed to publish content. Please reload.'); .pretifyError('Failed to publish content. Please reload.');
} }
public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> { public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -265,7 +270,7 @@ export class ContentsService {
.pretifyError('Failed to unpublish content. Please reload.'); .pretifyError('Failed to unpublish content. Please reload.');
} }
public archiveContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> { public archiveContent(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/archive`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/archive`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -275,7 +280,7 @@ export class ContentsService {
.pretifyError('Failed to archive content. Please reload.'); .pretifyError('Failed to archive content. Please reload.');
} }
public restoreContent(appName: string, schemaName: string, id: string, version: Version): Observable<any> { public restoreContent(appName: string, schemaName: string, id: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/restore`); const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/restore`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)

6
src/Squidex/app/shared/services/event-consumers.service.ts

@ -47,9 +47,11 @@ export class EventConsumersService {
public getEventConsumers(): Observable<EventConsumerDto[]> { public getEventConsumers(): Observable<EventConsumerDto[]> {
const url = this.apiUrl.buildUrl('/api/event-consumers'); const url = this.apiUrl.buildUrl('/api/event-consumers');
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { return items.map(item => {
return new EventConsumerDto( return new EventConsumerDto(

6
src/Squidex/app/shared/services/history.service.ts

@ -39,9 +39,11 @@ export class HistoryService {
public getHistory(appName: string, channel: string): Observable<HistoryEventDto[]> { public getHistory(appName: string, channel: string): Observable<HistoryEventDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/history?channel=${channel}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/history?channel=${channel}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { return items.map(item => {
return new HistoryEventDto( return new HistoryEventDto(

6
src/Squidex/app/shared/services/languages.service.ts

@ -32,9 +32,11 @@ export class LanguagesService {
public getLanguages(): Observable<LanguageDto[]> { public getLanguages(): Observable<LanguageDto[]> {
const url = this.apiUrl.buildUrl('api/languages'); const url = this.apiUrl.buildUrl('api/languages');
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { return items.map(item => {
return new LanguageDto( return new LanguageDto(

13
src/Squidex/app/shared/services/plans.service.spec.ts

@ -44,14 +44,14 @@ describe('PlansService', () => {
let plans: AppPlansDto | null = null; let plans: AppPlansDto | null = null;
plansService.getPlans('my-app', version).subscribe(result => { plansService.getPlans('my-app').subscribe(result => {
plans = result; plans = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/plans'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/plans');
expect(req.request.method).toEqual('GET'); expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBe(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ req.flush({
currentPlanId: '123', currentPlanId: '123',
@ -75,6 +75,10 @@ describe('PlansService', () => {
maxContributors: 6500 maxContributors: 6500
} }
] ]
}, {
headers: {
etag: '2'
}
}); });
expect(plans).toEqual( expect(plans).toEqual(
@ -85,7 +89,8 @@ describe('PlansService', () => {
[ [
new PlanDto('free', 'Free', '14 €', 1000, 1500, 2500), new PlanDto('free', 'Free', '14 €', 1000, 1500, 2500),
new PlanDto('prof', 'Prof', '18 €', 4000, 5500, 6500) new PlanDto('prof', 'Prof', '18 €', 4000, 5500, 6500)
] ],
new Version('2')
)); ));
})); }));
@ -97,7 +102,7 @@ describe('PlansService', () => {
let planChanged: PlanChangedDto | null = null; let planChanged: PlanChangedDto | null = null;
plansService.putPlan('my-app', dto, version).subscribe(result => { plansService.putPlan('my-app', dto, version).subscribe(result => {
planChanged = result; planChanged = result.payload;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/plan'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/plan');

40
src/Squidex/app/shared/services/plans.service.ts

@ -15,7 +15,8 @@ import {
AnalyticsService, AnalyticsService,
ApiUrlConfig, ApiUrlConfig,
HTTP, HTTP,
Version Version,
Versioned
} from 'framework'; } from 'framework';
export class AppPlansDto { export class AppPlansDto {
@ -23,9 +24,19 @@ export class AppPlansDto {
public readonly currentPlanId: string, public readonly currentPlanId: string,
public readonly planOwner: string, public readonly planOwner: string,
public readonly hasPortal: boolean, public readonly hasPortal: boolean,
public readonly plans: PlanDto[] public readonly plans: PlanDto[],
public readonly version: Version
) { ) {
} }
public changePlanId(planId: string, version?: Version): AppPlansDto {
return new AppPlansDto(
planId,
this.planOwner,
this.hasPortal,
this.plans,
version || this.version);
}
} }
export class PlanDto { export class PlanDto {
@ -63,17 +74,19 @@ export class PlansService {
) { ) {
} }
public getPlans(appName: string, version?: Version): Observable<AppPlansDto> { public getPlans(appName: string): Observable<AppPlansDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/plans`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/plans`);
return HTTP.getVersioned(this.http, url, version) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response.plans; const body = response.payload.body;
const items: any[] = body.plans;
return new AppPlansDto( return new AppPlansDto(
response.currentPlanId, body.currentPlanId,
response.planOwner, body.planOwner,
response.hasPortal, body.hasPortal,
items.map(item => { items.map(item => {
return new PlanDto( return new PlanDto(
item.id, item.id,
@ -82,17 +95,20 @@ export class PlansService {
item.maxApiCalls, item.maxApiCalls,
item.maxAssetSize, item.maxAssetSize,
item.maxContributors); item.maxContributors);
})); }),
response.version);
}) })
.pretifyError('Failed to load plans. Please reload.'); .pretifyError('Failed to load plans. Please reload.');
} }
public putPlan(appName: string, dto: ChangePlanDto, version: Version): Observable<PlanChangedDto> { public putPlan(appName: string, dto: ChangePlanDto, version: Version): Observable<Versioned<PlanChangedDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned<any>(this.http, url, dto, version)
.map(response => { .map(response => {
return new PlanChangedDto(response.redirectUri); const body = response.payload.body;
return new Versioned(response.version, new PlanChangedDto(body.redirectUri));
}) })
.do(() => { .do(() => {
this.analytics.trackEvent('Plan', 'Changed', appName); this.analytics.trackEvent('Plan', 'Changed', appName);

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

@ -34,34 +34,38 @@ describe('SchemaDto', () => {
const modified = DateTime.now(); const modified = DateTime.now();
const modifier = 'me'; const modifier = 'me';
const version = new Version('1'); const version = new Version('1');
const newVersion = new Version('2');
it('should update isPublished property and user info when publishing', () => { it('should update isPublished property and user info when publishing', () => {
const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version); const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version);
const schema_2 = schema_1.publish(modifier, modified); const schema_2 = schema_1.publish(modifier, newVersion, modified);
expect(schema_2.isPublished).toBeTruthy(); expect(schema_2.isPublished).toBeTruthy();
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update isPublished property and user info when unpublishing', () => { it('should update isPublished property and user info when unpublishing', () => {
const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version); const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version);
const schema_2 = schema_1.unpublish(modifier, modified); const schema_2 = schema_1.unpublish(modifier, newVersion, modified);
expect(schema_2.isPublished).toBeFalsy(); expect(schema_2.isPublished).toBeFalsy();
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update properties property and user info when updating', () => { it('should update properties property and user info when updating', () => {
const newProperties = new SchemaPropertiesDto('New Name'); const newProperties = new SchemaPropertiesDto('New Name');
const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version); const schema_1 = new SchemaDto('1', 'name', properties, false, creator, creator, creation, creation, version);
const schema_2 = schema_1.update(newProperties, modifier, modified); const schema_2 = schema_1.update(newProperties, modifier, newVersion, modified);
expect(schema_2.properties).toEqual(newProperties); expect(schema_2.properties).toEqual(newProperties);
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update scripts properties and user info when configure scripts', () => { it('should update scripts properties and user info when configure scripts', () => {
@ -74,7 +78,7 @@ describe('SchemaDto', () => {
'<script-change>'); '<script-change>');
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []); const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []);
const schema_2 = schema_1.configureScripts(newScripts, modifier, modified); const schema_2 = schema_1.configureScripts(newScripts, modifier, newVersion, modified);
expect(schema_2.scriptQuery).toEqual('<script-query>'); expect(schema_2.scriptQuery).toEqual('<script-query>');
expect(schema_2.scriptCreate).toEqual('<script-create>'); expect(schema_2.scriptCreate).toEqual('<script-create>');
@ -83,6 +87,7 @@ describe('SchemaDto', () => {
expect(schema_2.scriptChange).toEqual('<script-change>'); expect(schema_2.scriptChange).toEqual('<script-change>');
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
}); });
@ -93,34 +98,38 @@ describe('SchemaDetailsDto', () => {
const modified = DateTime.now(); const modified = DateTime.now();
const modifier = 'me'; const modifier = 'me';
const version = new Version('1'); const version = new Version('1');
const newVersion = new Version('2');
it('should update isPublished property and user info when publishing', () => { it('should update isPublished property and user info when publishing', () => {
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []); const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []);
const schema_2 = schema_1.publish(modifier, modified); const schema_2 = schema_1.publish(modifier, newVersion, modified);
expect(schema_2.isPublished).toBeTruthy(); expect(schema_2.isPublished).toBeTruthy();
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update isPublished property and user info when unpublishing', () => { it('should update isPublished property and user info when unpublishing', () => {
const schema_1 = new SchemaDetailsDto('1', 'name', properties, true, creator, creator, creation, creation, version, []); const schema_1 = new SchemaDetailsDto('1', 'name', properties, true, creator, creator, creation, creation, version, []);
const schema_2 = schema_1.unpublish(modifier, modified); const schema_2 = schema_1.unpublish(modifier, newVersion, modified);
expect(schema_2.isPublished).toBeFalsy(); expect(schema_2.isPublished).toBeFalsy();
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update properties property and user info when updating', () => { it('should update properties property and user info when updating', () => {
const newProperties = new SchemaPropertiesDto('New Name'); const newProperties = new SchemaPropertiesDto('New Name');
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []); const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, []);
const schema_2 = schema_1.update(newProperties, modifier, modified); const schema_2 = schema_1.update(newProperties, modifier, newVersion, modified);
expect(schema_2.properties).toEqual(newProperties); expect(schema_2.properties).toEqual(newProperties);
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update fields property and user info when adding field', () => { it('should update fields property and user info when adding field', () => {
@ -128,11 +137,12 @@ describe('SchemaDetailsDto', () => {
const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number')); const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number'));
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1]); const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1]);
const schema_2 = schema_1.addField(field2, modifier, modified); const schema_2 = schema_1.addField(field2, modifier, newVersion, modified);
expect(schema_2.fields).toEqual([field1, field2]); expect(schema_2.fields).toEqual([field1, field2]);
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update fields property and user info when removing field', () => { it('should update fields property and user info when removing field', () => {
@ -140,11 +150,12 @@ describe('SchemaDetailsDto', () => {
const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number')); const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number'));
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1, field2]); const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1, field2]);
const schema_2 = schema_1.removeField(field1, modifier, modified); const schema_2 = schema_1.removeField(field1, modifier, newVersion, modified);
expect(schema_2.fields).toEqual([field2]); expect(schema_2.fields).toEqual([field2]);
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update fields property and user info when replacing fields', () => { it('should update fields property and user info when replacing fields', () => {
@ -152,11 +163,12 @@ describe('SchemaDetailsDto', () => {
const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number')); const field2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Number'));
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1, field2]); const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1, field2]);
const schema_2 = schema_1.replaceFields([field2, field1], modifier, modified); const schema_2 = schema_1.replaceFields([field2, field1], modifier, newVersion, modified);
expect(schema_2.fields).toEqual([field2, field1]); expect(schema_2.fields).toEqual([field2, field1]);
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
it('should update fields property and user info when updating field', () => { it('should update fields property and user info when updating field', () => {
@ -165,11 +177,12 @@ describe('SchemaDetailsDto', () => {
const field2_2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Boolean')); const field2_2 = new FieldDto(2, '2', false, false, false, 'l', createProperties('Boolean'));
const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1_0, field2_1]); const schema_1 = new SchemaDetailsDto('1', 'name', properties, false, creator, creator, creation, creation, version, [field1_0, field2_1]);
const schema_2 = schema_1.updateField(field2_2, modifier, modified); const schema_2 = schema_1.updateField(field2_2, modifier, newVersion, modified);
expect(schema_2.fields).toEqual([field1_0, field2_2]); expect(schema_2.fields).toEqual([field1_0, field2_2]);
expect(schema_2.lastModified).toEqual(modified); expect(schema_2.lastModified).toEqual(modified);
expect(schema_2.lastModifiedBy).toEqual(modifier); expect(schema_2.lastModifiedBy).toEqual(modifier);
expect(schema_2.version).toEqual(newVersion);
}); });
}); });
@ -264,7 +277,7 @@ describe('SchemasService', () => {
let schema: SchemaDetailsDto | null = null; let schema: SchemaDetailsDto | null = null;
schemasService.getSchema('my-app', 'my-schema', version).subscribe(result => { schemasService.getSchema('my-app', 'my-schema').subscribe(result => {
schema = result; schema = result;
}); });
@ -281,7 +294,6 @@ describe('SchemasService', () => {
createdBy: 'Created1', createdBy: 'Created1',
lastModified: '2017-12-12T10:10', lastModified: '2017-12-12T10:10',
lastModifiedBy: 'LastModifiedBy1', lastModifiedBy: 'LastModifiedBy1',
version: 11,
properties: { properties: {
label: 'label1', label: 'label1',
hints: 'hints1' hints: 'hints1'
@ -381,13 +393,17 @@ describe('SchemasService', () => {
scriptUpdate: '<script-update>', scriptUpdate: '<script-update>',
scriptDelete: '<script-delete>', scriptDelete: '<script-delete>',
scriptChange: '<script-change>' scriptChange: '<script-change>'
}, {
headers: {
etag: '2'
}
}); });
expect(schema).toEqual( expect(schema).toEqual(
new SchemaDetailsDto('id1', 'name1', new SchemaPropertiesDto('label1', 'hints1'), true, 'Created1', 'LastModifiedBy1', new SchemaDetailsDto('id1', 'name1', new SchemaPropertiesDto('label1', 'hints1'), true, 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2016-12-12T10:10'),
DateTime.parseISO_UTC('2017-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'),
new Version('11'), new Version('2'),
[ [
new FieldDto(1, 'field1', true, true, true, 'language', createProperties('Number')), new FieldDto(1, 'field1', true, true, true, 'language', createProperties('Number')),
new FieldDto(2, 'field2', true, true, true, 'language', createProperties('String')), new FieldDto(2, 'field2', true, true, true, 'language', createProperties('String')),
@ -414,7 +430,7 @@ describe('SchemasService', () => {
let schema: SchemaDetailsDto | null = null; let schema: SchemaDetailsDto | null = null;
schemasService.getSchema('my-app', 'my-schema', version).subscribe(result => { schemasService.getSchema('my-app', 'my-schema').subscribe(result => {
schema = result; schema = result;
}); });
@ -435,19 +451,25 @@ describe('SchemasService', () => {
let schema: SchemaDetailsDto | null = null; let schema: SchemaDetailsDto | null = null;
schemasService.postSchema('my-app', dto, user, now, version).subscribe(result => { schemasService.postSchema('my-app', dto, user, now).subscribe(result => {
schema = result; schema = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas');
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBe(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: '1' }); req.flush({
id: '1'
}, {
headers: {
etag: '2'
}
});
expect(schema).toEqual( expect(schema).toEqual(
new SchemaDetailsDto('1', dto.name, new SchemaPropertiesDto(), false, user, user, now, now, version, [])); new SchemaDetailsDto('1', dto.name, new SchemaPropertiesDto(), false, user, user, now, now, new Version('2'), []));
})); }));
it('should make post request to add field', it('should make post request to add field',
@ -458,7 +480,7 @@ describe('SchemasService', () => {
let field: FieldDto | null = null; let field: FieldDto | null = null;
schemasService.postField('my-app', 'my-schema', dto, version).subscribe(result => { schemasService.postField('my-app', 'my-schema', dto, version).subscribe(result => {
field = result; field = result.payload;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/fields'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/fields');

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

@ -19,7 +19,8 @@ import {
LocalCacheService, LocalCacheService,
HTTP, HTTP,
ValidatorsEx, ValidatorsEx,
Version Version,
Versioned
} from 'framework'; } from 'framework';
export const fieldTypes: string[] = [ export const fieldTypes: string[] = [
@ -86,7 +87,7 @@ export class SchemaDto {
) { ) {
} }
public publish(user: string, now?: DateTime): SchemaDto { public publish(user: string, version: Version, now?: DateTime): SchemaDto {
return new SchemaDto( return new SchemaDto(
this.id, this.id,
this.name, this.name,
@ -94,10 +95,10 @@ export class SchemaDto {
true, true,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version); version);
} }
public unpublish(user: string, now?: DateTime): SchemaDto { public unpublish(user: string, version: Version, now?: DateTime): SchemaDto {
return new SchemaDto( return new SchemaDto(
this.id, this.id,
this.name, this.name,
@ -105,10 +106,10 @@ export class SchemaDto {
false, false,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version); version);
} }
public update(properties: SchemaPropertiesDto, user: string, now?: DateTime): SchemaDto { public update(properties: SchemaPropertiesDto, user: string, version: Version, now?: DateTime): SchemaDto {
return new SchemaDto( return new SchemaDto(
this.id, this.id,
this.name, this.name,
@ -116,7 +117,7 @@ export class SchemaDto {
this.isPublished, this.isPublished,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version); version);
} }
} }
@ -132,7 +133,7 @@ export class SchemaDetailsDto extends SchemaDto {
super(id, name, properties, isPublished, createdBy, lastModifiedBy, created, lastModified, version); super(id, name, properties, isPublished, createdBy, lastModifiedBy, created, lastModified, version);
} }
public publish(user: string, now?: DateTime): SchemaDetailsDto { public publish(user: string, version: Version, now?: DateTime): SchemaDetailsDto {
return new SchemaDetailsDto( return new SchemaDetailsDto(
this.id, this.id,
this.name, this.name,
@ -140,7 +141,7 @@ export class SchemaDetailsDto extends SchemaDto {
true, true,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
this.fields, this.fields,
this.scriptQuery, this.scriptQuery,
this.scriptCreate, this.scriptCreate,
@ -149,7 +150,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.scriptChange); this.scriptChange);
} }
public unpublish(user: string, now?: DateTime): SchemaDetailsDto { public unpublish(user: string, version: Version, now?: DateTime): SchemaDetailsDto {
return new SchemaDetailsDto( return new SchemaDetailsDto(
this.id, this.id,
this.name, this.name,
@ -157,7 +158,7 @@ export class SchemaDetailsDto extends SchemaDto {
false, false,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
this.fields, this.fields,
this.scriptQuery, this.scriptQuery,
this.scriptCreate, this.scriptCreate,
@ -166,7 +167,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.scriptChange); this.scriptChange);
} }
public configureScripts(scripts: UpdateSchemaScriptsDto, user: string, now?: DateTime): SchemaDetailsDto { public configureScripts(scripts: UpdateSchemaScriptsDto, user: string, version: Version, now?: DateTime): SchemaDetailsDto {
return new SchemaDetailsDto( return new SchemaDetailsDto(
this.id, this.id,
this.name, this.name,
@ -174,7 +175,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.isPublished, this.isPublished,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
this.fields, this.fields,
scripts.scriptQuery, scripts.scriptQuery,
scripts.scriptCreate, scripts.scriptCreate,
@ -183,7 +184,7 @@ export class SchemaDetailsDto extends SchemaDto {
scripts.scriptChange); scripts.scriptChange);
} }
public update(properties: SchemaPropertiesDto, user: string, now?: DateTime): SchemaDetailsDto { public update(properties: SchemaPropertiesDto, user: string, version: Version, now?: DateTime): SchemaDetailsDto {
return new SchemaDetailsDto( return new SchemaDetailsDto(
this.id, this.id,
this.name, this.name,
@ -191,7 +192,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.isPublished, this.isPublished,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
this.fields, this.fields,
this.scriptQuery, this.scriptQuery,
this.scriptCreate, this.scriptCreate,
@ -200,7 +201,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.scriptChange); this.scriptChange);
} }
public addField(field: FieldDto, user: string, now?: DateTime): SchemaDetailsDto { public addField(field: FieldDto, user: string, version: Version, now?: DateTime): SchemaDetailsDto {
return new SchemaDetailsDto( return new SchemaDetailsDto(
this.id, this.id,
this.name, this.name,
@ -208,7 +209,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.isPublished, this.isPublished,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
[...this.fields, field], [...this.fields, field],
this.scriptQuery, this.scriptQuery,
this.scriptCreate, this.scriptCreate,
@ -217,7 +218,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.scriptChange); this.scriptChange);
} }
public updateField(field: FieldDto, user: string, now?: DateTime): SchemaDetailsDto { public updateField(field: FieldDto, user: string, version: Version, now?: DateTime): SchemaDetailsDto {
return new SchemaDetailsDto( return new SchemaDetailsDto(
this.id, this.id,
this.name, this.name,
@ -225,7 +226,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.isPublished, this.isPublished,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
this.fields.map(f => f.fieldId === field.fieldId ? field : f), this.fields.map(f => f.fieldId === field.fieldId ? field : f),
this.scriptQuery, this.scriptQuery,
this.scriptCreate, this.scriptCreate,
@ -234,7 +235,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.scriptChange); this.scriptChange);
} }
public replaceFields(fields: FieldDto[], user: string, now?: DateTime): SchemaDetailsDto { public replaceFields(fields: FieldDto[], user: string, version: Version, now?: DateTime): SchemaDetailsDto {
return new SchemaDetailsDto( return new SchemaDetailsDto(
this.id, this.id,
this.name, this.name,
@ -242,7 +243,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.isPublished, this.isPublished,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
fields, fields,
this.scriptQuery, this.scriptQuery,
this.scriptCreate, this.scriptCreate,
@ -251,7 +252,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.scriptChange); this.scriptChange);
} }
public removeField(field: FieldDto, user: string, now?: DateTime): SchemaDetailsDto { public removeField(field: FieldDto, user: string, version: Version, now?: DateTime): SchemaDetailsDto {
return new SchemaDetailsDto( return new SchemaDetailsDto(
this.id, this.id,
this.name, this.name,
@ -259,7 +260,7 @@ export class SchemaDetailsDto extends SchemaDto {
this.isPublished, this.isPublished,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
this.fields.filter(f => f.fieldId !== field.fieldId), this.fields.filter(f => f.fieldId !== field.fieldId),
this.scriptQuery, this.scriptQuery,
this.scriptCreate, this.scriptCreate,
@ -698,9 +699,11 @@ export class SchemasService {
public getSchemas(appName: string): Observable<SchemaDto[]> { public getSchemas(appName: string): Observable<SchemaDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { return items.map(item => {
const properties = new SchemaPropertiesDto(item.properties.label, item.properties.hints); const properties = new SchemaPropertiesDto(item.properties.label, item.properties.hints);
@ -719,12 +722,14 @@ export class SchemasService {
.pretifyError('Failed to load schemas. Please reload.'); .pretifyError('Failed to load schemas. Please reload.');
} }
public getSchema(appName: string, id: string, version?: Version): Observable<SchemaDetailsDto> { public getSchema(appName: string, id: string): Observable<SchemaDetailsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const fields = response.fields.map((item: any) => { const body = response.payload.body;
const fields = body.fields.map((item: any) => {
const propertiesDto = const propertiesDto =
createProperties( createProperties(
item.properties.fieldType, item.properties.fieldType,
@ -740,23 +745,23 @@ export class SchemasService {
propertiesDto); propertiesDto);
}); });
const properties = new SchemaPropertiesDto(response.properties.label, response.properties.hints); const properties = new SchemaPropertiesDto(body.properties.label, body.properties.hints);
return new SchemaDetailsDto( return new SchemaDetailsDto(
response.id, body.id,
response.name, properties, body.name, properties,
response.isPublished, body.isPublished,
response.createdBy, body.createdBy,
response.lastModifiedBy, body.lastModifiedBy,
DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(body.created),
DateTime.parseISO_UTC(response.lastModified), DateTime.parseISO_UTC(body.lastModified),
new Version(response.version.toString()), response.version,
fields, fields,
response.scriptQuery, body.scriptQuery,
response.scriptCreate, body.scriptCreate,
response.scriptUpdate, body.scriptUpdate,
response.scriptDelete, body.scriptDelete,
response.scriptChange); body.scriptChange);
}) })
.catch(error => { .catch(error => {
if (error instanceof HttpErrorResponse && error.status === 404) { if (error instanceof HttpErrorResponse && error.status === 404) {
@ -772,15 +777,17 @@ export class SchemasService {
.pretifyError('Failed to load schema. Please reload.'); .pretifyError('Failed to load schema. Please reload.');
} }
public postSchema(appName: string, dto: CreateSchemaDto, user: string, now: DateTime, version: Version): Observable<SchemaDetailsDto> { public postSchema(appName: string, dto: CreateSchemaDto, user: string, now: DateTime): Observable<SchemaDetailsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`);
return HTTP.postVersioned(this.http, url, dto, version) return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => { .map(response => {
const body = response.payload.body;
now = now || DateTime.now(); now = now || DateTime.now();
return new SchemaDetailsDto( return new SchemaDetailsDto(
response.id, body.id,
dto.name, dto.name,
dto.properties || new SchemaPropertiesDto(), dto.properties || new SchemaPropertiesDto(),
false, false,
@ -788,13 +795,13 @@ export class SchemasService {
user, user,
now, now,
now, now,
version, response.version,
dto.fields || [], dto.fields || [],
response.scriptQuery, body.scriptQuery,
response.scriptCreate, body.scriptCreate,
response.scriptUpdate, body.scriptUpdate,
response.scriptDelete, body.scriptDelete,
response.scriptChange); body.scriptChange);
}) })
.do(schema => { .do(schema => {
this.analytics.trackEvent('Schema', 'Created', appName); this.analytics.trackEvent('Schema', 'Created', appName);
@ -805,19 +812,23 @@ export class SchemasService {
.pretifyError('Failed to create schema. Please reload.'); .pretifyError('Failed to create schema. Please reload.');
} }
public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable<FieldDto> { public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable<Versioned<FieldDto>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`);
return HTTP.postVersioned(this.http, url, dto, version) return HTTP.postVersioned<any>(this.http, url, dto, version)
.map(response => { .map(response => {
return new FieldDto( const body = response.payload.body;
response.id,
const field = new FieldDto(
body.id,
dto.name, dto.name,
false, false,
false, false,
false, false,
dto.partitioning, dto.partitioning,
dto.properties); dto.properties);
return new Versioned(response.version, field);
}) })
.do(() => { .do(() => {
this.analytics.trackEvent('Schema', 'FieldCreated', appName); this.analytics.trackEvent('Schema', 'FieldCreated', appName);
@ -825,7 +836,7 @@ export class SchemasService {
.pretifyError('Failed to add field. Please reload.'); .pretifyError('Failed to add field. Please reload.');
} }
public deleteSchema(appName: string, schemaName: string, version: Version): Observable<any> { public deleteSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)
@ -838,7 +849,7 @@ export class SchemasService {
.pretifyError('Failed to delete schema. Please reload.'); .pretifyError('Failed to delete schema. Please reload.');
} }
public putSchemaScripts(appName: string, schemaName: string, dto: UpdateSchemaScriptsDto, version: Version): Observable<any> { public putSchemaScripts(appName: string, schemaName: string, dto: UpdateSchemaScriptsDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/scripts`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/scripts`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
@ -848,7 +859,7 @@ export class SchemasService {
.pretifyError('Failed to update schema scripts. Please reload.'); .pretifyError('Failed to update schema scripts. Please reload.');
} }
public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable<any> { public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
@ -858,7 +869,7 @@ export class SchemasService {
.pretifyError('Failed to update schema. Please reload.'); .pretifyError('Failed to update schema. Please reload.');
} }
public putFieldOrdering(appName: string, schemaName: string, dto: number[], version: Version): Observable<any> { public putFieldOrdering(appName: string, schemaName: string, dto: number[], version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/ordering`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/ordering`);
return HTTP.putVersioned(this.http, url, { fieldIds: dto }, version) return HTTP.putVersioned(this.http, url, { fieldIds: dto }, version)
@ -868,7 +879,7 @@ export class SchemasService {
.pretifyError('Failed to reorder fields. Please reload.'); .pretifyError('Failed to reorder fields. Please reload.');
} }
public publishSchema(appName: string, schemaName: string, version: Version): Observable<any> { public publishSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -878,7 +889,7 @@ export class SchemasService {
.pretifyError('Failed to publish schema. Please reload.'); .pretifyError('Failed to publish schema. Please reload.');
} }
public unpublishSchema(appName: string, schemaName: string, version: Version): Observable<any> { public unpublishSchema(appName: string, schemaName: string, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -888,7 +899,7 @@ export class SchemasService {
.pretifyError('Failed to unpublish schema. Please reload.'); .pretifyError('Failed to unpublish schema. Please reload.');
} }
public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable<any> { public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
@ -898,7 +909,7 @@ export class SchemasService {
.pretifyError('Failed to update field. Please reload.'); .pretifyError('Failed to update field. Please reload.');
} }
public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> { public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -908,7 +919,7 @@ export class SchemasService {
.pretifyError('Failed to enable field. Please reload.'); .pretifyError('Failed to enable field. Please reload.');
} }
public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> { public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -918,7 +929,7 @@ export class SchemasService {
.pretifyError('Failed to disable field. Please reload.'); .pretifyError('Failed to disable field. Please reload.');
} }
public lockField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> { public lockField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/lock`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/lock`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -928,7 +939,7 @@ export class SchemasService {
.pretifyError('Failed to lock field. Please reload.'); .pretifyError('Failed to lock field. Please reload.');
} }
public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> { public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -938,7 +949,7 @@ export class SchemasService {
.pretifyError('Failed to show field. Please reload.'); .pretifyError('Failed to show field. Please reload.');
} }
public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> { public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`);
return HTTP.putVersioned(this.http, url, {}, version) return HTTP.putVersioned(this.http, url, {}, version)
@ -948,7 +959,7 @@ export class SchemasService {
.pretifyError('Failed to hide field. Please reload.'); .pretifyError('Failed to hide field. Please reload.');
} }
public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<any> { public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`);
return HTTP.deleteVersioned(this.http, url, version) return HTTP.deleteVersioned(this.http, url, version)

24
src/Squidex/app/shared/services/usages.service.ts

@ -62,9 +62,11 @@ export class UsagesService {
public getMonthCalls(app: string): Observable<CurrentCallsDto> { public getMonthCalls(app: string): Observable<CurrentCallsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/month`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/month`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
return new CurrentCallsDto(response.count, response.maxAllowed); const body = response.payload.body;
return new CurrentCallsDto(body.count, body.maxAllowed);
}) })
.pretifyError('Failed to load monthly api calls. Please reload.'); .pretifyError('Failed to load monthly api calls. Please reload.');
} }
@ -72,9 +74,11 @@ export class UsagesService {
public getTodayStorage(app: string): Observable<CurrentStorageDto> { public getTodayStorage(app: string): Observable<CurrentStorageDto> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
return new CurrentStorageDto(response.size, response.maxAllowed); const body = response.payload.body;
return new CurrentStorageDto(body.size, body.maxAllowed);
}) })
.pretifyError('Failed to load todays storage size. Please reload.'); .pretifyError('Failed to load todays storage size. Please reload.');
} }
@ -82,9 +86,11 @@ export class UsagesService {
public getCallsUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<CallsUsageDto[]> { public getCallsUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<CallsUsageDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { return items.map(item => {
return new CallsUsageDto( return new CallsUsageDto(
@ -99,9 +105,11 @@ export class UsagesService {
public getStorageUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<StorageUsageDto[]> { public getStorageUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable<StorageUsageDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`); const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate.toUTCStringFormat('YYYY-MM-DD')}/${toDate.toUTCStringFormat('YYYY-MM-DD')}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { return items.map(item => {
return new StorageUsageDto( return new StorageUsageDto(

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

@ -73,9 +73,11 @@ export class UsersService {
public getUsers(query?: string): Observable<UserDto[]> { public getUsers(query?: string): Observable<UserDto[]> {
const url = this.apiUrl.buildUrl(`api/users?query=${query || ''}`); const url = this.apiUrl.buildUrl(`api/users?query=${query || ''}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const body = response.payload.body;
const items: any[] = body;
return items.map(item => { return items.map(item => {
return new UserDto( return new UserDto(
@ -92,14 +94,16 @@ export class UsersService {
public getUser(id: string): Observable<UserDto> { public getUser(id: string): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/users/${id}`); const url = this.apiUrl.buildUrl(`api/users/${id}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const body = response.payload.body;
return new UserDto( return new UserDto(
response.id, body.id,
response.email, body.email,
response.displayName, body.displayName,
response.pictureUrl, body.pictureUrl,
response.isLocked); body.isLocked);
}) })
.pretifyError('Failed to load user. Please reload.'); .pretifyError('Failed to load user. Please reload.');
} }
@ -116,9 +120,11 @@ export class UserManagementService {
public getUsers(take: number, skip: number, query?: string): Observable<UsersDto> { public getUsers(take: number, skip: number, query?: string): Observable<UsersDto> {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`); const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response.items; const body = response.payload.body;
const items: any[] = body.items;
const users = items.map(item => { const users = items.map(item => {
return new UserDto( return new UserDto(
@ -129,7 +135,7 @@ export class UserManagementService {
item.isLocked); item.isLocked);
}); });
return new UsersDto(response.total, users); return new UsersDto(body.total, users);
}) })
.pretifyError('Failed to load users. Please reload.'); .pretifyError('Failed to load users. Please reload.');
} }
@ -137,14 +143,16 @@ export class UserManagementService {
public getUser(id: string): Observable<UserDto> { public getUser(id: string): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`); const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const body = response.payload.body;
return new UserDto( return new UserDto(
response.id, body.id,
response.email, body.email,
response.displayName, body.displayName,
response.pictureUrl, body.pictureUrl,
response.isLocked); body.isLocked);
}) })
.pretifyError('Failed to load user. Please reload.'); .pretifyError('Failed to load user. Please reload.');
} }
@ -152,9 +160,11 @@ export class UserManagementService {
public postUser(dto: CreateUserDto): Observable<UserDto> { public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl('api/user-management'); const url = this.apiUrl.buildUrl('api/user-management');
return HTTP.postVersioned(this.http, url, dto) return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => { .map(response => {
return new UserDto(response.id, dto.email, dto.displayName, response.pictureUrl, false); const body = response.payload.body;
return new UserDto(body.id, dto.email, dto.displayName, body.pictureUrl, false);
}) })
.pretifyError('Failed to create user. Please reload.'); .pretifyError('Failed to create user. Please reload.');
} }

14
src/Squidex/app/shared/services/webhooks.service.spec.ts

@ -29,6 +29,7 @@ describe('WebhookDto', () => {
const modified = DateTime.now(); const modified = DateTime.now();
const modifier = 'me'; const modifier = 'me';
const version = new Version('1'); const version = new Version('1');
const newVersion = new Version('2');
it('should update url and schemas', () => { it('should update url and schemas', () => {
const webhook_1 = new WebhookDto('id1', 'token1', creator, creator, creation, creation, version, [], 'http://squidex.io/hook', 1, 2, 3, 4); const webhook_1 = new WebhookDto('id1', 'token1', creator, creator, creation, creation, version, [], 'http://squidex.io/hook', 1, 2, 3, 4);
@ -37,12 +38,13 @@ describe('WebhookDto', () => {
[ [
new WebhookSchemaDto('1', true, true, true, true), new WebhookSchemaDto('1', true, true, true, true),
new WebhookSchemaDto('2', true, true, true, true) new WebhookSchemaDto('2', true, true, true, true)
]), modifier, modified); ]), modifier, newVersion, modified);
expect(webhook_2.url).toEqual('http://squidex.io/hook2'); expect(webhook_2.url).toEqual('http://squidex.io/hook2');
expect(webhook_2.schemas.length).toEqual(2); expect(webhook_2.schemas.length).toEqual(2);
expect(webhook_2.lastModified).toEqual(modified); expect(webhook_2.lastModified).toEqual(modified);
expect(webhook_2.lastModifiedBy).toEqual(modifier); expect(webhook_2.lastModifiedBy).toEqual(modifier);
expect(webhook_2.version).toEqual(newVersion);
}); });
}); });
@ -132,16 +134,20 @@ describe('WebhooksService', () => {
let webhook: WebhookDto | null = null; let webhook: WebhookDto | null = null;
webhooksService.postWebhook('my-app', dto, user, now, version).subscribe(result => { webhooksService.postWebhook('my-app', dto, user, now).subscribe(result => {
webhook = result; webhook = result;
}); });
const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/webhooks');
expect(req.request.method).toEqual('POST'); expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }); req.flush({ id: 'id1', sharedSecret: 'token1', schemaId: 'schema1' }, {
headers: {
etag: '1'
}
});
expect(webhook).toEqual( expect(webhook).toEqual(
new WebhookDto('id1', 'token1', user, user, now, now, version, [], 'http://squidex.io/hook', 0, 0, 0, 0)); new WebhookDto('id1', 'token1', user, user, now, now, version, [], 'http://squidex.io/hook', 0, 0, 0, 0));

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

@ -16,7 +16,8 @@ import {
ApiUrlConfig, ApiUrlConfig,
DateTime, DateTime,
HTTP, HTTP,
Version Version,
Versioned
} from 'framework'; } from 'framework';
export class WebhookDto { export class WebhookDto {
@ -37,13 +38,13 @@ export class WebhookDto {
) { ) {
} }
public update(update: UpdateWebhookDto, user: string, now?: DateTime): WebhookDto { public update(update: UpdateWebhookDto, user: string, version: Version, now?: DateTime): WebhookDto {
return new WebhookDto( return new WebhookDto(
this.id, this.id,
this.sharedSecret, this.sharedSecret,
this.createdBy, user, this.createdBy, user,
this.created, now || DateTime.now(), this.created, now || DateTime.now(),
this.version, version,
update.schemas, update.schemas,
update.url, update.url,
this.totalSucceeded, this.totalSucceeded,
@ -115,9 +116,9 @@ export class WebhooksService {
public getWebhooks(appName: string): Observable<WebhookDto[]> { public getWebhooks(appName: string): Observable<WebhookDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response; const items: any[] = response.payload.body;
return items.map(item => { return items.map(item => {
const schemas = item.schemas.map((schema: any) => const schemas = item.schemas.map((schema: any) =>
@ -147,19 +148,21 @@ export class WebhooksService {
.pretifyError('Failed to load webhooks. Please reload.'); .pretifyError('Failed to load webhooks. Please reload.');
} }
public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now: DateTime, version: Version): Observable<WebhookDto> { public postWebhook(appName: string, dto: CreateWebhookDto, user: string, now: DateTime): Observable<WebhookDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks`);
return HTTP.postVersioned(this.http, url, dto, version) return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => { .map(response => {
const body = response.payload.body;
return new WebhookDto( return new WebhookDto(
response.id, body.id,
response.sharedSecret, body.sharedSecret,
user, user,
user, user,
now, now,
now, now,
version, response.version,
dto.schemas, dto.schemas,
dto.url, dto.url,
0, 0, 0, 0); 0, 0, 0, 0);
@ -170,7 +173,7 @@ export class WebhooksService {
.pretifyError('Failed to create webhook. Please reload.'); .pretifyError('Failed to create webhook. Please reload.');
} }
public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version: Version): Observable<any> { public putWebhook(appName: string, id: string, dto: UpdateWebhookDto, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/${id}`);
return HTTP.putVersioned(this.http, url, dto, version) return HTTP.putVersioned(this.http, url, dto, version)
@ -193,11 +196,13 @@ export class WebhooksService {
public getEvents(appName: string, take: number, skip: number): Observable<WebhookEventsDto> { public getEvents(appName: string, take: number, skip: number): Observable<WebhookEventsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events?take=${take}&skip=${skip}`); const url = this.apiUrl.buildUrl(`api/apps/${appName}/webhooks/events?take=${take}&skip=${skip}`);
return HTTP.getVersioned(this.http, url) return HTTP.getVersioned<any>(this.http, url)
.map(response => { .map(response => {
const items: any[] = response.items; const body = response.payload.body;
const items: any[] = body.items;
return new WebhookEventsDto(response.total, items.map(item => { return new WebhookEventsDto(body.total, items.map(item => {
return new WebhookEventDto( return new WebhookEventDto(
item.id, item.id,
DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.created),

Loading…
Cancel
Save