Browse Source

Update Rich text and improve caching.

pull/493/head
Sebastian 7 years ago
parent
commit
de84bf0737
  1. 2
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  2. 26
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  3. 3
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs
  4. 2
      frontend/app/declarations.d.ts
  5. 36
      frontend/app/features/content/pages/content/content-page.component.html
  6. 2
      frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html
  7. 2
      frontend/app/shared/components/forms/rich-editor.component.scss
  8. 71
      frontend/app/shared/components/forms/rich-editor.component.ts
  9. 21
      frontend/package-lock.json
  10. 1
      frontend/package.json

2
backend/src/Squidex.Web/Pipeline/CachingFilter.cs

@ -68,7 +68,7 @@ namespace Squidex.Web.Pipeline
} }
} }
cachingManager.Finish(httpContext, cachingOptions.MaxSurrogateKeysSize); cachingManager.Finish(httpContext);
} }
} }
} }

26
backend/src/Squidex.Web/Pipeline/CachingManager.cs

@ -13,6 +13,7 @@ using System.Text;
using System.Threading; using System.Threading;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -23,13 +24,12 @@ namespace Squidex.Web.Pipeline
{ {
public sealed class CachingManager : IRequestCache public sealed class CachingManager : IRequestCache
{ {
private readonly ObjectPool<StringBuilder> stringBuilderPool;
private readonly CachingOptions cachingOptions;
private readonly IHttpContextAccessor httpContextAccessor; private readonly IHttpContextAccessor httpContextAccessor;
internal sealed class CacheContext : IRequestCache, IDisposable internal sealed class CacheContext : IRequestCache, IDisposable
{ {
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());
private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
private readonly HashSet<string> keys = new HashSet<string>(); private readonly HashSet<string> keys = new HashSet<string>();
private readonly HashSet<string> headers = new HashSet<string>(); private readonly HashSet<string> headers = new HashSet<string>();
@ -87,7 +87,7 @@ namespace Squidex.Web.Pipeline
} }
} }
public void Finish(HttpResponse response, int maxSurrogateKeySize) public void Finish(HttpResponse response, int maxSurrogateKeySize, ObjectPool<StringBuilder> stringBuilderPool)
{ {
if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag)) if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag))
{ {
@ -104,7 +104,7 @@ namespace Squidex.Web.Pipeline
{ {
const int GuidLength = 36; const int GuidLength = 36;
var stringBuilder = StringBuilderPool.Get(); var stringBuilder = stringBuilderPool.Get();
try try
{ {
foreach (var key in keys) foreach (var key in keys)
@ -136,7 +136,7 @@ namespace Squidex.Web.Pipeline
} }
finally finally
{ {
StringBuilderPool.Return(stringBuilder); stringBuilderPool.Return(stringBuilder);
} }
} }
@ -164,11 +164,19 @@ namespace Squidex.Web.Pipeline
} }
} }
public CachingManager(IHttpContextAccessor httpContextAccessor) public CachingManager(IHttpContextAccessor httpContextAccessor, IOptions<CachingOptions> cachingOptions)
{ {
Guard.NotNull(httpContextAccessor); Guard.NotNull(httpContextAccessor);
Guard.NotNull(cachingOptions);
this.httpContextAccessor = httpContextAccessor; this.httpContextAccessor = httpContextAccessor;
this.cachingOptions = cachingOptions.Value;
stringBuilderPool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy
{
MaximumRetainedCapacity = cachingOptions.Value.MaxSurrogateKeysSize
});
} }
public void Start(HttpContext httpContext) public void Start(HttpContext httpContext)
@ -217,7 +225,7 @@ namespace Squidex.Web.Pipeline
} }
} }
public void Finish(HttpContext httpContext, int maxSurrogateKeySize) public void Finish(HttpContext httpContext)
{ {
Guard.NotNull(httpContext); Guard.NotNull(httpContext);
@ -225,7 +233,7 @@ namespace Squidex.Web.Pipeline
if (cacheContext != null) if (cacheContext != null)
{ {
cacheContext.Finish(httpContext.Response, maxSurrogateKeySize); cacheContext.Finish(httpContext.Response, cachingOptions.MaxSurrogateKeysSize, stringBuilderPool);
} }
} }
} }

3
backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -38,7 +39,7 @@ namespace Squidex.Web.Pipeline
A.CallTo(() => httpContextAccessor.HttpContext) A.CallTo(() => httpContextAccessor.HttpContext)
.Returns(httpContext); .Returns(httpContext);
cachingManager = new CachingManager(httpContextAccessor); cachingManager = new CachingManager(httpContextAccessor, Options.Create(new CachingOptions()));
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var actionFilters = new List<IFilterMetadata>(); var actionFilters = new List<IFilterMetadata>();

