Browse Source

Merge branch 'master' of github.com:Squidex/squidex

pull/421/head
Sebastian 6 years ago
parent
commit
efd890697c
  1. 5
      src/Squidex.Infrastructure/Migrations/Migrator.cs
  2. 1
      src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs
  3. 2
      src/Squidex/Config/Startup/MigratorHost.cs
  4. 6
      src/Squidex/Config/Web/WebServices.cs
  5. 4
      src/Squidex/app/features/content/pages/content/content-field.component.html
  6. 4
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  7. 2
      src/Squidex/app/features/content/shared/content.component.html
  8. 6
      src/Squidex/app/features/content/shared/content.component.ts
  9. 2
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts
  10. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  11. 24
      src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html
  12. 4
      src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss
  13. 42
      src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts
  14. 2
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  15. 2
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html
  16. 8
      src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html
  17. 16
      src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts
  18. 2
      src/Squidex/app/framework/angular/forms/json-editor.component.scss
  19. 2
      src/Squidex/app/framework/angular/panel.component.html
  20. 4
      src/Squidex/app/framework/angular/panel.component.ts
  21. 4
      src/Squidex/app/framework/services/dialog.service.spec.ts
  22. 2
      src/Squidex/app/framework/services/dialog.service.ts
  23. 2
      src/Squidex/app/shared/components/queries/filter-comparison.component.html
  24. 6
      src/Squidex/app/shared/components/queries/filter-comparison.component.ts
  25. 4
      src/Squidex/app/shared/components/queries/filter-logical.component.html
  26. 6
      src/Squidex/app/shared/components/queries/filter-logical.component.ts
  27. 27
      src/Squidex/app/shared/services/schemas.service.spec.ts
  28. 32
      src/Squidex/app/shared/services/schemas.service.ts
  29. 179
      src/Squidex/app/shared/services/schemas.types.ts
  30. 86
      src/Squidex/app/shared/state/contents.forms.spec.ts
  31. 10
      src/Squidex/app/shared/state/schemas.forms.ts
  32. 16
      src/Squidex/app/shared/state/schemas.state.spec.ts
  33. 8
      src/Squidex/app/shared/state/schemas.state.ts
  34. 4
      tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs

5
src/Squidex.Infrastructure/Migrations/Migrator.cs

