Browse Source

Remove asset metadata and improve code editor. (#1004)

* Remove asset metadata and improve code editor.

* Fix tests

* Update resizer

* Update packages again.
pull/1005/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
07c25530f9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  2. 10
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs
  3. 16
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs
  4. 12
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  5. 4
      backend/src/Squidex/Config/Web/WebExtensions.cs
  6. 22
      backend/src/Squidex/Squidex.csproj
  7. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs
  8. 34
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs
  9. 2
      frontend/src/app/features/rules/pages/events/rule-event.component.html
  10. 4
      frontend/src/app/features/rules/shared/actions/formattable-input.component.html
  11. 2
      frontend/src/app/features/rules/shared/actions/formattable-input.component.scss
  12. 149
      frontend/src/app/features/rules/shared/actions/formattable-input.component.ts
  13. 11
      frontend/src/app/framework/angular/forms/editors/code-editor.component.ts
  14. 2
      tools/TestSuite/docker-compose.yml

2
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -28,7 +28,7 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="NJsonSchema" Version="10.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.9.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.11.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

10
backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs

@ -36,9 +36,11 @@ public sealed class ImageAssetMetadataSource : IAssetMetadataSource
if (imageInfo != null)
{
var isSwapped = imageInfo.Orientation > ImageOrientation.TopLeft;
var needsFix =
imageInfo.HasSensitiveMetadata ||
imageInfo.Orientation > ImageOrientation.TopLeft;
if (command.File != null && isSwapped)
if (command.File != null && needsFix)
{
var tempFile = TempAssetFile.Create(command.File);
@ -46,7 +48,7 @@ public sealed class ImageAssetMetadataSource : IAssetMetadataSource
{
await using (var tempStream = tempFile.OpenWrite())
{
await assetThumbnailGenerator.FixOrientationAsync(uploadStream, mimeType, tempStream, ct);
await assetThumbnailGenerator.FixAsync(uploadStream, mimeType, tempStream, ct);
}
}
@ -60,7 +62,7 @@ public sealed class ImageAssetMetadataSource : IAssetMetadataSource
command.File = tempFile;
}
if (command.Type == AssetType.Unknown || isSwapped)
if (command.Type == AssetType.Unknown || needsFix)
{
command.Type = AssetType.Image;

16
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs

@ -48,8 +48,6 @@ internal sealed class Builder
public IInterfaceGraphType ComponentInterface { get; } = new ComponentInterfaceGraphType();
public List<SchemaInfo> Schemas { get; private set; }
public Builder(IAppEntity app, GraphQLOptions options)
{
partitionResolver = app.PartitionResolver();
@ -66,9 +64,9 @@ internal sealed class Builder
allSchemas.AddRange(SchemaInfo.Build(schemas, typeNames).Where(x => x.Fields.Count > 0));
// Only published normal schemas (not components are used for entities).
Schemas = allSchemas.Where(x => x.Schema.SchemaDef.IsPublished && x.Schema.SchemaDef.Type != SchemaType.Component).ToList();
var normalSchemas = allSchemas.Where(x => x.Schema.SchemaDef.IsPublished && x.Schema.SchemaDef.Type != SchemaType.Component).ToList();
foreach (var schemaInfo in Schemas)
foreach (var schemaInfo in normalSchemas)
{
var contentType = new ContentGraphType(schemaInfo);
@ -76,7 +74,7 @@ internal sealed class Builder
contentResultTypes[schemaInfo] = new ContentResultGraphType(contentType, schemaInfo);
}
foreach (var schemaInfo in Schemas)
foreach (var schemaInfo in normalSchemas)
{
var componentType = new ComponentGraphType(schemaInfo);
@ -85,7 +83,7 @@ internal sealed class Builder
var newSchema = new GraphQLSchema
{
Query = new ApplicationQueries(this, Schemas)
Query = new ApplicationQueries(this, normalSchemas)
};
newSchema.RegisterType(ComponentInterface);
@ -94,9 +92,9 @@ internal sealed class Builder
newSchema.Directives.Register(SharedTypes.CacheDirective);
newSchema.Directives.Register(SharedTypes.OptimizeFieldQueriesDirective);
if (Schemas.Any())
if (normalSchemas.Any())
{
var mutations = new ApplicationMutations(this, Schemas);
var mutations = new ApplicationMutations(this, normalSchemas);
if (mutations.Fields.Count > 0)
{
@ -111,7 +109,7 @@ internal sealed class Builder
foreach (var (schemaInfo, contentType) in contentTypes)
{
contentType.Initialize(this, schemaInfo, Schemas);
contentType.Initialize(this, schemaInfo, normalSchemas);
}
foreach (var (schemaInfo, componentType) in componentTypes)

12
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -24,12 +24,12 @@
<PackageReference Include="NodaTime" Version="3.1.9" />
<PackageReference Include="OpenTelemetry.Api" Version="1.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="5.9.0" />
<PackageReference Include="Squidex.Caching" Version="5.9.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="5.9.0" />
<PackageReference Include="Squidex.Log" Version="5.9.0" />
<PackageReference Include="Squidex.Messaging" Version="5.9.0" />
<PackageReference Include="Squidex.Text" Version="5.9.0" />
<PackageReference Include="Squidex.Assets" Version="5.11.0" />
<PackageReference Include="Squidex.Caching" Version="5.11.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="5.11.0" />
<PackageReference Include="Squidex.Log" Version="5.11.0" />
<PackageReference Include="Squidex.Messaging" Version="5.11.0" />
<PackageReference Include="Squidex.Text" Version="5.11.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

4
backend/src/Squidex/Config/Web/WebExtensions.cs

@ -32,7 +32,7 @@ public static class WebExtensions
public static IApplicationBuilder UseSquidexLocalization(this IApplicationBuilder app)
{
var supportedCultures = new[] { "en", "nl", "it", "zh", "pt","fr" };
var supportedCultures = new[] { "en", "nl", "it", "zh", "pt", "fr" };
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
@ -93,7 +93,7 @@ public static class WebExtensions
httpContext.Response.Headers[HeaderNames.ContentType] = "application/json";
return httpContext.Response.WriteAsync(json);
return httpContext.Response.WriteAsync(json, httpContext.RequestAborted);
});
app.UseHealthChecks("/readiness", new HealthCheckOptions

22
backend/src/Squidex/Squidex.csproj

@ -62,18 +62,18 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc7" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.1.22" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.S3" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="5.9.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="5.11.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="5.11.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="5.11.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="5.11.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="5.11.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="5.11.0" />
<PackageReference Include="Squidex.Assets.S3" Version="5.11.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="5.11.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="16.0.0" />
<PackageReference Include="Squidex.Hosting" Version="5.9.0" />
<PackageReference Include="Squidex.Messaging.All" Version="5.9.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.9.0" />
<PackageReference Include="Squidex.Hosting" Version="5.11.0" />
<PackageReference Include="Squidex.Messaging.All" Version="5.11.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="5.11.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup>

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs

@ -51,7 +51,7 @@ public class AppCommandMiddlewareTests : HandlerTestBase<AppDomainObject.State>
var file = new NoopAssetFile();
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, CancellationToken))
.Returns(new ImageInfo(100, 100, ImageOrientation.None, ImageFormat.PNG));
.Returns(new ImageInfo(ImageFormat.PNG, 100, 100, ImageOrientation.None, false));
await HandleAsync(new UploadAppImage { File = file }, None.Value);

34
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs

@ -43,6 +43,9 @@ public class ImageAssetMetadataSourceTests : GivenContext
{
var command = new CreateAsset { File = file };
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken))
.Returns(Task.FromResult<ImageInfo?>(null));
await sut.EnhanceAsync(command, CancellationToken);
Assert.Empty(command.Tags);
@ -54,7 +57,7 @@ public class ImageAssetMetadataSourceTests : GivenContext
var command = new CreateAsset { File = file };
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken))
.Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG));
.Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false));
await sut.EnhanceAsync(command, CancellationToken);
@ -62,7 +65,7 @@ public class ImageAssetMetadataSourceTests : GivenContext
Assert.Equal(600, command.Metadata.GetPixelHeight());
Assert.Equal(AssetType.Image, command.Type);
A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A<Stream>._, A<CancellationToken>._))
A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A<Stream>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
@ -72,10 +75,31 @@ public class ImageAssetMetadataSourceTests : GivenContext
var command = new CreateAsset { File = file };
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, CancellationToken))
.Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG));
.Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false));
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken))
.Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.BottomRight, false)).Once();
await sut.EnhanceAsync(command, CancellationToken);
Assert.Equal(800, command.Metadata.GetPixelWidth());
Assert.Equal(600, command.Metadata.GetPixelHeight());
Assert.Equal(AssetType.Image, command.Type);
A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A<Stream>._, CancellationToken))
.MustHaveHappened();
}
[Fact]
public async Task Should_fix_image_if_it_contains_sensitive_metadata()
{
var command = new CreateAsset { File = file };
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A<Stream>._, file.MimeType, CancellationToken))
.Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, false));
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, CancellationToken))
.Returns(new ImageInfo(600, 800, ImageOrientation.BottomRight, ImageFormat.PNG)).Once();
.Returns(new ImageInfo(ImageFormat.PNG, 800, 600, ImageOrientation.None, true)).Once();
await sut.EnhanceAsync(command, CancellationToken);
@ -83,7 +107,7 @@ public class ImageAssetMetadataSourceTests : GivenContext
Assert.Equal(600, command.Metadata.GetPixelHeight());
Assert.Equal(AssetType.Image, command.Type);
A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A<Stream>._, CancellationToken))
A.CallTo(() => assetThumbnailGenerator.FixAsync(stream, file.MimeType, A<Stream>._, CancellationToken))
.MustHaveHappened();
}