2
frontend/app/declarations.d.ts

@ -17,4 +17,4 @@ declare module 'sortablejs' {
} }
export function create(element: any, options: any): Ref; export function create(element: any, options: any): Ref;
} }

36
frontend/app/features/content/pages/content/content-page.component.html

@ -21,26 +21,28 @@
<ng-container menu> <ng-container menu>
<ng-container *ngIf="content; else noContent"> <ng-container *ngIf="content; else noContent">
<div class="dropdown dropdown-options ml-1" *ngIf="!schema.isSingleton"> <ng-container *ngIf="!schema.isSingleton">
<sqx-preview-button [schema]="schema" [content]="content"></sqx-preview-button> <sqx-preview-button [schema]="schema" [content]="content"></sqx-preview-button>
<ng-container *ngIf="content?.canDelete"> <div class="dropdown dropdown-options ml-1">
<button type="button" class="btn btn-outline-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions> <ng-container *ngIf="content?.canDelete">
<i class="icon-more"></i> <button type="button" class="btn btn-outline-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
</button> <i class="icon-more"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade> <ng-container *sqxModal="dropdown;closeAlways:true">
<a class="dropdown-item dropdown-item-delete" <div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
(sqxConfirmClick)="delete()" <a class="dropdown-item dropdown-item-delete"
confirmTitle="Delete content" (sqxConfirmClick)="delete()"
confirmText="Do you really want to delete the content?"> confirmTitle="Delete content"
Delete confirmText="Do you really want to delete the content?">
</a> Delete
</div> </a>
</div>
</ng-container>
</ng-container> </ng-container>
</ng-container> </div>
</div> </ng-container>
<ng-container *ngIf="content?.canUpdate"> <ng-container *ngIf="content?.canUpdate">
<button type="submit" class="btn btn-primary ml-1" title="CTRL + S"> <button type="submit" class="btn btn-primary ml-1" title="CTRL + S">

2
frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html

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

2
frontend/app/shared/components/forms/rich-editor.component.scss

@ -3,5 +3,5 @@
} }
.editor { .editor {
height: 300px; height: 400px;
} }

71
frontend/app/shared/components/forms/rich-editor.component.ts