@ -7,6 +7,7 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
@ -32,7 +33,7 @@ namespace Squidex.Infrastructure.Migrations
this.log = log;
}
public async Task MigrateAsync()
public async Task MigrateAsync(CancellationToken ct = default)
{
var version = 0;
@ -49,7 +50,7 @@ namespace Squidex.Infrastructure.Migrations
version = await migrationStatus.GetVersionAsync();
while (true)
while (!ct.IsCancellationRequested)
{
var (newVersion, migrations) = migrationPath.GetNext(version);

1
src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs

@ -28,6 +28,7 @@ namespace Squidex.Infrastructure.Orleans
{
for (var i = 1; i <= NumTries; i++)
{
ct.ThrowIfCancellationRequested();
try
{
var grain = grainFactory.GetGrain<T>(SingleGrain.Id);

2
src/Squidex/Config/Startup/MigratorHost.cs

@ -25,7 +25,7 @@ namespace Squidex.Config.Startup
protected override Task StartAsync(ISemanticLog log, CancellationToken ct)
{
return migrator.MigrateAsync();
return migrator.MigrateAsync(ct);
}
}
}

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

2
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts

@ -94,7 +94,7 @@ export class FieldWizardComponent implements OnInit {
const value = this.editForm.submit();
if (value) {
const properties = createProperties(this.field.properties['fieldType'], value);
const properties = createProperties(this.field.properties.fieldType, value);
this.schemasState.updateField(this.schema, this.field as RootFieldDto, { properties })
.subscribe(() => {

2
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -115,7 +115,7 @@ export class FieldComponent implements OnChanges {
const value = this.editForm.submit();
if (value) {
const properties = createProperties(this.field.properties['fieldType'], value);
const properties = createProperties(this.field.properties.fieldType, value);
this.schemasState.updateField(this.schema, this.field, { properties })
.subscribe(() => {

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

@ -1,9 +1,31 @@
<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 disabled [ngModel]="export"></sqx-json-editor>
<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>

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

@ -1,2 +1,6 @@
@import '_vars';
@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>

2
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html

@ -1,5 +1,5 @@
<div [formGroup]="editForm">
<div class="form-group row" *ngIf="showUnique">
<div class="form-group row" [class.hidden]="!showUnique">
<div class="col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldUnique" formControlName="isUnique" />

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

32
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);
@ -222,12 +224,8 @@ export class RootFieldDto extends FieldDto {
return this.properties.fieldType === 'Array';
}
public get isString() {
return this.properties.fieldType === 'String';
}
public get isTranslatable() {
return this.isLocalizable && this.isString && (this.properties.editor === 'Input' || this.properties.editor === 'Textarea');
return this.isLocalizable && this.properties.isTranslateable;
}
constructor(links: ResourceLinks, fieldId: number, name: string, properties: FieldPropertiesDto,
@ -284,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;
@ -346,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'];

179
src/Squidex/app/shared/services/schemas.types.ts

@ -62,42 +62,46 @@ export function createProperties(fieldType: FieldType, values?: any): FieldPrope
switch (fieldType) {
case 'Array':
properties = new ArrayFieldPropertiesDto(values);
properties = new ArrayFieldPropertiesDto();
break;
case 'Assets':
properties = new AssetsFieldPropertiesDto(values);
properties = new AssetsFieldPropertiesDto();
break;
case 'Boolean':
properties = new BooleanFieldPropertiesDto('Checkbox', values);
properties = new BooleanFieldPropertiesDto();
break;
case 'DateTime':
properties = new DateTimeFieldPropertiesDto('DateTime', values);
properties = new DateTimeFieldPropertiesDto();
break;
case 'Geolocation':
properties = new GeolocationFieldPropertiesDto(values);
properties = new GeolocationFieldPropertiesDto();
break;
case 'Json':
properties = new JsonFieldPropertiesDto(values);
properties = new JsonFieldPropertiesDto();
break;
case 'Number':
properties = new NumberFieldPropertiesDto('Input', values);
properties = new NumberFieldPropertiesDto();
break;
case 'References':
properties = new ReferencesFieldPropertiesDto('List', values);
properties = new ReferencesFieldPropertiesDto();
break;
case 'String':
properties = new StringFieldPropertiesDto('Input', values);
properties = new StringFieldPropertiesDto();
break;
case 'Tags':
properties = new TagsFieldPropertiesDto(values);
properties = new TagsFieldPropertiesDto();
break;
case 'UI':
properties = new UIFieldPropertiesDto(values);
properties = new UIFieldPropertiesDto();
break;
default:
throw 'Invalid properties type';
}
if (values) {
Object.assign(properties, values);
}
return properties;
}
@ -129,19 +133,15 @@ export abstract class FieldPropertiesDto {
public abstract fieldType: FieldType;
public readonly editorUrl?: string;
public readonly label?: string;
public readonly hints?: string;
public readonly placeholder?: string;
public readonly isRequired: boolean = false;
public readonly isListField: boolean = false;
public readonly isReferenceField: boolean = false;
public readonly isRequired: boolean = false;
public readonly label?: string;
public readonly placeholder?: string;
constructor(public readonly editor: string,
props?: Partial<FieldPropertiesDto>
) {
if (props) {
Object.assign(this, props);
}
public get isTranslateable() {
return false;
}
public get isComplexUI() {
@ -162,14 +162,8 @@ export abstract class FieldPropertiesDto {
export class ArrayFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Array';
public readonly minItems?: number;
public readonly maxItems?: number;
constructor(
props?: Partial<ArrayFieldPropertiesDto>
) {
super('Default', props);
}
public readonly minItems?: number;
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitArray(this);
@ -179,92 +173,78 @@ export class ArrayFieldPropertiesDto extends FieldPropertiesDto {
export class AssetsFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Assets';
public readonly minItems?: number;
public readonly allowDuplicates?: boolean;
public readonly allowedExtensions?: string[];
public readonly aspectHeight?: number;
public readonly aspectWidth?: number;
public readonly maxHeight?: number;
public readonly maxItems?: number;
public readonly minSize?: number;
public readonly maxSize?: number;
public readonly allowedExtensions?: string[];
public readonly mustBeImage?: boolean;
public readonly minWidth?: number;
public readonly maxWidth?: number;
public readonly minHeight?: number;
public readonly maxHeight?: number;
public readonly aspectWidth?: number;
public readonly aspectHeight?: number;
public readonly allowDuplicates?: boolean;
public readonly minItems?: number;
public readonly minSize?: number;
public readonly minWidth?: number;
public readonly mustBeImage?: boolean;
public get isSortable() {
return false;
}
constructor(
props?: Partial<AssetsFieldPropertiesDto>
) {
super('Default', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitAssets(this);
}
}
export type BooleanFieldEditor = 'Checkbox' | 'Toggle';
export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Boolean';
public readonly inlineEditable: boolean = false;
public readonly defaultValue?: boolean;
public readonly editor: BooleanFieldEditor = 'Checkbox';
public readonly inlineEditable: boolean = false;
public get isComplexUI() {
return false;
}
constructor(editor: string,
props?: Partial<BooleanFieldPropertiesDto>
) {
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitBoolean(this);
}
}
export type DateTimeFieldEditor = 'DateTime' | 'Date';
export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'DateTime';
public readonly calculatedDefaultValue?: string;
public readonly defaultValue?: string;
public readonly editor: DateTimeFieldEditor = 'DateTime';
public readonly maxValue?: string;
public readonly minValue?: string;
public readonly calculatedDefaultValue?: string;
public get isComplexUI() {
return false;
}
constructor(editor: string,
props?: Partial<DateTimeFieldPropertiesDto>
) {
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitDateTime(this);
}
}
export type GeolocationFieldEditor = 'Map';
export class GeolocationFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Geolocation';
public readonly editor: GeolocationFieldEditor = 'Map';
public get isSortable() {
return false;
}
constructor(
props?: Partial<GeolocationFieldPropertiesDto>
) {
super('Map', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitGeolocation(this);
}
@ -277,87 +257,75 @@ export class JsonFieldPropertiesDto extends FieldPropertiesDto {
return false;
}
constructor(
props?: Partial<JsonFieldPropertiesDto>
) {
super('Default', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitJson(this);
}
}
export type NumberFieldEditor = 'Input' | 'Radio' | 'Dropdown' | 'Stars';
export class NumberFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Number';
public readonly allowedValues?: number[];
public readonly defaultValue?: number;
public readonly editor: NumberFieldEditor = 'Input';
public readonly inlineEditable: boolean = false;
public readonly isUnique: boolean = false;
public readonly defaultValue?: number;
public readonly maxValue?: number;
public readonly minValue?: number;
public readonly allowedValues?: number[];
public get isComplexUI() {
return false;
}
constructor(editor: string,
props?: Partial<NumberFieldPropertiesDto>
) {
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitNumber(this);
}
}
export type ReferencesFieldEditor = 'List' | 'Dropdown';
export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'References';
public readonly minItems?: number;
public readonly allowDuplicates?: boolean;
public readonly editor: ReferencesFieldEditor = 'List';
public readonly maxItems?: number;
public readonly editor: string;
public readonly schemaId?: string;
public readonly minItems?: number;
public readonly resolveReference?: boolean;
public readonly allowDuplicates?: boolean;
public readonly schemaId?: string;
public get isSortable() {
return false;
}
constructor(editor: string,
props?: Partial<ReferencesFieldPropertiesDto>
) {
super(editor, props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitReferences(this);
}
}
export type StringEditor = 'Color' | 'Dropdown' | 'Html' | 'Input' | 'Markdown' | 'Radio' | 'RichText' | 'Slug' | 'TextArea';
export class StringFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'String';
public readonly inlineEditable = false;
public readonly isUnique: boolean = false;
public readonly allowedValues?: string[];
public readonly defaultValue?: string;
public readonly editor: StringEditor = 'Input';
public readonly inlineEditable: boolean = false;
public readonly isUnique: boolean = false;
public readonly maxLength?: number;
public readonly minLength?: number;
public readonly pattern?: string;
public readonly patternMessage?: string;
public readonly minLength?: number;
public readonly maxLength?: number;
public readonly allowedValues?: string[];
public get isComplexUI() {
return this.editor !== 'Input' && this.editor !== 'Color' && this.editor !== 'Radio' && this.editor !== 'Slug' && this.editor !== 'TextArea';
}
constructor(editor: string,
props?: Partial<StringFieldPropertiesDto>
) {
super(editor, props);
public get isTranslateable() {
return this.editor === 'Input' || this.editor === 'TextArea';
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
@ -365,12 +333,15 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto {
}
}
export type TagsFieldEditor = 'Tags' | 'Checkboxes' | 'Dropdown';
export class TagsFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Tags';
public readonly minItems?: number;
public readonly maxItems?: number;
public readonly allowedValues?: string[];
public readonly editor: TagsFieldEditor = 'Tags';
public readonly maxItems?: number;
public readonly minItems?: number;
public get isComplexUI() {
return false;
@ -380,12 +351,6 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto {
return false;
}
constructor(
props?: Partial<TagsFieldPropertiesDto>
) {
super('Tags', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitTags(this);
}
@ -394,6 +359,8 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto {
export class UIFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'UI';
public readonly editor = 'Separator';
public get isComplexUI() {
return false;
}
@ -406,12 +373,6 @@ export class UIFieldPropertiesDto extends FieldPropertiesDto {
return false;
}
constructor(
props?: Partial<TagsFieldPropertiesDto>
) {
super('Separator', props);
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitUI(this);
}

86
src/Squidex/app/shared/state/contents.forms.spec.ts

@ -9,32 +9,22 @@ import { AbstractControl, FormArray } from '@angular/forms';
import {
AppLanguageDto,
ArrayFieldPropertiesDto,
AssetsFieldPropertiesDto,
BooleanFieldPropertiesDto,
createProperties,
DateTime,
DateTimeFieldPropertiesDto,
EditContentForm,
FieldDefaultValue,
FieldFormatter,
FieldPropertiesDto,
FieldsValidators,
GeolocationFieldPropertiesDto,
getContentValue,
HtmlValue,
ImmutableArray,
JsonFieldPropertiesDto,
LanguageDto,
NestedFieldDto,
NumberFieldPropertiesDto,
PartitionConfig,
ReferencesFieldPropertiesDto,
RootFieldDto,
SchemaDetailsDto,
SchemaPropertiesDto,
StringFieldPropertiesDto,
TagsFieldPropertiesDto,
Version
} from '@app/shared/internal';
@ -67,9 +57,9 @@ describe('SchemaDetailsDto', () => {
});
it('should return configured fields as list fields if no schema field are declared', () => {
const field1 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: true }) });
const field2 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: false }), id: 2 });
const field3 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: true }), id: 3 });
const field1 = createField({ properties: createProperties('Array', { isListField: true }) });
const field2 = createField({ properties: createProperties('Array', { isListField: false }), id: 2 });
const field3 = createField({ properties: createProperties('Array', { isListField: true }), id: 3 });
const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] });
@ -77,9 +67,9 @@ describe('SchemaDetailsDto', () => {
});
it('should return first fields as list fields if no schema field is declared', () => {
const field1 = createField({ properties: new ArrayFieldPropertiesDto() });
const field2 = createField({ properties: new ArrayFieldPropertiesDto(), id: 2 });
const field3 = createField({ properties: new ArrayFieldPropertiesDto(), id: 3 });
const field1 = createField({ properties: createProperties('Array') });
const field2 = createField({ properties: createProperties('Array'), id: 2 });
const field3 = createField({ properties: createProperties('Array'), id: 3 });
const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] });
@ -95,50 +85,50 @@ describe('SchemaDetailsDto', () => {
describe('FieldDto', () => {
it('should return label as display name', () => {
const field = createField({ properties: new AssetsFieldPropertiesDto({ label: 'Label' }) });
const field = createField({ properties: createProperties('Array', { label: 'Label' }) });
expect(field.displayName).toBe('Label');
});
it('should return name as display name if label is null', () => {
const field = createField({ properties: new AssetsFieldPropertiesDto() });
const field = createField({ properties: createProperties('Assets') });
expect(field.displayName).toBe('field1');
});
it('should return name as display name label is empty', () => {
const field = createField({ properties: new AssetsFieldPropertiesDto({ label: '' }) });
const field = createField({ properties: createProperties('Assets', { label: '' }) });
expect(field.displayName).toBe('field1');
});
it('should return placeholder as display placeholder', () => {
const field = createField({ properties: new AssetsFieldPropertiesDto({ placeholder: 'Placeholder' }) });
const field = createField({ properties: createProperties('Assets', { placeholder: 'Placeholder' }) });
expect(field.displayPlaceholder).toBe('Placeholder');
});
it('should return empty as display placeholder if placeholder is null', () => {
const field = createField({ properties: new AssetsFieldPropertiesDto() });
const field = createField({ properties: createProperties('Assets') });
expect(field.displayPlaceholder).toBe('');
});
it('should return localizable if partitioning is language', () => {
const field = createField({ properties: new AssetsFieldPropertiesDto(), partitioning: 'language' });
const field = createField({ properties: createProperties('Assets'), partitioning: 'language' });
expect(field.isLocalizable).toBeTruthy();
});
it('should not return localizable if partitioning is invarient', () => {
const field = createField({ properties: new AssetsFieldPropertiesDto(), partitioning: 'invariant' });
const field = createField({ properties: createProperties('Assets'), partitioning: 'invariant' });
expect(field.isLocalizable).toBeFalsy();
});
});
describe('ArrayField', () => {
const field = createField({ properties: new ArrayFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) });
const field = createField({ properties: createProperties('Array', { isRequired: true, minItems: 1, maxItems: 5 }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(2);
@ -162,7 +152,7 @@ describe('ArrayField', () => {
});
describe('AssetsField', () => {
const field = createField({ properties: new AssetsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) });
const field = createField({ properties: createProperties('Assets', { isRequired: true, minItems: 1, maxItems: 5 }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(3);
@ -186,7 +176,7 @@ describe('AssetsField', () => {
});
describe('TagsField', () => {
const field = createField({ properties: new TagsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) });
const field = createField({ properties: createProperties('Tags', { isRequired: true, minItems: 1, maxItems: 5 }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(2);
@ -210,7 +200,7 @@ describe('TagsField', () => {
});
describe('BooleanField', () => {
const field = createField({ properties: new BooleanFieldPropertiesDto('Checkbox', { isRequired: true }) });
const field = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', isRequired: true }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(1);
@ -229,7 +219,7 @@ describe('BooleanField', () => {
});
it('should return default value for default properties', () => {
const field2 = createField({ properties: new BooleanFieldPropertiesDto('Checkbox', { defaultValue: true }) });
const field2 = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', defaultValue: true }) });
expect(FieldDefaultValue.get(field2)).toBeTruthy();
});
@ -237,7 +227,7 @@ describe('BooleanField', () => {
describe('DateTimeField', () => {
const now = DateTime.parseISO_UTC('2017-10-12T16:30:10Z');
const field = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { isRequired: true }) });
const field = createField({ properties: createProperties('DateTime', { editor: 'DateTime', isRequired: true }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(1);
@ -252,38 +242,38 @@ describe('DateTimeField', () => {
});
it('should format to date', () => {
const dateField = createField({ properties: new DateTimeFieldPropertiesDto('Date') });
const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) });
expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('2017-12-12');
});
it('should format to date time', () => {
const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime') });
const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime' }) });
expect(FieldFormatter.format(field2, '2017-12-12T16:00:00Z')).toBe('2017-12-12 16:00:00');
});
it('should return default for DateFieldProperties', () => {
const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { defaultValue: '2017-10-12T16:00:00Z' }) });
const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', defaultValue: '2017-10-12T16:00:00Z' }) });
expect(FieldDefaultValue.get(field2)).toEqual('2017-10-12T16:00:00Z');
});
it('should return calculated date when Today for DateFieldProperties', () => {
const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { calculatedDefaultValue: 'Today' }) });
const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Today' }) });
expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12T00:00:00Z');
});
it('should return calculated date when Now for DateFieldProperties', () => {
const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { calculatedDefaultValue: 'Now' }) });
const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Now' }) });
expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12T16:30:10Z');
});
});
describe('GeolocationField', () => {
const field = createField({ properties: new GeolocationFieldPropertiesDto({ isRequired: true }) });
const field = createField({ properties: createProperties('Geolocation', { isRequired: true }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(1);
@ -303,7 +293,7 @@ describe('GeolocationField', () => {
});
describe('JsonField', () => {
const field = createField({ properties: new JsonFieldPropertiesDto({ isRequired: true }) });
const field = createField({ properties: createProperties('Json', { isRequired: true }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(1);
@ -323,7 +313,7 @@ describe('JsonField', () => {
});
describe('NumberField', () => {
const field = createField({ properties: new NumberFieldPropertiesDto('Input', { isRequired: true, minValue: 1, maxValue: 6, allowedValues: [1, 3] }) });
const field = createField({ properties: createProperties('Number', { isRequired: true, minValue: 1, maxValue: 6, allowedValues: [1, 3] }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(3);
@ -338,44 +328,44 @@ describe('NumberField', () => {
});
it('should format to stars if html allowed', () => {
const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) });
expect(FieldFormatter.format(field2, 3)).toEqual(new HtmlValue('&#9733; &#9733; &#9733; '));
});
it('should format to short star view for many stars', () => {
const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) });
expect(FieldFormatter.format(field2, 42)).toEqual(new HtmlValue('&#9733; 42'));
});
it('should format to short star view for no stars', () => {
const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) });
expect(FieldFormatter.format(field2, 0)).toEqual(new HtmlValue('&#9733; 0'));
});
it('should format to short star view for negative stars', () => {
const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) });
expect(FieldFormatter.format(field2, -13)).toEqual(new HtmlValue('&#9733; -13'));
});
it('should not format to stars if html not allowed', () => {
const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') });
const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) });
expect(FieldFormatter.format(field2, 3, false)).toEqual('3');
});
it('should return default value for default properties', () => {
const field2 = createField({ properties: new NumberFieldPropertiesDto('Input', { defaultValue: 13 }) });
const field2 = createField({ properties: createProperties('Number', { defaultValue: 13 }) });
expect(FieldDefaultValue.get(field2)).toEqual(13);
});
});
describe('ReferencesField', () => {
const field = createField({ properties: new ReferencesFieldPropertiesDto('List', { isRequired: true, minItems: 1, maxItems: 5 }) });
const field = createField({ properties: createProperties('References', { editor: 'List', isRequired: true, minItems: 1, maxItems: 5 }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(3);
@ -399,7 +389,7 @@ describe('ReferencesField', () => {
});
describe('StringField', () => {
const field = createField({ properties: new StringFieldPropertiesDto('Input', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] }) });
const field = createField({ properties: createProperties('String', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] }) });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(4);
@ -414,7 +404,7 @@ describe('StringField', () => {
});
it('should return default value for default properties', () => {
const field2 = createField({ properties: new StringFieldPropertiesDto('Input', { defaultValue: 'MyDefault' }) });
const field2 = createField({ properties: createProperties('String', { defaultValue: 'MyDefault' }) });
expect(FieldDefaultValue.get(field2)).toEqual('MyDefault');
});
@ -422,8 +412,8 @@ describe('StringField', () => {
describe('GetContentValue', () => {
const language = new LanguageDto('en', 'English');
const fieldInvariant = createField({ properties: new NumberFieldPropertiesDto('Input'), partitioning: 'invariant' });
const fieldLocalized = createField({ properties: new NumberFieldPropertiesDto('Input') });
const fieldInvariant = createField({ properties: createProperties('Number'), partitioning: 'invariant' });
const fieldLocalized = createField({ properties: createProperties('Number') });
it('should resolve invariant field from references value', () => {
const content: any = {

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

4
tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs

@ -108,7 +108,7 @@ namespace Squidex.Infrastructure.Migrations
A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException());
await Assert.ThrowsAsync<MigrationFailedException>(sut.MigrateAsync);
await Assert.ThrowsAsync<MigrationFailedException>(() => sut.MigrateAsync());
A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened();
A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened();
@ -147,7 +147,7 @@ namespace Squidex.Infrastructure.Migrations
var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 };
await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(sut.MigrateAsync)));
await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync())));
A.CallTo(() => migrator_0_1.UpdateAsync())
.MustHaveHappened(1, Times.Exactly);

Loading…
Cancel
Save