2
frontend/src/app/features/rules/pages/events/rule-event.component.html

@ -49,7 +49,7 @@
</div>
<div class="row event-dump" *ngIf="event.lastDump">
<div class="col-12">
<sqx-code-editor [ngModel]="event.lastDump" [disabled]="true" [wordWrap]="true" height="auto" mode="ace/editor/text"></sqx-code-editor>
<sqx-code-editor [ngModel]="event.lastDump" [disabled]="true" [wordWrap]="true" height="auto" mode="ace/mode/text"></sqx-code-editor>
</div>
</div>
</td>

4
frontend/src/app/features/rules/shared/actions/formattable-input.component.html

@ -1,6 +1,6 @@
<ng-container *ngIf="type === 'Text'; else code">
<div class="input-group">
<sqx-code-editor [completion]="actualCompletion" [singleLine]="true" [mode]="aceMode"></sqx-code-editor>
<sqx-code-editor [completion]="editorCompletion" [singleLine]="true" [mode]="editorMode" [snippets]="mode === 'Script'"></sqx-code-editor>
<div class="input-group-apppend">
<select class="form-select" [ngModel]="mode" (ngModelChange)="setMode($event)" [disabled]="disabled" tabindex="-1">
@ -17,5 +17,5 @@
</button>
</div>
<sqx-code-editor [completion]="actualCompletion" [height]="350" [mode]="aceMode"></sqx-code-editor>
<sqx-code-editor [completion]="editorCompletion" [height]="350" [mode]="editorMode" [snippets]="mode === 'Script'"></sqx-code-editor>
</ng-template>