@ -72,10 +72,16 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
} }
public ngAfterViewInit() { public ngAfterViewInit() {
const self = this; this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/5.2.0/tinymce.min.js').then(() => {
const timer = setInterval(() => {
const target = this.editor.nativeElement;
if (document.body.contains(target)) {
tinymce.init(this.getEditorOptions(target));
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.4/tinymce.min.js').then(() => { clearInterval(timer);
tinymce.init(self.getEditorOptions()); }
}, 10);
}); });
} }
@ -95,21 +101,13 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
this.assetsDialog.show(); this.assetsDialog.show();
} }
private getEditorOptions() { private getEditorOptions(target: any): any {
const self = this; const self = this;
return { return {
convert_fonts_to_spans: true, ...DEFAULT_PROPS,
convert_urls: false,
paste_data_images: true, images_upload_handler: (blob: any, success: (url: string) => void, failure: (message: string) => void) => {
plugins: 'code image media link lists advlist paste',
min_height: 300,
max_height: 800,
removed_menuitems: 'newdocument',
resize: true,
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets',
images_upload_handler: (blob: any, success: (url: string) => void, failed: () => void) => {
const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() }); const file = new File([blob.blob()], blob.filename(), { lastModified: new Date().getTime() });
self.assetUploader.uploadFile(file) self.assetUploader.uploadFile(file)
@ -119,26 +117,27 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
} }
}, error => { }, error => {
if (!Types.is(error, UploadCanceled)) { if (!Types.is(error, UploadCanceled)) {
failed(); failure('Failed');
} }
}); });
}, },
setup: (editor: any) => { setup: (editor: any) => {
self.tinyEditor = editor; editor.ui.registry.addButton('assets', {
self.tinyEditor.addButton('assets', { onAction: self.showSelector,
onclick: self.showSelector, icon: 'gallery',
icon: 'assets',
text: '', text: '',
tooltip: 'Insert Assets' tooltip: 'Insert Assets'
}); });
self.tinyEditor.on('init', () => { editor.on('init', () => {
self.tinyEditor = editor;
self.setContent(); self.setContent();
self.setReadOnly(); self.setReadOnly();
}); });
self.tinyEditor.on('change', () => { editor.on('change', () => {
const value = editor.getContent(); const value = editor.getContent();
if (this.value !== value) { if (this.value !== value) {
@ -148,7 +147,7 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
} }
}); });
self.tinyEditor.on('paste', (event: ClipboardEvent) => { editor.on('paste', (event: ClipboardEvent) => {
if (event.clipboardData) { if (event.clipboardData) {
for (let i = 0; i < event.clipboardData.items.length; i++) { for (let i = 0; i < event.clipboardData.items.length; i++) {
const file = event.clipboardData.items[i].getAsFile(); const file = event.clipboardData.items[i].getAsFile();
@ -160,7 +159,7 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
} }
}); });
self.tinyEditor.on('drop', (event: DragEvent) => { editor.on('drop', (event: DragEvent) => {
if (event.dataTransfer) { if (event.dataTransfer) {
for (let i = 0; i < event.dataTransfer.files.length; i++) { for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files.item(i); const file = event.dataTransfer.files.item(i);
@ -174,21 +173,19 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
return false; return false;
}); });
self.tinyEditor.on('blur', () => { editor.on('blur', () => {
self.callTouched(); self.callTouched();
}); });
self.setReadOnly();
}, },
target: self.editor.nativeElement target
}; };
} }
public writeValue(obj: any) { public writeValue(obj: any) {
this.value = Types.isString(obj) ? obj : ''; this.value = Types.isString(obj) ? obj : '';
if (this.tinyEditor) { if (this.tinyEditor && this.tinyEditor.initialized) {
this.setContent(); this.setContent();
} }
} }
@ -196,7 +193,7 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled; this.isDisabled = isDisabled;
if (this.tinyEditor) { if (this.tinyEditor && this.tinyEditor.initialized) {
this.setReadOnly(); this.setReadOnly();
} }
} }
@ -265,4 +262,16 @@ export class RichEditorComponent extends StatefulControlComponent<undefined, str
} }
}); });
} }
} }
const DEFAULT_PROPS = {
convert_fonts_to_spans: true,
convert_urls: false,
paste_data_images: true,
plugins: 'code image media link lists advlist paste',
min_height: 400,
max_height: 800,
removed_menuitems: 'newdocument',
resize: true,
toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter | bullist numlist outdent indent | link image media | assets'
};

21
frontend/package-lock.json

@ -490,6 +490,14 @@
"integrity": "sha512-LlhwGivHkUV8ehNmaXjGGXopLm91G9ORIRcjw7Ya47jVAIGudewFZM2PdPXBvueZfRWwYzLt083wiPfKRXrSlg==", "integrity": "sha512-LlhwGivHkUV8ehNmaXjGGXopLm91G9ORIRcjw7Ya47jVAIGudewFZM2PdPXBvueZfRWwYzLt083wiPfKRXrSlg==",
"dev": true "dev": true
}, },
"@types/jquery": {
"version": "3.3.33",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.33.tgz",
"integrity": "sha512-U6IdXYGkfUI42SR79vB2Spj+h1Ly3J3UZjpd8mi943lh126TK7CB+HZOxGh2nM3IySor7wqVQdemD/xtydsBKA==",
"requires": {
"@types/sizzle": "*"
}
},
"@types/json5": { "@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@ -557,6 +565,19 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg=="
},
"@types/tinymce": {
"version": "4.5.24",
"resolved": "https://registry.npmjs.org/@types/tinymce/-/tinymce-4.5.24.tgz",
"integrity": "sha512-g2aFp+/GHTD6P2ZI2tBwZGIDi+64oocoDleCHtAWLYlFOR9fjPO+pdvmGjpJG3C7XeMZvugFx2NhRBL+Jb/wbQ==",
"requires": {
"@types/jquery": "*"
}
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.8.5", "version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",

1
frontend/package.json

@ -62,6 +62,7 @@
"@types/node": "13.7.4", "@types/node": "13.7.4",
"@types/react": "16.9.21", "@types/react": "16.9.21",
"@types/react-dom": "16.9.5", "@types/react-dom": "16.9.5",
"@types/tinymce": "^4.5.24",
"browserslist": "4.8.7", "browserslist": "4.8.7",
"caniuse-lite": "1.0.30001028", "caniuse-lite": "1.0.30001028",
"circular-dependency-plugin": "5.2.0", "circular-dependency-plugin": "5.2.0",

Loading…
Cancel
Save