Browse Source

Feature/sync schema (#417)

* Sync schemas in UI
pull/421/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
0997858fc1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Squidex/Config/Web/WebServices.cs
  2. 4
      src/Squidex/app/features/content/pages/content/content-field.component.html
  3. 4
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  4. 2
      src/Squidex/app/features/content/shared/content.component.html
  5. 6
      src/Squidex/app/features/content/shared/content.component.ts
  6. 40
      src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html
  7. 6
      src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss
  8. 42
      src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts
  9. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  10. 8
      src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html
  11. 16
      src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts
  12. 2
      src/Squidex/app/framework/angular/forms/json-editor.component.scss
  13. 2
      src/Squidex/app/framework/angular/panel.component.html
  14. 4
      src/Squidex/app/framework/angular/panel.component.ts
  15. 4
      src/Squidex/app/framework/services/dialog.service.spec.ts
  16. 2
      src/Squidex/app/framework/services/dialog.service.ts
  17. 2
      src/Squidex/app/shared/components/queries/filter-comparison.component.html
  18. 6
      src/Squidex/app/shared/components/queries/filter-comparison.component.ts
  19. 4
      src/Squidex/app/shared/components/queries/filter-logical.component.html
  20. 6
      src/Squidex/app/shared/components/queries/filter-logical.component.ts
  21. 27
      src/Squidex/app/shared/services/schemas.service.spec.ts
  22. 26
      src/Squidex/app/shared/services/schemas.service.ts
  23. 10
      src/Squidex/app/shared/state/schemas.forms.ts
  24. 16
      src/Squidex/app/shared/state/schemas.state.spec.ts
  25. 8
      src/Squidex/app/shared/state/schemas.state.ts

6
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<ApiPermissionUnifier>()
.AsOptional<IClaimsTransformation>();
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
services.AddMvc(options =>
{
options.Filters.Add<ETagFilter>();

4
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -9,7 +9,7 @@
<sqx-field-languages
[field]="field"
[language]="language"
(languageChange)="languageChange.emit($event)"
(languageChange)="emitLanguageChange($event)"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">
@ -67,7 +67,7 @@
<sqx-field-languages
[field]="field"
[language]="language"
(languageChange)="languageChange.emit($event)"
(languageChange)="emitLanguageChange($event)"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">

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

2
src/Squidex/app/features/content/shared/content.component.html

@ -4,7 +4,7 @@
<input type="checkbox" class="form-check"
[disabled]="!selectable"
[ngModel]="selected || !selectable"
(ngModelChange)="selectedChange.emit($event)" />
(ngModelChange)="emitSelectedChange($event)" />
</ng-container>
<ng-template #referenceTemplate>

6
src/Squidex/app/features/content/shared/content.component.ts

@ -42,7 +42,7 @@ export class ContentComponent implements OnChanges {
public statusChange = new EventEmitter<string>();
@Output()
public selectedChange = new EventEmitter();
public selectedChange = new EventEmitter<boolean>();
@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();
}

40
src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html

@ -1,9 +1,31 @@
<sqx-modal-dialog (close)="emitComplete()" large="true">
<ng-container title>
Export Schema
</ng-container>
<ng-container content>
<sqx-json-editor disabled [ngModel]="export"></sqx-json-editor>
</ng-container>
</sqx-modal-dialog>
<form [formGroup]="synchronizeForm.form" (submit)="synchronizeSchema()">
<sqx-modal-dialog (close)="emitComplete()" large="true">
<ng-container title>
Export Schema
</ng-container>
<ng-container content>
<sqx-json-editor formControlName="json"></sqx-json-editor>
</ng-container>
<ng-container footer *ngIf="schema.canSynchronize">
<div class="float-left form-inline">
<div class="form-check pr-4">
<input class="form-check-input" type="checkbox" id="fieldsDelete" formControlName="fieldsDelete" />
<label class="form-check-label" for="fieldsDelete">
Delete fields
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldsRecreate" formControlName="fieldsRecreate" />
<label class="form-check-label" for="fieldsRecreate">
Recreate fields
</label>
</div>
</div>
<button type="submit" class="float-right btn btn-success" [disabled]="synchronizeForm.submitted | async">Synchronize</button>
</ng-container>
</sqx-modal-dialog>
</form>

6
src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss

@ -1,2 +1,6 @@
@import '_vars';
@import '_mixins';
@import '_mixins';
.json {
min-height: 500px;
}

42
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() {

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

@ -7,7 +7,7 @@
<ng-container menu>
<button type="button" class="btn btn-text mr-1" (click)="exportDialog.show()">
JSON Preview
JSON View
</button>
<div class="btn-group mr-1" #buttonPublish>

8
src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html

@ -1,7 +1,7 @@
<div class="step">
<div class="row no-gutters step-header">
<div class="col-auto">
<button class="btn btn-initial mr-1" (click)="makeInitial.emit()"
<button class="btn btn-initial mr-1" (click)="emitMakeInitial()"
[class.enabled]="step.name !== workflow.initial && !step.isLocked"
[class.active]="step.name === workflow.initial"
[disabled]="step.name === workflow.initial || step.isLocked || disabled">
@ -29,7 +29,7 @@
<small class="text-decent">(Cannot be removed)</small>
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-danger" (click)="remove.emit()" *ngIf="!step.isLocked && workflow.steps.length > 2" [disabled]="disabled">
<button type="button" class="btn btn-text-danger" (click)="emitRemove()" *ngIf="!step.isLocked && workflow.steps.length > 2" [disabled]="disabled">
<i class="icon-bin2"></i>
</button>
</div>
@ -40,7 +40,7 @@
[transition]="transition"
[disabled]="disabled"
[roles]="roles"
(remove)="transitionRemove.emit(transition)"
(remove)="emitTransitionRemove(transition)"
(update)="changeTransition(transition, $event)">
</sqx-workflow-transition>
@ -56,7 +56,7 @@
</sqx-dropdown>
</div>
<div class="col pl-2">
<button class="btn btn-outline-secondary" (click)="transitionAdd.emit(openStep)">
<button class="btn btn-outline-secondary" (click)="emitTransitionAdd(openStep)">
<i class="icon-plus"></i>
</button>
</div>

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

2
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 {

2
src/Squidex/app/framework/angular/panel.component.html

@ -18,7 +18,7 @@
<ng-container *ngIf="showClose">
<ng-container *ngIf="customClose; else defaultClose">
<a class="panel-close" (click)="close.emit()">
<a class="panel-close" (click)="emitClose()">
<i class="icon-close"></i>
</a>
</ng-container>

4
src/Squidex/app/framework/angular/panel.component.ts

@ -124,4 +124,8 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
}
}
}
public emitClose() {
this.close.emit();
}
}

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

2
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
) {
}