2
frontend/src/app/features/rules/shared/actions/formattable-input.component.scss

@ -30,6 +30,8 @@ sqx-code-editor {
&.active {
color: $color-black;
font-family: inherit;
font-weight: 500;
}
}
}

149
frontend/src/app/features/rules/shared/actions/formattable-input.component.ts

@ -6,17 +6,17 @@
*/
import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, Input, ViewChild } from '@angular/core';
import { ControlValueAccessor, DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { CodeEditorComponent, ScriptCompletions, Types } from '@app/framework';
type TemplateMode = 'Text' | 'Script' | 'Liquid';
const TEMPLATE_MODES: ReadonlyArray<TemplateMode> = ['Text', 'Script', 'Liquid'];
export const SQX_FORMATTABLE_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FormattableInputComponent), multi: true,
};
type TemplateMode = 'Text' | 'Script' | 'Liquid';
const MODES: ReadonlyArray<TemplateMode> = ['Text', 'Script', 'Liquid'];
@Component({
selector: 'sqx-formattable-input[type]',
styleUrls: ['./formattable-input.component.scss'],
@ -37,91 +37,70 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie
@Input()
public completion: ScriptCompletions | undefined | null;
@ViewChild(DefaultValueAccessor)
public inputEditor!: DefaultValueAccessor;
@ViewChild(CodeEditorComponent)
public codeEditor!: CodeEditorComponent;
public disabled = false;
public modes = MODES;
public modes = TEMPLATE_MODES;
public mode: TemplateMode = 'Text';
public aceMode = 'ace/editor/text';
public get actualCompletion() {
return this.mode === 'Script' ? this.completion : null;
}
public get valueAccessor(): ControlValueAccessor {
return this.codeEditor || this.inputEditor;
}
public editorMode = 'ace/mode/text';
public editorCompletion?: ScriptCompletions | undefined | null;
public ngAfterViewInit() {
this.valueAccessor.registerOnChange((value: any) => {
this.codeEditor.registerOnChange((value: any) => {
this.value = value;
this.fnChanged(this.convertValue(value));
this.fnChanged(getValueFromMode(value, this.mode));
});
this.valueAccessor.registerOnTouched(() => {
this.codeEditor.registerOnTouched(() => {
this.fnTouched();
});
this.valueAccessor.writeValue(this.value);
this.codeEditor.writeValue(this.value);
}
public writeValue(obj: any) {
let mode: TemplateMode = 'Text';
const { value, mode } = getModeFromValue(obj);
if (Types.isString(obj)) {
this.value = obj;
if (obj.endsWith(')')) {
const lower = obj.toLowerCase();
if (lower.startsWith('liquid(')) {
this.value = obj.substring(7, obj.length - 1);
mode = 'Liquid';
} else if (lower.startsWith('script(')) {
this.value = obj.substring(7, obj.length - 1);
mode = 'Script';
}
}
} else {
this.value = undefined;
}
this.value = value;
this.setMode(mode, false);
this.valueAccessor?.writeValue(this.value);
this.codeEditor?.writeValue(value);
}
public setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
this.setDisabled(isDisabled);
this.codeEditor?.setDisabledState?.(isDisabled);
}
this.valueAccessor?.setDisabledState?.(isDisabled);
private setDisabled(isDisabled: boolean) {
this.disabled = isDisabled;
}
public setMode(mode: TemplateMode, emit = true) {
if (this.mode !== mode) {
this.mode = mode;
if (mode === 'Script') {
this.aceMode = 'ace/mode/javascript';
} else if (mode === 'Liquid') {
this.aceMode = 'ace/mode/liquid';
} else {
this.aceMode = 'ace/editor/text';
}
if (emit) {
this.fnChanged(this.convertValue(this.value));
this.fnTouched();
}
if (this.mode === mode) {
return;
}
if (mode === 'Script') {
this.editorMode = 'ace/mode/javascript';
this.editorCompletion = this.completion;
} else if (mode === 'Liquid') {
this.editorMode = 'ace/mode/liquid';
this.editorCompletion = this.completion?.filter(x => x.type !== 'Function');
} else {
this.editorMode = 'ace/mode/text';
this.editorCompletion = this.completion?.filter(x => x.type !== 'Function');
}
this.mode = mode;
if (emit) {
this.fnChanged(getValueFromMode(this.value, mode));
this.fnTouched();
}
}
@ -132,23 +111,45 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie
public registerOnTouched(fn: any) {
this.fnTouched = fn;
}
}
private convertValue(value: string | undefined) {
if (!value) {
return value;
}
function getValueFromMode(value: string | undefined, mode: TemplateMode) {
if (!value) {
return value;
}
value = value.trim();
value = value.trim();
switch (this.mode) {
case 'Liquid': {
return `Liquid(${value})`;
}
case 'Script': {
return `Script(${value})`;
}
switch (mode) {
case 'Liquid': {
return `Liquid(${value})`;
}
case 'Script': {
`Script(${value})`;
}
}
return value;
return value;
}
function getModeFromValue(value: any): { value: string | undefined; mode: TemplateMode } {
if (!Types.isString(value) || !value) {
return { value, mode: 'Text' };
}
if (value.endsWith(')')) {
const lower = value.toLowerCase();
if (lower.startsWith('liquid(')) {
value = value.substring(7, value.length - 1);
return { value, mode: 'Liquid' };
} else if (lower.startsWith('script(')) {
value = value.substring(7, value.length - 1);
return { value, mode: 'Script' };
}
}
return { value, mode: 'Text' };
}

11
frontend/src/app/framework/angular/forms/editors/code-editor.component.ts

@ -27,7 +27,7 @@ export const SQX_CODE_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CodeEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, FocusComponent {
export class CodeEditorComponent extends StatefulControlComponent<{}, any> implements AfterViewInit, FocusComponent {
private aceEditor: any;
private aceTools: any;
private valueChanged = new Subject();
@ -56,6 +56,9 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
@Input()
public singleLine = false;
@Input()
public snippets = true;
@Input()
public wordWrap = false;
@ -87,7 +90,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
this.setMode();
}
if (changes.height || changes.maxLines || changes.singleLine) {
if (changes.height || changes.maxLines || changes.singleLine || changes.snippets) {
this.setOptions();
}
@ -96,7 +99,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
}
}
public writeValue(obj: string) {
public writeValue(obj: any) {
try {
if (Types.isNull(obj) || Types.isUndefined(obj)) {
this.value = '';
@ -285,7 +288,7 @@ export class CodeEditorComponent extends StatefulControlComponent<{}, string> im
autoScrollEditorIntoView: this.singleLine,
enableBasicAutocompletion: !!this.aceTools,
enableLiveAutocompletion: !!this.aceTools,
enableSnippets: !!this.aceTools && !this.singleLine,
enableSnippets: !!this.aceTools && !this.singleLine && this.snippets,
highlightActiveLine: !this.singleLine,
maxLines,
minLines,

2
tools/TestSuite/docker-compose.yml

@ -85,7 +85,7 @@ services:
- mongo
resizer:
image: squidex/resizer:dev-40
image: squidex/resizer:1.3.0
networks:
- internal
depends_on:

Loading…
Cancel
Save