From 0997858fc154b524df9dda1870f58e4a771316d4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 23 Sep 2019 13:12:08 +0200 Subject: [PATCH] Feature/sync schema (#417) * Sync schemas in UI --- src/Squidex/Config/Web/WebServices.cs | 6 +++ .../content/content-field.component.html | 4 +- .../pages/content/content-field.component.ts | 4 ++ .../content/shared/content.component.html | 2 +- .../content/shared/content.component.ts | 6 ++- .../schema/schema-export-form.component.html | 40 ++++++++++++++---- .../schema/schema-export-form.component.scss | 6 ++- .../schema/schema-export-form.component.ts | 42 ++++++++++++++++--- .../pages/schema/schema-page.component.html | 2 +- .../workflows/workflow-step.component.html | 8 ++-- .../workflows/workflow-step.component.ts | 16 +++++++ .../angular/forms/json-editor.component.scss | 2 +- .../framework/angular/panel.component.html | 2 +- .../app/framework/angular/panel.component.ts | 4 ++ .../framework/services/dialog.service.spec.ts | 4 +- .../app/framework/services/dialog.service.ts | 2 +- .../queries/filter-comparison.component.html | 2 +- .../queries/filter-comparison.component.ts | 6 ++- .../queries/filter-logical.component.html | 4 +- .../queries/filter-logical.component.ts | 6 ++- .../shared/services/schemas.service.spec.ts | 27 ++++++++++++ .../app/shared/services/schemas.service.ts | 26 +++++++++++- src/Squidex/app/shared/state/schemas.forms.ts | 10 +++++ .../app/shared/state/schemas.state.spec.ts | 16 +++++++ src/Squidex/app/shared/state/schemas.state.ts | 8 ++++ 25 files changed, 218 insertions(+), 37 deletions(-) diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs index 7cbcc446d..97ef2eb56 100644 --- a/src/Squidex/Config/Web/WebServices.cs +++ b/src/Squidex/Config/Web/WebServices.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -52,6 +53,11 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsOptional(); + services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + services.AddMvc(options => { options.Filters.Add(); diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 970faac5e..034c25fdb 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -9,7 +9,7 @@ @@ -67,7 +67,7 @@ diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.ts b/src/Squidex/app/features/content/pages/content/content-field.component.ts index 8355376dc..40f587cdf 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-field.component.ts @@ -184,6 +184,10 @@ export class ContentFieldComponent implements DoCheck, OnChanges { } } + public emitLanguageChange(language: AppLanguageDto) { + this.languageChange.emit(language); + } + public prefix(language: AppLanguageDto) { return `(${language.iso2Code})`; } diff --git a/src/Squidex/app/features/content/shared/content.component.html b/src/Squidex/app/features/content/shared/content.component.html index a43854b26..509a397f6 100644 --- a/src/Squidex/app/features/content/shared/content.component.html +++ b/src/Squidex/app/features/content/shared/content.component.html @@ -4,7 +4,7 @@ + (ngModelChange)="emitSelectedChange($event)" /> diff --git a/src/Squidex/app/features/content/shared/content.component.ts b/src/Squidex/app/features/content/shared/content.component.ts index dc42ed5ad..56ad23dd7 100644 --- a/src/Squidex/app/features/content/shared/content.component.ts +++ b/src/Squidex/app/features/content/shared/content.component.ts @@ -42,7 +42,7 @@ export class ContentComponent implements OnChanges { public statusChange = new EventEmitter(); @Output() - public selectedChange = new EventEmitter(); + public selectedChange = new EventEmitter(); @Input() public selected = false; @@ -140,6 +140,10 @@ export class ContentComponent implements OnChanges { this.updateValues(); } + public emitSelectedChange(isSelected: boolean) { + this.selectedChange.emit(isSelected); + } + public emitDelete() { this.delete.emit(); } diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html index 56dec217d..b6361cefb 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html @@ -1,9 +1,31 @@ - - - Export Schema - - - - - - \ No newline at end of file +
+ + + Export Schema + + + + + + + +
+
+ + +
+ +
+ + +
+
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss index fbb752506..1adbc54e4 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss +++ b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss @@ -1,2 +1,6 @@ @import '_vars'; -@import '_mixins'; \ No newline at end of file +@import '_mixins'; + +.json { + min-height: 500px; +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts index dcf003200..abdfea678 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts @@ -5,26 +5,56 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; -import { SchemaDetailsDto } from '@app/shared'; +import { + SchemaDetailsDto, + SchemasState, + SynchronizeSchemaForm +} from '@app/shared'; @Component({ selector: 'sqx-schema-export-form', styleUrls: ['./schema-export-form.component.scss'], templateUrl: './schema-export-form.component.html' }) -export class SchemaExportFormComponent implements OnInit { +export class SchemaExportFormComponent implements OnChanges { @Output() public complete = new EventEmitter(); @Input() public schema: SchemaDetailsDto; - public export: any; + public synchronizeForm = new SynchronizeSchemaForm(this.formBuilder); - public ngOnInit() { - this.export = this.schema.export(); + constructor( + private readonly formBuilder: FormBuilder, + private readonly schemasState: SchemasState + ) { + } + + public ngOnChanges() { + this.synchronizeForm.form.get('json')!.setValue(this.schema.export()); + } + + public synchronizeSchema() { + const value = this.synchronizeForm.submit(); + + if (value) { + const request = { + ...value.json, + noFieldDeletion: !value.fieldsDelete, + noFieldRecreation: !value.fieldsDelete + }; + + this.schemasState.synchronize(this.schema, request) + .subscribe(() => { + this.synchronizeForm.submitCompleted(); + }, error => { + this.synchronizeForm.submitFailed(error); + }); + } } public emitComplete() { diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index a4689d3e4..d3bebe6cc 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -7,7 +7,7 @@
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html index 7a899def9..52ae9dba2 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html @@ -1,7 +1,7 @@
-
-
@@ -40,7 +40,7 @@ [transition]="transition" [disabled]="disabled" [roles]="roles" - (remove)="transitionRemove.emit(transition)" + (remove)="emitTransitionRemove(transition)" (update)="changeTransition(transition, $event)"> @@ -56,7 +56,7 @@
-
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts index c33fe36f7..c73fab4c7 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts +++ b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts @@ -88,6 +88,22 @@ export class WorkflowStepComponent implements OnChanges { this.update.emit({ noUpdate }); } + public emitMakeInitial() { + this.makeInitial.emit(); + } + + public emitTransitionAdd(transition: WorkflowStep) { + this.transitionAdd.emit(transition); + } + + public emitTransitionRemove(transition: WorkflowTransition) { + this.transitionRemove.emit(transition); + } + + public emitRemove() { + this.remove.emit(); + } + public trackByTransition(index: number, transition: WorkflowTransition) { return transition.to; } diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.scss b/src/Squidex/app/framework/angular/forms/json-editor.component.scss index 9a7b03689..ebf88817e 100644 --- a/src/Squidex/app/framework/angular/forms/json-editor.component.scss +++ b/src/Squidex/app/framework/angular/forms/json-editor.component.scss @@ -3,7 +3,7 @@ // sass-lint:disable class-name-format -$editor-height: 20rem; +$editor-height: 30rem; :host ::ng-deep { .ace_editor { diff --git a/src/Squidex/app/framework/angular/panel.component.html b/src/Squidex/app/framework/angular/panel.component.html index 11dc4494d..2223810c6 100644 --- a/src/Squidex/app/framework/angular/panel.component.html +++ b/src/Squidex/app/framework/angular/panel.component.html @@ -18,7 +18,7 @@ - + diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index b4f593f49..8f7015f50 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/src/Squidex/app/framework/angular/panel.component.ts @@ -124,4 +124,8 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { } } } + + public emitClose() { + this.close.emit(); + } } \ No newline at end of file diff --git a/src/Squidex/app/framework/services/dialog.service.spec.ts b/src/Squidex/app/framework/services/dialog.service.spec.ts index 376841628..5337b2684 100644 --- a/src/Squidex/app/framework/services/dialog.service.spec.ts +++ b/src/Squidex/app/framework/services/dialog.service.spec.ts @@ -29,7 +29,7 @@ describe('DialogService', () => { it('should create error notification', () => { const notification = Notification.error('MyError'); - expect(notification.displayTime).toBe(5000); + expect(notification.displayTime).toBe(10000); expect(notification.message).toBe('MyError'); expect(notification.messageType).toBe('danger'); }); @@ -37,7 +37,7 @@ describe('DialogService', () => { it('should create info notification', () => { const notification = Notification.info('MyInfo'); - expect(notification.displayTime).toBe(5000); + expect(notification.displayTime).toBe(10000); expect(notification.message).toBe('MyInfo'); expect(notification.messageType).toBe('info'); }); diff --git a/src/Squidex/app/framework/services/dialog.service.ts b/src/Squidex/app/framework/services/dialog.service.ts index 087f09626..1def51649 100644 --- a/src/Squidex/app/framework/services/dialog.service.ts +++ b/src/Squidex/app/framework/services/dialog.service.ts @@ -47,7 +47,7 @@ export class Notification { constructor( public readonly message: string, public readonly messageType: string, - public readonly displayTime: number = 5000 + public readonly displayTime: number = 10000 ) { } diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.html b/src/Squidex/app/shared/components/queries/filter-comparison.component.html index 626f506a6..33c65524b 100644 --- a/src/Squidex/app/shared/components/queries/filter-comparison.component.html +++ b/src/Squidex/app/shared/components/queries/filter-comparison.component.html @@ -60,7 +60,7 @@
-
diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.ts b/src/Squidex/app/shared/components/queries/filter-comparison.component.ts index ca18b65e5..af8497792 100644 --- a/src/Squidex/app/shared/components/queries/filter-comparison.component.ts +++ b/src/Squidex/app/shared/components/queries/filter-comparison.component.ts @@ -100,7 +100,11 @@ export class FilterComparisonComponent implements OnChanges { this.fieldModel = newModel; } - private emitChange() { + public emitRemove() { + this.remove.emit(); + } + + public emitChange() { this.change.emit(); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.html b/src/Squidex/app/shared/components/queries/filter-logical.component.html index aa7e1eae9..5f04ae50d 100644 --- a/src/Squidex/app/shared/components/queries/filter-logical.component.html +++ b/src/Squidex/app/shared/components/queries/filter-logical.component.html @@ -13,7 +13,7 @@
-
@@ -26,7 +26,7 @@ + (remove)="removeFilter(filter)" (change)="emitChange()"> diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.ts b/src/Squidex/app/shared/components/queries/filter-logical.component.ts index 40c494749..6bc74b4fa 100644 --- a/src/Squidex/app/shared/components/queries/filter-logical.component.ts +++ b/src/Squidex/app/shared/components/queries/filter-logical.component.ts @@ -94,7 +94,11 @@ export class FilterLogicalComponent { } } - private emitChange() { + public emitRemove() { + this.remove.emit(); + } + + public emitChange() { this.change.emit(); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index f01968ff5..0af51cbeb 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -181,6 +181,33 @@ describe('SchemasService', () => { expect(schema!).toEqual(createSchemaDetails(12)); })); + it('should make put request to synchronize schema', + inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { + + const dto = {}; + + const resource: Resource = { + _links: { + ['update/sync']: { method: 'PUT', href: '/api/apps/my-app/schemas/my-schema/sync' } + } + }; + + let schema: SchemaDetailsDto; + + schemasService.putSchemaSync('my-app', resource, dto, version).subscribe(result => { + schema = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/sync'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toBe(version.value); + + req.flush(schemaDetailsResponse(12)); + + expect(schema!).toEqual(createSchemaDetails(12)); + })); + it('should make put request to update category', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index cf4b819ce..6726d945d 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -41,6 +41,7 @@ export class SchemaDto { public readonly canOrderFields: boolean; public readonly canPublish: boolean; public readonly canReadContents: boolean; + public readonly canSynchronize: boolean; public readonly canUnpublish: boolean; public readonly canUpdate: boolean; public readonly canUpdateCategory: boolean; @@ -69,6 +70,7 @@ export class SchemaDto { this.canOrderFields = hasAnyLink(links, 'fields/order'); this.canPublish = hasAnyLink(links, 'publish'); this.canReadContents = hasAnyLink(links, 'contents'); + this.canSynchronize = hasAnyLink(this, 'update/sync'); this.canUnpublish = hasAnyLink(links, 'unpublish'); this.canUpdate = hasAnyLink(links, 'update'); this.canUpdateCategory = hasAnyLink(links, 'update/category'); @@ -125,7 +127,7 @@ export class SchemaDetailsDto extends SchemaDto { const clone = {}; for (const key in source) { - if (source.hasOwnProperty(key) && exclude.indexOf(key) < 0) { + if (source.hasOwnProperty(key) && exclude.indexOf(key) < 0 && key.indexOf('can') !== 0) { const value = source[key]; if (value) { @@ -139,7 +141,7 @@ export class SchemaDetailsDto extends SchemaDto { const result: any = { fields: this.fields.map(field => { - const copy = cleanup(field, 'fieldId'); + const copy = cleanup(field, 'fieldId', '_links'); copy.properties = cleanup(field.properties); @@ -280,6 +282,11 @@ export interface UpdateFieldDto { readonly properties: FieldPropertiesDto; } +export interface SynchronizeSchemaDto { + noFieldDeletiong?: boolean; + noFieldRecreation?: boolean; +} + export interface UpdateSchemaDto { readonly label?: string; readonly hints?: string; @@ -342,6 +349,21 @@ export class SchemasService { pretifyError('Failed to update schema scripts. Please reload.')); } + public putSchemaSync(appName: string, resource: Resource, dto: SynchronizeSchemaDto & any, version: Version): Observable { + const link = resource._links['update/sync']; + + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + map(({ payload }) => { + return parseSchemaWithDetails(payload.body); + }), + tap(() => { + this.analytics.trackEvent('Schema', 'Updated', appName); + }), + pretifyError('Failed to synchronize schema. Please reload.')); + } + public putSchema(appName: string, resource: Resource, dto: UpdateSchemaDto, version: Version): Observable { const link = resource._links['update']; diff --git a/src/Squidex/app/shared/state/schemas.forms.ts b/src/Squidex/app/shared/state/schemas.forms.ts index 5442beb61..85e837918 100644 --- a/src/Squidex/app/shared/state/schemas.forms.ts +++ b/src/Squidex/app/shared/state/schemas.forms.ts @@ -65,6 +65,16 @@ export class AddPreviewUrlForm extends Form { + constructor(formBuilder: FormBuilder) { + super(formBuilder.group({ + json: {}, + fieldsDelete: false, + fieldsRecreate: false + })); + } +} + export class ConfigurePreviewUrlsForm extends Form { constructor( private readonly formBuilder: FormBuilder diff --git a/src/Squidex/app/shared/state/schemas.state.spec.ts b/src/Squidex/app/shared/state/schemas.state.spec.ts index 9607e8372..8529366d9 100644 --- a/src/Squidex/app/shared/state/schemas.state.spec.ts +++ b/src/Squidex/app/shared/state/schemas.state.spec.ts @@ -302,6 +302,22 @@ describe('SchemasState', () => { expect(schemasState.snapshot.selectedSchema).toEqual(updated); }); + it('should update schema and selected schema when schema synced', () => { + const request = {}; + + const updated = createSchemaDetails(1, '_new'); + + schemasService.setup(x => x.putSchemaSync(app, schema1, It.isAny(), version)) + .returns(() => of(updated)).verifiable(); + + schemasState.synchronize(schema1, request).subscribe(); + + const schema1New = schemasState.snapshot.schemas.at(0); + + expect(schema1New).toEqual(updated); + expect(schemasState.snapshot.selectedSchema).toEqual(updated); + }); + it('should update schema and selected schema when scripts configured', () => { const request = { query: '' }; diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/src/Squidex/app/shared/state/schemas.state.ts index e921befe6..0da3fad96 100644 --- a/src/Squidex/app/shared/state/schemas.state.ts +++ b/src/Squidex/app/shared/state/schemas.state.ts @@ -219,6 +219,14 @@ export class SchemasState extends State { shareSubscribed(this.dialogs)); } + public synchronize(schema: SchemaDto, request: {}): Observable { + return this.schemasService.putSchemaSync(this.appName, schema, request, schema.version).pipe( + tap(updated => { + this.replaceSchema(updated); + }), + shareSubscribed(this.dialogs)); + } + public update(schema: SchemaDto, request: UpdateSchemaDto): Observable { return this.schemasService.putSchema(this.appName, schema, request, schema.version).pipe( tap(updated => {