2
src/Squidex/app/shared/components/queries/filter-comparison.component.html

@ -60,7 +60,7 @@
</div>
</div>
<div class="col-auto pl-2">
<button type="button" class="btn btn-text-danger" (click)="remove.emit()">
<button type="button" class="btn btn-text-danger" (click)="emitRemove()">
<i class="icon-bin2"></i>
</button>
</div>

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

4
src/Squidex/app/shared/components/queries/filter-logical.component.html

@ -13,7 +13,7 @@
<div class="col-auto pl-2" *ngIf="!isRoot">
<button type="button" class="btn btn-text-danger" (click)="remove.emit()">
<button type="button" class="btn btn-text-danger" (click)="emitRemove()">
<i class="icon-bin2"></i>
</button>
</div>
@ -26,7 +26,7 @@
<span class="filter-line-h"></span>
<sqx-filter-node [filter]="filter" [model]="model" [level]="level + 1"
(remove)="removeFilter(filter)" (change)="change.emit()">
(remove)="removeFilter(filter)" (change)="emitChange()">
</sqx-filter-node>
</div>

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

27
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) => {

26
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<SchemaDetailsDto> {
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<SchemaDetailsDto> {
const link = resource._links['update'];

10
src/Squidex/app/shared/state/schemas.forms.ts

@ -65,6 +65,16 @@ export class AddPreviewUrlForm extends Form<FormGroup, { name: string, url: stri
}
}
export class SynchronizeSchemaForm extends Form<FormGroup, { json: any, fieldsDelete: boolean, fieldsRecreate: boolean }> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
json: {},
fieldsDelete: false,
fieldsRecreate: false
}));
}
}
export class ConfigurePreviewUrlsForm extends Form<FormArray, { [name: string]: string }> {
constructor(
private readonly formBuilder: FormBuilder

16
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 = <SchemaDetailsDto>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: '<query-script>' };

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

@ -219,6 +219,14 @@ export class SchemasState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
public synchronize(schema: SchemaDto, request: {}): Observable<SchemaDetailsDto> {
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<SchemaDetailsDto> {
return this.schemasService.putSchema(this.appName, schema, request, schema.version).pipe(
tap(updated => {

Loading…
Cancel
Save