Browse Source

Autotranslation with DeepL

pull/347/head
Sebastian Stehle 7 years ago
parent
commit
a507163248
  1. 89
      src/Squidex.Infrastructure/Translations/DeepLTranslator.cs
  2. 17
      src/Squidex.Infrastructure/Translations/ITranslator.cs
  3. 22
      src/Squidex.Infrastructure/Translations/NoopTranslator.cs
  4. 25
      src/Squidex.Infrastructure/Translations/Translation.cs
  5. 18
      src/Squidex.Infrastructure/Translations/TranslationResult.cs
  6. 2
      src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs
  7. 2
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  8. 32
      src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs
  9. 30
      src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs
  10. 53
      src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs
  11. 18
      src/Squidex/Config/Domain/InfrastructureServices.cs
  12. 2
      src/Squidex/WebStartup.cs
  13. 2
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  14. 4
      src/Squidex/app/features/assets/pages/assets-page.component.html
  15. 4
      src/Squidex/app/features/content/pages/content/content-field.component.html
  16. 62
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  17. 2
      src/Squidex/app/features/content/pages/content/content-history-page.component.html
  18. 10
      src/Squidex/app/features/content/pages/content/content-page.component.html
  19. 2
      src/Squidex/app/features/content/pages/content/field-languages.component.ts
  20. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  21. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  22. 8
      src/Squidex/app/features/content/shared/content-status.component.html
  23. 8
      src/Squidex/app/features/content/shared/content-status.component.ts
  24. 10
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  25. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  26. 8
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  27. 4
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  28. 8
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  29. 10
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  30. 8
      src/Squidex/app/features/settings/pages/languages/languages-page.component.html
  31. 8
      src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html
  32. 4
      src/Squidex/app/features/settings/pages/plans/plans-page.component.html
  33. 8
      src/Squidex/app/features/settings/pages/roles/roles-page.component.html
  34. 2
      src/Squidex/app/framework/angular/forms/autocomplete.component.html
  35. 2
      src/Squidex/app/framework/angular/forms/tag-editor.component.html
  36. 8
      src/Squidex/app/framework/angular/modals/dialog-renderer.component.html
  37. 56
      src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss
  38. 18
      src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts
  39. 147
      src/Squidex/app/framework/angular/modals/modal-target.directive.ts
  40. 3
      src/Squidex/app/framework/angular/modals/tooltip.component.html
  41. 13
      src/Squidex/app/framework/angular/modals/tooltip.component.scss
  42. 56
      src/Squidex/app/framework/angular/modals/tooltip.component.ts
  43. 62
      src/Squidex/app/framework/angular/modals/tooltip.directive.ts
  44. 2
      src/Squidex/app/framework/declarations.ts
  45. 1
      src/Squidex/app/framework/internal.ts
  46. 6
      src/Squidex/app/framework/module.ts
  47. 20
      src/Squidex/app/framework/services/dialog.service.spec.ts
  48. 18
      src/Squidex/app/framework/services/dialog.service.ts
  49. 85
      src/Squidex/app/framework/utils/modal-positioner.spec.ts
  50. 116
      src/Squidex/app/framework/utils/modal-positioner.ts
  51. 2
      src/Squidex/app/shared/components/asset.component.html
  52. 4
      src/Squidex/app/shared/components/comment.component.html
  53. 4
      src/Squidex/app/shared/components/history-list.component.html
  54. 4
      src/Squidex/app/shared/components/language-selector.component.html
  55. 4
      src/Squidex/app/shared/components/search-form.component.html
  56. 1
      src/Squidex/app/shared/internal.ts
  57. 2
      src/Squidex/app/shared/module.ts
  58. 14
      src/Squidex/app/shared/services/news.service.ts
  59. 8
      src/Squidex/app/shared/services/schemas.service.ts
  60. 57
      src/Squidex/app/shared/services/translations.service.spec.ts
  61. 49
      src/Squidex/app/shared/services/translations.service.ts
  62. 7
      src/Squidex/app/theme/icomoon/Read Me.txt
  63. 8
      src/Squidex/app/theme/icomoon/demo-files/demo.css
  64. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.eot
  65. 1
      src/Squidex/app/theme/icomoon/fonts/icomoon.svg
  66. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.ttf
  67. BIN
      src/Squidex/app/theme/icomoon/fonts/icomoon.woff
  68. 2
      src/Squidex/app/theme/icomoon/selection.json
  69. 391
      src/Squidex/app/theme/icomoon/style.css
  70. 7
      src/Squidex/appsettings.json

89
src/Squidex.Infrastructure/Translations/DeepLTranslator.cs

@ -0,0 +1,89 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure.Json;
namespace Squidex.Infrastructure.Translations
{
public sealed class DeepLTranslator : ITranslator
{
private const string Url = "https://api.deepl.com/v2/translate";
private readonly HttpClient httpClient = new HttpClient();
private readonly string authKey;
private readonly IJsonSerializer jsonSerializer;
private sealed class Response
{
public ResponseTranslation[] Translations { get; set; }
}
private sealed class ResponseTranslation
{
public string Text { get; set; }
}
public DeepLTranslator(string authKey, IJsonSerializer jsonSerializer)
{
Guard.NotNull(authKey, nameof(authKey));
Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
this.authKey = authKey;
this.jsonSerializer = jsonSerializer;
}
public async Task<Translation> Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(sourceText) || targetLanguage == null)
{
return new Translation(TranslationResult.NotTranslated, sourceText);
}
var parameters = new Dictionary<string, string>
{
["auth_key"] = authKey,
["text"] = sourceText,
["target_lang"] = GetLanguageCode(targetLanguage)
};
if (sourceLanguage != null)
{
parameters["source_lang"] = GetLanguageCode(sourceLanguage);
}
var response = await httpClient.PostAsync(Url, new FormUrlEncodedContent(parameters), ct);
var responseString = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var result = jsonSerializer.Deserialize<Response>(responseString);
if (result?.Translations?.Length == 1)
{
return new Translation(TranslationResult.Translated, result.Translations[0].Text);
}
}
if (response.StatusCode == HttpStatusCode.BadRequest)
{
return new Translation(TranslationResult.LanguageNotSupported, resultText: responseString);
}
return new Translation(TranslationResult.Failed, resultText: responseString);
}
private string GetLanguageCode(Language language)
{
return language.Iso2Code.Substring(0, 2).ToUpperInvariant();
}
}
}

17
src/Squidex.Infrastructure/Translations/ITranslator.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Translations
{
public interface ITranslator
{
Task<Translation> Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default);
}
}

22
src/Squidex.Infrastructure/Translations/NoopTranslator.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Translations
{
public sealed class NoopTranslator : ITranslator
{
public Task<Translation> Translate(string sourceText, Language targetLanguage, Language sourceLanguage = null, CancellationToken ct = default)
{
var result = new Translation(TranslationResult.NotImplemented);
return Task.FromResult(result);
}
}
}

25
src/Squidex.Infrastructure/Translations/Translation.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Translations
{
public sealed class Translation
{
public TranslationResult Result { get; }
public string Text { get; }
public string ResultText { get; set; }
public Translation(TranslationResult result, string text = null, string resultText = null)
{
Text = text;
Result = result;
ResultText = resultText;
}
}
}

18
src/Squidex.Infrastructure/Translations/TranslationResult.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Translations
{
public enum TranslationResult
{
Translated,
LanguageNotSupported,
NotTranslated,
NotImplemented,
Failed
}
}

2
src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs

@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.News.Service
{
public sealed class FeaturesService
{
private const int FeatureVersion = 1;
private const int FeatureVersion = 2;
private static readonly QueryContext Flatten = QueryContext.Default.Flatten();
private readonly SquidexClient<NewsEntity, FeatureDto> client;

2
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -37,7 +37,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <summary>
/// Get schemas.
/// </summary>
/// <param name="app">The name of the app to get the schemas for.</param>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Schemas returned.
/// 404 => App not found.

32
src/Squidex/Areas/Api/Controllers/Translations/Models/TranslateDto.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Controllers.Translations.Models
{
public sealed class TranslateDto
{
/// <summary>
/// The text to translate.
/// </summary>
[Required]
public string Text { get; set; }
/// <summary>
/// The target language.
/// </summary>
[Required]
public Language TargetLanguage { get; set; }
/// <summary>
/// The optional source language.
/// </summary>
public Language SourceLanguage { get; set; }
}
}

30
src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Translations;
namespace Squidex.Areas.Api.Controllers.Translations.Models
{
public sealed class TranslationDto
{
/// <summary>
/// The result of the translation.
/// </summary>
public TranslationResult Result { get; set; }
/// <summary>
/// The translated text.
/// </summary>
public string Text { get; set; }
public static TranslationDto FromTranslation(Translation translation)
{
return SimpleMapper.Map(translation, new TranslationDto());
}
}
}

53
src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Translations.Models;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Translations;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Translations
{
/// <summary>
/// Manage translations.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Translations))]
public sealed class TranslationsController : ApiController
{
private readonly ITranslator translator;
public TranslationsController(ICommandBus commandBus, ITranslator translator)
: base(commandBus)
{
this.translator = translator;
}
/// <summary>
/// Translate a text.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The translation request.</param>
/// <returns>
/// 200 => Text translated.
/// </returns>
[HttpPost]
[Route("apps/{app}/translations/")]
[ProducesResponseType(typeof(TranslationDto), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> GetLanguages(string app, [FromBody] TranslateDto request)
{
var result = await translator.Translate(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted);
var response = TranslationDto.FromTranslation(result);
return Ok(response);
}
}
}

18
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using Squidex.Areas.Api.Controllers.News.Service;
@ -18,6 +19,8 @@ using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Shared.Users;
@ -27,8 +30,21 @@ namespace Squidex.Config.Domain
{
public static class InfrastructureServices
{
public static void AddMyInfrastructureServices(this IServiceCollection services)
public static void AddMyInfrastructureServices(this IServiceCollection services, IConfiguration config)
{
var deeplAuthKey = config.GetValue<string>("translations:deeplAuthKey");
if (!string.IsNullOrWhiteSpace(deeplAuthKey))
{
services.AddSingletonAs(c => new DeepLTranslator(deeplAuthKey, c.GetRequiredService<IJsonSerializer>()))
.As<ITranslator>();
}
else
{
services.AddSingletonAs<NoopTranslator>()
.As<ITranslator>();
}
services.AddHealthChecks()
.AddCheck<GCHealthCheck>("GC", tags: new[] { "node" })
.AddCheck<OrleansHealthCheck>("Orleans", tags: new[] { "cluster" })

2
src/Squidex/WebStartup.cs

@ -58,7 +58,7 @@ namespace Squidex
services.AddMyEventPublishersServices(config);
services.AddMyEventStoreServices(config);
services.AddMyIdentityServer();
services.AddMyInfrastructureServices();
services.AddMyInfrastructureServices(config);
services.AddMyLoggingServices(config);
services.AddMyMigrationServices();
services.AddMyMvc();

2
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -51,7 +51,7 @@
<tbody *ngFor="let userInfo of usersState.users | async; trackBy: trackByUser">
<tr [routerLink]="userInfo.user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" [attr.alt]="userInfo.user.name" [attr.title]="userInfo.user.name" [attr.src]="userInfo.user | sqxUserDtoPicture" />
<img class="user-picture" [attr.alt]="userInfo.user.name" attr="{{userInfo.user.name}}" [attr.src]="userInfo.user | sqxUserDtoPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{userInfo.user.displayName}}</span>

4
src/Squidex/app/features/assets/pages/assets-page.component.html

@ -53,9 +53,7 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="filtersLink">Filters</sqx-tooltip>
<a class="panel-link" routerLink="filters" routerLinkActive="active" #filtersLink>
<a class="panel-link" routerLink="filters" routerLinkActive="active" title="Filters" titlePosition="left">
<i class="icon-filter"></i>
</a>
</div>

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

@ -2,6 +2,10 @@
<div [class.col-12]="!fieldFormCompare" [class.col-6]="fieldFormCompare">
<div class="table-items-row" [class.field-invalid]="isInvalid | async">
<div class="languages-buttons">
<button *ngIf="isTranslateable" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="translate()" title="Autotranslate from master language">
<i class="icon-translate"></i>
</button>
<sqx-field-languages
[field]="field"
[language]="language"

62
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -12,12 +12,15 @@ import { combineLatest } from 'rxjs/operators';
import {
AppLanguageDto,
AppsState,
EditContentForm,
fieldInvariant,
invalid$,
LocalStoreService,
RootFieldDto,
SchemaDto,
TranslateDto,
TranslationsService,
Types,
value$
} from '@app/shared';
@ -59,17 +62,28 @@ export class ContentFieldComponent implements OnChanges {
public isInvalid: Observable<boolean>;
public isDifferent: Observable<boolean>;
public isTranslateable: boolean;
constructor(
private readonly localStore: LocalStoreService
private readonly appsState: AppsState,
private readonly localStore: LocalStoreService,
private readonly translations: TranslationsService
) {
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['field']) {
this.showAllControls = this.localStore.getBoolean(this.configKey());
}
if (changes['fieldForm']) {
this.isInvalid = invalid$(this.fieldForm);
}
if (changes['fieldForm'] || changes['field'] || changes['languages']) {
this.isTranslateable = this.field.isTranslateable;
}
if ((changes['fieldForm'] || changes['fieldFormCompare']) && this.fieldFormCompare) {
this.isDifferent =
value$(this.fieldForm).pipe(
@ -77,10 +91,6 @@ export class ContentFieldComponent implements OnChanges {
(lhs, rhs) => !Types.jsJsonEquals(lhs, rhs)));
}
if (changes['field']) {
this.showAllControls = this.localStore.getBoolean(this.configKey());
}
const control = this.findControl(this.fieldForm);
if (this.selectedFormControl !== control) {
@ -120,6 +130,46 @@ export class ContentFieldComponent implements OnChanges {
}
}
public translate() {
const master = this.languages.find(x => x.isMaster);
if (master) {
const masterCode = master.iso2Code;
const masterValue = this.fieldForm.get(masterCode)!.value;
if (masterValue) {
if (this.showAllControls) {
for (let language of this.languages) {
if (!language.isMaster) {
this.translateValue(masterValue, masterCode, language.iso2Code);
}
}
} else {
this.translateValue(masterValue, masterCode, this.language.iso2Code);
}
}
}
}
private translateValue(text: string, sourceLanguage: string, targetLanguage: string) {
const control = this.fieldForm.get(targetLanguage);
if (control) {
const value = control.value;
if (!value) {
const request = new TranslateDto(text, sourceLanguage, targetLanguage);
this.translations.translate(this.appsState.appName, request)
.subscribe(result => {
if (result.text) {
control.setValue(result.text);
}
});
}
}
}
private findControl(form: FormGroup) {
if (this.field.isLocalizable) {
return form.controls[this.language.iso2Code];
@ -129,7 +179,7 @@ export class ContentFieldComponent implements OnChanges {
}
public prefix(language: AppLanguageDto) {
return `(${language.iso2Code}`;
return `(${language.iso2Code})`;
}
public trackByLanguage(index: number, language: AppLanguageDto) {

2
src/Squidex/app/features/content/pages/content/content-history-page.component.html

@ -6,7 +6,7 @@
<ng-container content>
<div *ngFor="let event of events | async; trackBy: trackByEvent" class="event row no-gutters">
<div class="col-auto">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
<img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [attr.src]="event.actor | sqxUserPictureRef" />
</div>
<div class="col pl-2">
<div class="event-message">

10
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -126,19 +126,15 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">commentsLink</sqx-tooltip>
<a class="panel-link" routerLink="comments" routerLinkActive="active" #commentsLink>
<a class="panel-link" routerLink="comments" routerLinkActive="active" title="Comments" titlePosition="left">
<i class="icon-comments"></i>
</a>
<sqx-tooltip position="leftTop" [target]="historyLink">History</sqx-tooltip>
<a class="panel-link" routerLink="history" routerLinkActive="active" #historyLink>
<a class="panel-link" routerLink="history" routerLinkActive="active" title="History" titlePosition="left" #historyLink>
<i class="icon-time"></i>
</a>
<sqx-onboarding-tooltip helpId="history" [for]="historyLink" position="leftTop" after="120000">
<sqx-onboarding-tooltip helpId="history" [for]="historyLink" position="left-top" after="120000">
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.
</sqx-onboarding-tooltip>
</div>

2
src/Squidex/app/features/content/pages/content/field-languages.component.ts

@ -24,7 +24,7 @@ import { AppLanguageDto, RootFieldDto } from '@app/shared';
[languages]="languages">
</sqx-language-selector>
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="topRight" after="120000">
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="top-right" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
</ng-container>

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

@ -126,9 +126,7 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="filtersLink">Filters</sqx-tooltip>
<a class="panel-link" routerLink="filters" routerLinkActive="active" #filtersLink>
<a class="panel-link" routerLink="filters" routerLinkActive="active" title="Filters" titlePosition="left">
<i class="icon-filter"></i>
</a>
</div>

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

@ -32,7 +32,7 @@
</td>
<td class="cell-user" *ngIf="!isCompact && patchForm.form.pristine" (click)="shouldStop($event)">
<img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<ng-container *ngIf="patchForm.form.dirty">

8
src/Squidex/app/features/content/shared/content-status.component.html

@ -1,17 +1,13 @@
<ng-container *ngIf="!scheduledTo">
<span class="content-status content-status-{{displayStatus | lowercase}} mr-1" [class.middle]="alignMiddle" #statusIcon>
<span class="content-status content-status-{{displayStatus | lowercase}} mr-1" [class.middle]="alignMiddle" title="{{displayStatus}}" titlePosition="top">
<i class="icon-circle"></i>
</span>
<sqx-tooltip [target]="statusIcon">{{displayStatus}}</sqx-tooltip>
</ng-container>
<ng-container *ngIf="scheduledTo">
<span class="content-status content-status-{{scheduledTo | lowercase}} mr-1" [class.middle]="alignMiddle" #statusIcon>
<span class="content-status content-status-{{scheduledTo | lowercase}} mr-1" [class.middle]="alignMiddle" title="{{displayStatus}}" titlePosition="top">
<i class="icon-clock"></i>
</span>
<sqx-tooltip position="topRight" [target]="statusIcon">Will be set to '{{scheduledTo}}' at {{scheduledAt | sqxFullDateTime}}</sqx-tooltip>
</ng-container>
<span class="content-status-label" *ngIf="showLabel">{{displayStatus}}</span>

8
src/Squidex/app/features/content/shared/content-status.component.ts

@ -35,7 +35,13 @@ export class ContentStatusComponent {
public alignMiddle = true;
public get displayStatus() {
return !!this.isPending ? 'Pending' : this.status;
if (this.scheduledAt) {
return `Will be set to '${this.scheduledTo}' at ${this.scheduledAt.toStringFormat('LLLL')}`;
} else if (this.isPending) {
return 'Pending';
} else {
return this.status;
}
}
}

10
src/Squidex/app/features/rules/pages/rules/rules-page.component.html

@ -77,19 +77,15 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">Events</sqx-tooltip>
<a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" #historyLink>
<a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i>
</a>
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left" #helpLink>
<i class="icon-help"></i>
</a>
<sqx-onboarding-tooltip helpId="help" [for]="helpLink" position="leftTop" after="180000">
<sqx-onboarding-tooltip helpId="help" [for]="helpLink" position="left-top" after="180000">
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" sqxExternalLink>https://docs.squidex.io</a> for the full documentation.
</sqx-onboarding-tooltip>
</div>

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

@ -7,7 +7,7 @@
<span class="field-name">
<i class="field-icon icon-type-{{field.properties.fieldType}}"></i>
<span [class.field-hidden]="field.isHidden" [attr.title]="field.isHidden ? 'Hidden Field' : 'Visible Field'">{{field.displayName}}</span>
<span [class.field-hidden]="field.isHidden" title="{{field.isHidden ? 'Hidden Field' : 'Visible Field'}}">{{field.displayName}}</span>
<span class="field-partitioning ml-2" *ngIf="field['isLocalizable']">localizable</span>
</span>
</div>

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

@ -48,11 +48,11 @@
</div>
</div>
<sqx-onboarding-tooltip helpId="history" [for]="buttonOptions" position="bottomRight" after="60000">
<sqx-onboarding-tooltip helpId="history" [for]="buttonOptions" position="bottom-right" after="60000">
Open the context menu to delete the schema or to create some scripts for content changes.
</sqx-onboarding-tooltip>
<sqx-onboarding-tooltip helpId="history" [for]="buttonPublish" position="bottomRight" after="240000">
<sqx-onboarding-tooltip helpId="history" [for]="buttonPublish" position="bottom-right" after="240000">
Note, that you have to publish the schema before you can add content to it.
</sqx-onboarding-tooltip>
</ng-container>
@ -83,9 +83,7 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left">
<i class="icon-help"></i>
</a>
</div>

4
src/Squidex/app/features/settings/pages/backups/backups-page.component.html

@ -94,9 +94,7 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left">
<i class="icon-help"></i>
</a>
</div>

8
src/Squidex/app/features/settings/pages/clients/clients-page.component.html

@ -48,15 +48,11 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">History</sqx-tooltip>
<a class="panel-link" routerLink="history" routerLinkActive="active" #historyLink>
<a class="panel-link" routerLink="history" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i>
</a>
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left">
<i class="icon-help"></i>
</a>
</div>

10
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -26,7 +26,7 @@
<tbody *ngFor="let contributorInfo of contributors; trackBy: trackByContributor">
<tr>
<td class="cell-user">
<img class="user-picture" [attr.title]="contributorInfo.contributor.contributorId | sqxUserName" [attr.src]="contributorInfo.contributor.contributorId | sqxUserPicture" />
<img class="user-picture" title="{{contributorInfo.contributor.contributorId | sqxUserName}}" [attr.src]="contributorInfo.contributor.contributorId | sqxUserPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{contributorInfo.contributor.contributorId | sqxUserName}}</span>
@ -74,15 +74,11 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">History</sqx-tooltip>
<a class="panel-link" routerLink="history" routerLinkActive="active" #historyLink>
<a class="panel-link" routerLink="history" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i>
</a>
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left">
<i class="icon-help"></i>
</a>
</div>

8
src/Squidex/app/features/settings/pages/languages/languages-page.component.html

@ -44,15 +44,11 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">History</sqx-tooltip>
<a class="panel-link" routerLink="history" routerLinkActive="active" #historyLink>
<a class="panel-link" routerLink="history" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i>
</a>
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left">
<i class="icon-help"></i>
</a>
</div>

8
src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html

@ -26,15 +26,11 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">History</sqx-tooltip>
<a class="panel-link" routerLink="history" routerLinkActive="active" #historyLink>
<a class="panel-link" routerLink="history" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i>
</a>
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left">
<i class="icon-help"></i>
</a>
</div>

4
src/Squidex/app/features/settings/pages/plans/plans-page.component.html

@ -80,9 +80,7 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">History</sqx-tooltip>
<a class="panel-link" routerLink="history" routerLinkActive="active" #historyLink>
<a class="panel-link" routerLink="history" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i>
</a>
</div>

8
src/Squidex/app/features/settings/pages/roles/roles-page.component.html

@ -39,15 +39,11 @@
<ng-container sidebar>
<div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">History</sqx-tooltip>
<a class="panel-link" routerLink="history" routerLinkActive="active" #historyLink>
<a class="panel-link" routerLink="history" routerLinkActive="active" title="History" titlePosition="left">
<i class="icon-time"></i>
</a>
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left">
<i class="icon-help"></i>
</a>
</div>

2
src/Squidex/app/framework/angular/forms/autocomplete.component.html

@ -5,7 +5,7 @@
autocorrect="off"
autocapitalize="off">
<div *ngIf="snapshot.suggestedItems.length > 0" [sqxModalTarget]="input" class="control-dropdown" #container position="bottomLeft">
<div *ngIf="snapshot.suggestedItems.length > 0" [sqxModalTarget]="input" class="control-dropdown" #container position="bottom-left">
<div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex"
[container]="container"

2
src/Squidex/app/framework/angular/forms/tag-editor.component.html

@ -23,7 +23,7 @@
spellcheck="false">
</div>
<div *ngIf="snapshot.suggestedItems.length > 0" [sqxModalTarget]="form" class="control-dropdown" #container position="bottomLeft">
<div *ngIf="snapshot.suggestedItems.length > 0" [sqxModalTarget]="form" class="control-dropdown" #container position="bottom-left">
<div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex"
[container]="container"

8
src/Squidex/app/framework/angular/modals/dialog-renderer.component.html

@ -15,10 +15,16 @@
</ng-container>
</sqx-modal-dialog>
<div class="notification-container notification-container-{{position}}">
<div class="notification-container notification-container-bottom-right">
<div class="alert alert-dismissible alert-{{notification.messageType}}" *ngFor="let notification of snapshot.notifications" (click)="close(notification)" @fade>
<button type="button" class="close" data-dismiss="alert" (closed)="close(notification)">&times;</button>
<span [innerHTML]="notification.message"></span>
</div>
</div>
<ng-container *ngIf="snapshot.tooltip; let tooltip">
<div class="tooltip2 tooltip2-{{tooltip.position}}" [sqxModalTarget]="tooltip.target" [position]="tooltip.position" [offset]="8">
{{tooltip.text}}
</div>
</ng-container>

56
src/Squidex/app/framework/angular/modals/dialog-renderer.component.scss

@ -1,6 +1,8 @@
@import '_mixins';
@import '_vars';
// sass-lint:disable single-line-per-selector
.notification-container {
& {
margin: .625rem;
@ -14,19 +16,65 @@
max-height: 20rem;
}
&-topright {
&-top-right {
@include fixed(0, 0, auto, auto);
}
&-topleft {
&-top-left {
@include fixed(0, auto, auto, 0);
}
&-bottomright {
&-bottom-right {
@include fixed(auto, 0, 0, auto);
}
&-bottomleft {
&-bottom-left {
@include fixed(auto, auto, 0, 0);
}
}
$caret-size: 6px;
.tooltip2 {
& {
color: $color-dark-foreground;
background: $color-tooltip;
border: 0;
font-size: .9rem;
font-weight: normal;
white-space: nowrap;
padding: .5rem;
}
&-left {
&::after {
@include caret-right($color-tooltip, $caret-size);
@include absolute(50%, -$caret-size * 2, auto, auto);
margin-top: -$caret-size;
}
}
&-right {
&::after {
@include caret-left($color-tooltip, $caret-size);
@include absolute(50%, auto, auto, -$caret-size * 2);
margin-top: -$caret-size;
}
}
&-top {
&::after {
@include caret-bottom($color-tooltip, $caret-size);
@include absolute(auto, auto, -$caret-size * 2, 50%);
margin-left: -$caret-size;
}
}
&-bottom {
&::after {
@include caret-top($color-tooltip, $caret-size);
@include absolute(-$caret-size * 2, auto, auto, 50%);
margin-left: -$caret-size;
}
}
}

18
src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights r vbeserved
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { timer } from 'rxjs';
import {
@ -16,11 +16,14 @@ import {
Notification,
StatefulComponent
} from '@app/framework/internal';
import { Tooltip } from '@app/shared';
interface State {
dialogRequest?: DialogRequest | null;
notifications: Notification[];
tooltip?: Tooltip | null;
}
@Component({
@ -33,9 +36,6 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DialogRendererComponent extends StatefulComponent<State> implements OnInit {
@Input()
public position = 'bottomright';
public dialogView = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
@ -75,6 +75,16 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
this.next(s => ({ ...s, dialogRequest }));
}));
this.own(
this.dialogs.tooltips
.subscribe(tooltip => {
if (tooltip.text) {
this.next(s => ({ ...s, tooltip }));
} else if (!this.snapshot.tooltip || tooltip.target === this.snapshot.tooltip.target) {
this.next(s => ({ ...s, tooltip: null }));
}
}));
}
public cancel() {

147
src/Squidex/app/framework/angular/modals/modal-target.directive.ts

@ -5,35 +5,37 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, Renderer2 } from '@angular/core';
import { timer } from 'rxjs';
import { ResourceOwner } from '@app/framework/internal';
const POSITION_TOPLEFT = 'topLeft';
const POSITION_TOPRIGHT = 'topRight';
const POSITION_BOTTOMLEFT = 'bottomLeft';
const POSITION_BOTTOMRIGHT = 'bottomRight';
const POSITION_LEFTTOP = 'leftTop';
const POSITION_LEFTBOTTOM = 'leftBottom';
const POSITION_RIGHTTOP = 'rightTop';
const POSITION_RIGHTBOTTOM = 'rightBottom';
const POSITION_FULL = 'full';
import { positionModal } from '@app/shared';
@Directive({
selector: '[sqxModalTarget]'
})
export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy, OnInit {
export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy {
private targetElement: any;
@Input('sqxModalTarget')
public target: any;
public set target(element: any) {
if (element !== this.targetElement) {
this.ngOnDestroy();
this.targetElement = element;
if (element) {
this.subscribe(element);
this.updatePosition();
}
}
}
@Input()
public offset = 2;
@Input()
public position = POSITION_BOTTOMRIGHT;
public position = 'bottom-right';
@Input()
public autoPosition = true;
@ -45,12 +47,9 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit
super();
}
public ngOnInit() {
if (this.target) {
this.targetElement = this.target;
private subscribe(element: any) {
this.own(
this.renderer.listen(this.targetElement, 'resize', () => {
this.renderer.listen(element, 'resize', () => {
this.updatePosition();
}));
@ -61,7 +60,6 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit
this.own(timer(100, 100).subscribe(() => this.updatePosition()));
}
}
public ngAfterViewInit() {
const modalRef = this.element.nativeElement;
@ -77,110 +75,35 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit
return;
}
const viewportHeight = document.documentElement!.clientHeight;
const viewportWidth = document.documentElement!.clientWidth;
const modalRef = this.element.nativeElement;
const modalRect = this.element.nativeElement.getBoundingClientRect();
const targetRect: ClientRect = this.targetElement.getBoundingClientRect();
const fix = this.autoPosition;
let y = 0;
let x = 0;
let t = 0;
let l = 0;
if (this.position === 'full') {
x = -this.offset + targetRect.left;
y = -this.offset + targetRect.top;
switch (this.position) {
case POSITION_LEFTTOP:
case POSITION_RIGHTTOP: {
t = targetRect.top;
break;
}
case POSITION_LEFTBOTTOM:
case POSITION_RIGHTBOTTOM: {
t = targetRect.bottom - modalRect.height;
break;
}
case POSITION_BOTTOMLEFT:
case POSITION_BOTTOMRIGHT: {
t = targetRect.bottom + this.offset;
if (fix && t + modalRect.height > viewportHeight) {
const candidate = targetRect.top - modalRect.height - this.offset;
if (candidate > 0) {
t = candidate;
}
}
break;
}
case POSITION_TOPLEFT:
case POSITION_TOPRIGHT: {
t = targetRect.top - modalRect.height - this.offset;
if (fix && t < 0) {
const candidate = targetRect.bottom + this.offset;
if (candidate + modalRect.height > viewportHeight) {
t = candidate;
}
}
break;
}
}
switch (this.position) {
case POSITION_TOPLEFT:
case POSITION_BOTTOMLEFT: {
l = targetRect.left;
break;
}
case POSITION_TOPRIGHT:
case POSITION_BOTTOMRIGHT: {
l = targetRect.right - modalRect.width;
break;
}
case POSITION_RIGHTTOP:
case POSITION_RIGHTBOTTOM: {
l = targetRect.right + this.offset;
if (fix && l + modalRect.width > viewportWidth) {
const candidate = targetRect.right - modalRect.width - this.offset;
if (candidate > 0) {
l = candidate;
}
}
break;
}
case POSITION_LEFTTOP:
case POSITION_LEFTBOTTOM: {
l = targetRect.left - modalRect.width - this.offset;
if (this.autoPosition && l < 0) {
const candidate = targetRect.right + this.offset;
if (candidate + modalRect.width > viewportWidth) {
l = candidate;
}
}
break;
}
}
if (this.position === POSITION_FULL) {
t = targetRect.top - this.offset;
l = targetRect.left - this.offset;
const w = targetRect.width + 2 * this.offset;
const h = targetRect.height + 2 * this.offset;
const w = 2 * this.offset + targetRect.width;
const h = 2 * this.offset + targetRect.height;
this.renderer.setStyle(modalRef, 'width', `${w}px`);
this.renderer.setStyle(modalRef, 'height', `${h}px`);
} else {
const viewH = document.documentElement!.clientHeight;
const viewW = document.documentElement!.clientWidth;
const position = positionModal(targetRect, modalRect, this.position, this.offset, this.autoPosition, viewW, viewH);
x = position.x;
y = position.y;
}
this.renderer.setStyle(modalRef, 'top', `${t}px`);
this.renderer.setStyle(modalRef, 'left', `${l}px`);
this.renderer.setStyle(modalRef, 'top', `${y}px`);
this.renderer.setStyle(modalRef, 'left', `${x}px`);
this.renderer.setStyle(modalRef, 'right', 'auto');
this.renderer.setStyle(modalRef, 'bottom', 'auto');
this.renderer.setStyle(modalRef, 'margin', '0');

3
src/Squidex/app/framework/angular/modals/tooltip.component.html

@ -1,3 +0,0 @@
<div class="tooltip-container" *sqxModalView="modal;onRoot:true;closeAuto:false" [sqxModalTarget]="target" [position]="position">
<ng-content></ng-content>
</div>

13
src/Squidex/app/framework/angular/modals/tooltip.component.scss

@ -1,13 +0,0 @@
@import '_vars';
@import '_mixins';
.tooltip-container {
@include border-radius;
background: $color-tooltip;
border: 0;
font-size: .9rem;
font-weight: normal;
white-space: nowrap;
color: $color-dark-foreground;
padding: .5rem;
}

56
src/Squidex/app/framework/angular/modals/tooltip.component.ts

@ -1,56 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, Renderer2 } from '@angular/core';
import {
fadeAnimation,
ModalModel,
ResourceOwner
} from '@app/framework/internal';
@Component({
selector: 'sqx-tooltip',
styleUrls: ['./tooltip.component.scss'],
templateUrl: './tooltip.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent extends ResourceOwner implements OnInit {
@Input()
public target: any;
@Input()
public position = 'topLeft';
public modal = new ModalModel();
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2
) {
super();
}
public ngOnInit() {
if (this.target) {
this.own(
this.renderer.listen(this.target, 'mouseenter', () => {
this.modal.show();
this.changeDetector.detectChanges();
}));
this.own(
this.renderer.listen(this.target, 'mouseleave', () => {
this.modal.hide();
}));
}
}
}

62
src/Squidex/app/framework/angular/modals/tooltip.directive.ts

@ -0,0 +1,62 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable:directive-selector
import { Directive, ElementRef, Input, OnInit, Renderer2 } from '@angular/core';
import { DialogService, ResourceOwner } from '@app/framework/internal';
import { Tooltip } from '@app/shared';
@Directive({
selector: '[title]'
})
export class TooltipDirective extends ResourceOwner implements OnInit {
private titleText: string;
@Input()
public titlePosition = 'top-right';
@Input()
public set title(value: string) {
this.titleText = value;
this.unsetAttribute();
}
constructor(
private readonly dialogs: DialogService,
private readonly element: ElementRef,
private readonly renderer: Renderer2
) {
super();
}
public ngOnInit() {
const target = this.element.nativeElement;
this.own(
this.renderer.listen(target, 'mouseenter', () => {
if (this.titleText) {
this.dialogs.tooltip(new Tooltip(target, this.titleText, this.titlePosition));
}
}));
this.own(
this.renderer.listen(this.element.nativeElement, 'mouseleave', () => {
this.dialogs.tooltip(new Tooltip(target, null, this.titlePosition));
}));
}
private unsetAttribute() {
try {
this.renderer.setAttribute(this.element.nativeElement, 'title', '');
} catch {
return;
}
}
}

2
src/Squidex/app/framework/declarations.ts

@ -35,7 +35,7 @@ export * from './angular/modals/modal-dialog.component';
export * from './angular/modals/modal-target.directive';
export * from './angular/modals/modal-view.directive';
export * from './angular/modals/onboarding-tooltip.component';
export * from './angular/modals/tooltip.component';
export * from './angular/modals/tooltip.directive';
export * from './angular/modals/root-view.component';
export * from './angular/pipes/colors.pipes';

1
src/Squidex/app/framework/internal.ts

@ -27,6 +27,7 @@ export * from './utils/interpolator';
export * from './utils/immutable-array';
export * from './utils/lazy';
export * from './utils/math-helper';
export * from './utils/modal-positioner';
export * from './utils/modal-view';
export * from './utils/pager';
export * from './utils/permission';

6
src/Squidex/app/framework/module.ts

@ -80,7 +80,7 @@ import {
TitleComponent,
TitleService,
ToggleComponent,
TooltipComponent,
TooltipDirective,
TransformInputDirective,
UserReportComponent
} from './declarations';
@ -148,7 +148,7 @@ import {
TemplateWrapperDirective,
TitleComponent,
ToggleComponent,
TooltipComponent,
TooltipDirective,
TransformInputDirective,
UserReportComponent
],
@ -212,7 +212,7 @@ import {
TemplateWrapperDirective,
TitleComponent,
ToggleComponent,
TooltipComponent,
TooltipDirective,
TransformInputDirective,
UserReportComponent
]

20
src/Squidex/app/framework/services/dialog.service.spec.ts

@ -9,7 +9,8 @@ import {
DialogRequest,
DialogService,
DialogServiceFactory,
Notification
Notification,
Tooltip
} from './dialog.service';
describe('DialogService', () => {
@ -66,8 +67,25 @@ describe('DialogService', () => {
expect(isNext).toBeTruthy();
});
it('should publish tooltip', () => {
const dialogService = new DialogService();
const tooltip = new Tooltip('target', 'text', 'topLeft');
let publishedTooltip: Tooltip;
dialogService.tooltips.subscribe(result => {
publishedTooltip = result;
});
dialogService.tooltip(tooltip);
expect(publishedTooltip!).toBe(tooltip);
});
it('should publish notification', () => {
const dialogService = new DialogService();
const notification = Notification.error('Message');
let publishedNotification: Notification;

18
src/Squidex/app/framework/services/dialog.service.ts

@ -34,6 +34,15 @@ export class DialogRequest {
}
}
export class Tooltip {
constructor(
public readonly target: any,
public readonly text: string | null,
public readonly position: string
) {
}
}
export class Notification {
constructor(
public readonly message: string,
@ -55,11 +64,16 @@ export class Notification {
export class DialogService {
private readonly requestStream$ = new Subject<DialogRequest>();
private readonly notificationsStream$ = new Subject<Notification>();
private readonly tooltipStream$ = new Subject<Tooltip>();
public get dialogs(): Observable<DialogRequest> {
return this.requestStream$;
}
public get tooltips(): Observable<Tooltip> {
return this.tooltipStream$;
}
public get notifications(): Observable<Notification> {
return this.notificationsStream$;
}
@ -82,6 +96,10 @@ export class DialogService {
this.notificationsStream$.next(notification);
}
public tooltip(tooltip: Tooltip) {
this.tooltipStream$.next(tooltip);
}
public confirm(title: string, text: string): Observable<boolean> {
const request = new DialogRequest(title, text);

85
src/Squidex/app/framework/utils/modal-positioner.spec.ts

@ -0,0 +1,85 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { positionModal } from './modal-positioner';
describe('position', () => {
function buildRect(x: number, y: number, w: number, h: number): ClientRect {
return {
top: y,
left: x,
right: x + w,
width: w,
height: h,
bottom: y + h
};
}
const targetRect = buildRect(200, 200, 100, 100);
const tests = [
{ position: 'top', x: 235, y: 160 },
{ position: 'top-left', x: 200, y: 160 },
{ position: 'top-right', x: 270, y: 160 },
{ position: 'bottom', x: 235, y: 310 },
{ position: 'bottom-left', x: 200, y: 310 },
{ position: 'bottom-right', x: 270, y: 310 },
{ position: 'left', x: 160, y: 235 },
{ position: 'left-top', x: 160, y: 200 },
{ position: 'left-bottom', x: 160, y: 270 },
{ position: 'right', x: 310, y: 235 },
{ position: 'right-top', x: 310, y: 200 },
{ position: 'right-bottom', x: 310, y: 270 }
];
for (let test of tests) {
const modalRect = buildRect(0, 0, 30, 30);
it(`should calculate modal position for ${test.position}`, () => {
const result = positionModal(targetRect, modalRect, test.position, 10, false, 0, 0);
expect(result.x).toBe(test.x);
expect(result.y).toBe(test.y);
});
}
it('should calculate modal position for vertical top fix', () => {
const modalRect = buildRect(0, 0, 30, 200);
const result = positionModal(targetRect, modalRect, 'top-left', 10, true, 600, 600);
expect(result.x).toBe(200);
expect(result.y).toBe(310);
});
it('should calculate modal position for vertical bottom fix', () => {
const modalRect = buildRect(0, 0, 30, 70);
const result = positionModal(targetRect, modalRect, 'bottom-left', 10, true, 350, 350);
expect(result.x).toBe(200);
expect(result.y).toBe(120);
});
it('should calculate modal position for horizontal left fix', () => {
const modalRect = buildRect(0, 0, 200, 30);
const result = positionModal(targetRect, modalRect, 'left-top', 10, true, 600, 600);
expect(result.x).toBe(310);
expect(result.y).toBe(200);
});
it('should calculate modal position for horizontal right fix', () => {
const modalRect = buildRect(0, 0, 70, 30);
const result = positionModal(targetRect, modalRect, 'right-top', 10, true, 350, 350);
expect(result.x).toBe(120);
expect(result.y).toBe(200);
});
});

116
src/Squidex/app/framework/utils/modal-positioner.ts

@ -0,0 +1,116 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
const POSITION_TOP_CENTER = 'top';
const POSITION_TOP_LEFT = 'top-left';
const POSITION_TOP_RIGHT = 'top-right';
const POSITION_BOTTOM_CENTER = 'bottom';
const POSITION_BOTTOM_LEFT = 'bottom-left';
const POSITION_BOTTOM_RIGHT = 'bottom-right';
const POSITION_LEFT_CENTER = 'left';
const POSITION_LEFT_TOP = 'left-top';
const POSITION_LEFT_BOTTOM = 'left-bottom';
const POSITION_RIGHT_CENTER = 'right';
const POSITION_RIGHT_TOP = 'right-top';
const POSITION_RIGHT_BOTTOM = 'right-bottom';
export function positionModal(targetRect: ClientRect, modalRect: ClientRect, relativePosition: string, offset: number, fix: boolean, viewportHeight: number, viewportWidth: number): { x: number, y: number } {
let y = 0;
let x = 0;
switch (relativePosition) {
case POSITION_LEFT_TOP:
case POSITION_RIGHT_TOP: {
y = targetRect.top;
break;
}
case POSITION_LEFT_BOTTOM:
case POSITION_RIGHT_BOTTOM: {
y = targetRect.bottom - modalRect.height;
break;
}
case POSITION_BOTTOM_CENTER:
case POSITION_BOTTOM_LEFT:
case POSITION_BOTTOM_RIGHT: {
y = targetRect.bottom + offset;
if (fix && y + modalRect.height > viewportHeight) {
const candidate = targetRect.top - modalRect.height - offset;
if (candidate > 0) {
y = candidate;
}
}
break;
}
case POSITION_TOP_CENTER:
case POSITION_TOP_LEFT:
case POSITION_TOP_RIGHT: {
y = targetRect.top - modalRect.height - offset;
if (fix && y < 0) {
const candidate = targetRect.bottom + offset;
if (candidate + modalRect.height < viewportHeight) {
y = candidate;
}
}
break;
}
case POSITION_LEFT_CENTER:
case POSITION_RIGHT_CENTER:
y = targetRect.top + targetRect.height * 0.5 - modalRect.height * 0.5;
break;
}
switch (relativePosition) {
case POSITION_TOP_LEFT:
case POSITION_BOTTOM_LEFT: {
x = targetRect.left;
break;
}
case POSITION_TOP_RIGHT:
case POSITION_BOTTOM_RIGHT: {
x = targetRect.right - modalRect.width;
break;
}
case POSITION_RIGHT_CENTER:
case POSITION_RIGHT_TOP:
case POSITION_RIGHT_BOTTOM: {
x = targetRect.right + offset;
if (fix && x + modalRect.width > viewportWidth) {
const candidate = targetRect.left - modalRect.width - offset;
if (candidate > 0) {
x = candidate;
}
}
break;
}
case POSITION_LEFT_CENTER:
case POSITION_LEFT_TOP:
case POSITION_LEFT_BOTTOM: {
x = targetRect.left - modalRect.width - offset;
if (fix && x < 0) {
const candidate = targetRect.right + offset;
if (candidate + modalRect.width < viewportWidth) {
x = candidate;
}
}
break;
}
case POSITION_TOP_CENTER:
case POSITION_BOTTOM_CENTER:
x = targetRect.left + targetRect.width * 0.5 - modalRect.width * 0.5;
break;
}
return { x, y };
}

2
src/Squidex/app/shared/components/asset.component.html

@ -113,7 +113,7 @@
<ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}}
</td>
<td class="col-user" *ngIf="!isCompact">
<img class="user-picture" [attr.title]="asset.lastModifiedBy | sqxUserNameRef" [attr.src]="asset.lastModifiedBy | sqxUserPictureRef" />
<img class="user-picture" title="{{asset.lastModifiedBy | sqxUserNameRef}}" [attr.src]="asset.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="col-actions text-right" *ngIf="!isCompact">
<a class="btn btn-text-secondary" [href]="asset | sqxAssetUrl" sqxExternalLink="noicon" (click)="$event.stopPropagation()">

4
src/Squidex/app/shared/components/comment.component.html

@ -1,11 +1,11 @@
<div class="comment row no-gutters">
<div class="col-auto">
<img class="user-picture" [attr.title]="comment.user | sqxUserNameRef:null" [attr.src]="comment.user | sqxUserPictureRef" />
<img class="user-picture" title="{{comment.user | sqxUserNameRef}}" [attr.src]="comment.user | sqxUserPictureRef" />
</div>
<div class="col pl-2">
<div class="comment-message">
<div class="user-row">
<div class="user-ref">{{comment.user | sqxUserNameRef:null}}</div>
<div class="user-ref">{{comment.user | sqxUserNameRef}}</div>
<button *ngIf="comment.user === userId" type="button" class="btn btn-sm btn-text-danger item-remove" (click)="deleting.emit()!">
<i class="icon-bin2"></i>

4
src/Squidex/app/shared/components/history-list.component.html

@ -1,11 +1,11 @@
<ng-container *ngIf="events">
<div *ngFor="let event of events; trackBy: trackByEvent" class="event row no-gutters">
<div class="col-auto">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
<img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [attr.src]="event.actor | sqxUserPictureRef" />
</div>
<div class="col pl-2">
<div class="event-message">
<span class="user-ref">{{event.actor | sqxUserNameRef:null}}</span>
<span class="user-ref">{{event.actor | sqxUserNameRef}}</span>
<span [innerHTML]="event | sqxHistoryMessage"></span>
</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>

4
src/Squidex/app/shared/components/language-selector.component.html

@ -1,11 +1,11 @@
<div class="btn-group btn-group-{{size}}" *ngIf="isSmallMode">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" [attr.title]="language.englishName" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)" tabindex="-1">
<button type="button" class="btn btn-secondary" *ngFor="let language of languages" title="{{language.englishName}}" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)" tabindex="-1">
<span class="iso-code">{{language.iso2Code}}</span>
</button>
</div>
<div class="dropdown-options btn-group btn-group-{{size}}" *ngIf="isLargeMode">
<button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle()" #button tabindex="-1">
<button type="button" class="btn btn-secondary dropdown-toggle" title="{{selectedLanguage.englishName}}" (click)="dropdown.toggle()" #button tabindex="-1">
{{selectedLanguage.iso2Code}}
</button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="button" @fade>

4
src/Squidex/app/shared/components/search-form.component.html

@ -10,7 +10,7 @@
<i class="icon-caret-down"></i>
</a>
<sqx-onboarding-tooltip helpId="contentArchive" [for]="expand" position="bottomRight" after="60000">
<sqx-onboarding-tooltip helpId="contentArchive" [for]="expand" position="bottom-right" after="60000">
Click this icon to show the advanced search menu and to show the archive!
</sqx-onboarding-tooltip>
</ng-container>
@ -30,7 +30,7 @@
</ng-container>
</form>
<sqx-onboarding-tooltip helpId="contentFind" [for]="inputFind" position="bottomRight" after="120000">
<sqx-onboarding-tooltip helpId="contentFind" [for]="inputFind" position="bottom-right" after="120000">
Search for content using full text search over all fields and languages!
</sqx-onboarding-tooltip>

1
src/Squidex/app/shared/internal.ts

@ -39,6 +39,7 @@ export * from './services/plans.service';
export * from './services/rules.service';
export * from './services/schemas.service';
export * from './services/schemas.types';
export * from './services/translations.service';
export * from './services/ui.service';
export * from './services/usages.service';
export * from './services/users-provider.service';

2
src/Squidex/app/shared/module.ts

@ -77,6 +77,7 @@ import {
SchemasService,
SchemasState,
SearchFormComponent,
TranslationsService,
UIService,
UIState,
UnsetAppGuard,
@ -207,6 +208,7 @@ export class SqxSharedModule {
SchemaMustNotBeSingletonGuard,
SchemasService,
SchemasState,
TranslationsService,
UIService,
UIState,
UnsetAppGuard,

14
src/Squidex/app/shared/services/news.service.ts

@ -10,11 +10,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import {
ApiUrlConfig,
HTTP,
pretifyError
} from '@app/framework';
import { ApiUrlConfig, pretifyError } from '@app/framework';
export class FeatureDto {
constructor(
@ -43,11 +39,9 @@ export class NewsService {
public getFeatures(version: number): Observable<FeaturesDto> {
const url = this.apiUrl.buildUrl(`api/news/features?version=${version}`);
return HTTP.getVersioned<any>(this.http, url).pipe(
return this.http.get<any>(url).pipe(
map(response => {
const body = response.payload.body;
const items: any[] = body.features;
const items: any[] = response.features;
return new FeaturesDto(
items.map(item => {
@ -56,7 +50,7 @@ export class NewsService {
item.text
);
}),
body.version
response.version
);
}),
pretifyError('Failed to load features. Please reload.'));

8
src/Squidex/app/shared/services/schemas.service.ts

@ -132,6 +132,14 @@ export class RootFieldDto extends FieldDto {
return this.properties.fieldType === 'Array';
}
public get isString() {
return this.properties.fieldType === 'String';
}
public get isTranslateable() {
return this.isLocalizable && this.isString && (this.properties.editor === 'Input' || this.properties.editor === 'Textarea');
}
constructor(fieldId: number, name: string, properties: FieldPropertiesDto,
public readonly partitioning: string,
isLocked: boolean = false,

57
src/Squidex/app/shared/services/translations.service.spec.ts

@ -0,0 +1,57 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import {
ApiUrlConfig,
TranslateDto,
TranslationDto,
TranslationsService
} from './../';
describe('TranslationsService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
TranslationsService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make post request to translate text',
inject([TranslationsService, HttpTestingController], (translationsService: TranslationsService, httpMock: HttpTestingController) => {
let translation: TranslationDto;
const request = new TranslateDto('Hello', 'en', 'de');
translationsService.translate('my-app', request).subscribe(result => {
translation = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/translations');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
text: 'Hallo', result: 'Translated'
});
expect(translation!).toEqual(new TranslationDto('Translated', 'Hallo'));
}));
});

49
src/Squidex/app/shared/services/translations.service.ts

@ -0,0 +1,49 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiUrlConfig, pretifyError } from '@app/framework';
export class TranslationDto {
constructor(
public readonly result: string,
public readonly text: string
) {
}
}
export class TranslateDto {
constructor(
public readonly text: string,
public readonly sourceLanguage: string,
public readonly targetLanguage: string
) {
}
}
@Injectable()
export class TranslationsService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public translate(appName: string, request: TranslateDto): Observable<TranslationDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/translations`);
return this.http.post<any>(url, request).pipe(
map(response => {
return new TranslationDto(response.result, response.text);
}),
pretifyError('Failed to translate text. Please reload.'));
}
}

7
src/Squidex/app/theme/icomoon/Read Me.txt

@ -0,0 +1,7 @@
Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.
To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/#docs/local-fonts
You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.
You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection.

8
src/Squidex/app/theme/icomoon/demo-files/demo.css

@ -150,18 +150,18 @@ p {
font-size: 24px;
}
.fs2 {
font-size: 28px;
font-size: 32px;
}
.fs3 {
font-size: 24px;
font-size: 32px;
}
.fs4 {
font-size: 20px;
}
.fs5 {
font-size: 32px;
font-size: 24px;
}
.fs6 {
font-size: 32px;
font-size: 28px;
}

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

1
src/Squidex/app/theme/icomoon/fonts/icomoon.svg

@ -118,6 +118,7 @@
<glyph unicode="&#xe96c;" glyph-name="caret-top" horiz-adv-x="585" d="M585.143 155.423c0-20.005-16.567-36.571-36.571-36.571h-512c-20.005 0-36.571 16.567-36.571 36.571 0 9.728 3.986 18.871 10.862 25.71l256 256c6.839 6.839 16.018 10.862 25.71 10.862s18.871-3.986 25.71-10.862l256-256c6.839-6.839 10.862-16.018 10.862-25.71zM585.143 484.577c0-20.005-16.567-36.571-36.571-36.571h-512c-20.005 0-36.571 16.567-36.571 36.571 0 9.728 3.986 18.871 10.862 25.71l256 256c6.839 6.839 16.018 10.862 25.71 10.862s18.871-3.986 25.71-10.862l256-256c6.839-6.839 10.862-16.018 10.862-25.71z" />
<glyph unicode="&#xe96d;" glyph-name="external-link" d="M768 426.667c-25.6 0-42.667-17.067-42.667-42.667v-256c0-25.6-17.067-42.667-42.667-42.667h-469.333c-25.6 0-42.667 17.067-42.667 42.667v469.333c0 25.6 17.067 42.667 42.667 42.667h256c25.6 0 42.667 17.067 42.667 42.667s-17.067 42.667-42.667 42.667h-256c-72.533 0-128-55.467-128-128v-469.333c0-72.533 55.467-128 128-128h469.333c72.533 0 128 55.467 128 128v256c0 25.6-17.067 42.667-42.667 42.667zM934.4 827.734c-4.267 8.533-12.8 17.067-21.333 21.333-4.267 4.267-12.8 4.267-17.067 4.267h-256c-25.6 0-42.667-17.067-42.667-42.667s17.067-42.667 42.667-42.667h153.6l-396.8-396.8c-17.067-17.067-17.067-42.667 0-59.733 8.533-8.533 17.067-12.8 29.867-12.8s21.333 4.267 29.867 12.8l396.8 396.8v-153.6c0-25.6 17.067-42.667 42.667-42.667s42.667 17.067 42.667 42.667v256c0 4.267 0 12.8-4.267 17.067z" />
<glyph unicode="&#xe96e;" glyph-name="arrow_back" d="M854 468.667v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" />
<glyph unicode="&#xe96f;" glyph-name="translate" d="M678 212.667h138l-70 186zM790 512.667l192-512h-86l-48 128h-202l-48-128h-86l192 512h86zM550 296.667l-34-88-132 132-214-212-60 60 218 214c-54 60-96 124-128 194h86c26-50 58-98 98-142 62 68 108 146 136 228h-478v86h300v84h84v-84h300v-86h-126c-32-100-84-196-158-278l-2-2z" />
<glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" />
<glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" />

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
src/Squidex/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

2
src/Squidex/app/theme/icomoon/selection.json

File diff suppressed because one or more lines are too long

391
src/Squidex/app/theme/icomoon/style.css

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?rs9se1');
src: url('fonts/icomoon.eot?rs9se1#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?rs9se1') format('truetype'),
url('fonts/icomoon.woff?rs9se1') format('woff'),
url('fonts/icomoon.svg?rs9se1#icomoon') format('svg');
src: url('fonts/icomoon.eot?dvmkhg');
src: url('fonts/icomoon.eot?dvmkhg#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?dvmkhg') format('truetype'),
url('fonts/icomoon.woff?dvmkhg') format('woff'),
url('fonts/icomoon.svg?dvmkhg#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-translate:before {
content: "\e96f";
}
.icon-arrow_back:before {
content: "\e96e";
}
@ -54,195 +57,6 @@
.icon-download:before {
content: "\e93e";
}
.icon-clone:before {
content: "\e96a";
}
.icon-control-Tags:before {
content: "\e963";
}
.icon-control-Checkboxes:before {
content: "\e962";
}
.icon-control-Html:before {
content: "\e960";
}
.icon-single-content:before {
content: "\e958";
}
.icon-multiple-content:before {
content: "\e957";
}
.icon-type-Array:before {
content: "\e956";
}
.icon-exclamation:before {
content: "\e955";
}
.icon-orleans:before {
content: "\e94b";
}
.icon-document-lock:before {
content: "\e949";
}
.icon-document-unpublish:before {
content: "\e93f";
}
.icon-angle-down:before {
content: "\e900";
}
.icon-angle-left:before {
content: "\e901";
}
.icon-angle-right:before {
content: "\e931";
}
.icon-angle-up:before {
content: "\e903";
}
.icon-api:before {
content: "\e945";
}
.icon-assets:before {
content: "\e948";
}
.icon-bug:before {
content: "\e93d";
}
.icon-caret-down:before {
content: "\e92c";
}
.icon-caret-left:before {
content: "\e92a";
}
.icon-caret-right:before {
content: "\e929";
}
.icon-caret-up:before {
content: "\e92b";
}
.icon-contents:before {
content: "\e946";
}
.icon-trigger-ContentChanged:before {
content: "\e946";
}
.icon-control-Date:before {
content: "\e936";
}
.icon-control-DateTime:before {
content: "\e937";
}
.icon-control-Markdown:before {
content: "\e938";
}
.icon-grid:before {
content: "\f00a";
}
.icon-list1:before {
content: "\f0c9";
}
.icon-user-o:before {
content: "\e932";
}
.icon-rules:before {
content: "\e947";
}
.icon-caret-bottom:before {
content: "\e96b";
}
.icon-caret-top:before {
content: "\e96c";
}
.icon-show:before {
content: "\e964";
}
.icon-show-all:before {
content: "\e965";
}
.icon-hide:before {
content: "\e966";
}
.icon-hide-all:before {
content: "\e967";
}
.icon-spinner2:before {
content: "\e959";
}
.icon-star-full:before {
content: "\e95d";
}
.icon-star-empty:before {
content: "\e95e";
}
.icon-twitter:before {
content: "\e95c";
}
.icon-hour-glass:before {
content: "\e954";
}
.icon-spinner:before {
content: "\e953";
}
.icon-clock:before {
content: "\e950";
}
.icon-bin2:before {
content: "\e902";
}
.icon-earth:before {
content: "\e9ca";
}
.icon-elapsed:before {
content: "\e943";
}
.icon-google:before {
content: "\e93b";
}
.icon-lock:before {
content: "\e934";
}
.icon-microsoft:before {
content: "\e940";
}
.icon-pause:before {
content: "\e92f";
}
.icon-play:before {
content: "\e930";
}
.icon-reset:before {
content: "\e92e";
}
.icon-settings2:before {
content: "\e92d";
}
.icon-timeout:before {
content: "\e944";
}
.icon-unlocked:before {
content: "\e933";
}
.icon-grid1:before {
content: "\e952";
}
.icon-list:before {
content: "\e94e";
}
.icon-info:before {
content: "\e93c";
}
.icon-control-Color:before {
content: "\e94d";
}
.icon-browser:before {
content: "\e935";
}
.icon-checkmark:before {
content: "\e942";
}
.icon-control-Stars:before {
content: "\e93a";
}
.icon-prerender:before {
content: "\e94c";
}
@ -399,3 +213,192 @@
.icon-user:before {
content: "\e928";
}
.icon-control-Color:before {
content: "\e94d";
}
.icon-browser:before {
content: "\e935";
}
.icon-checkmark:before {
content: "\e942";
}
.icon-control-Stars:before {
content: "\e93a";
}
.icon-grid1:before {
content: "\e952";
}
.icon-list:before {
content: "\e94e";
}
.icon-info:before {
content: "\e93c";
}
.icon-caret-bottom:before {
content: "\e96b";
}
.icon-caret-top:before {
content: "\e96c";
}
.icon-show:before {
content: "\e964";
}
.icon-show-all:before {
content: "\e965";
}
.icon-hide:before {
content: "\e966";
}
.icon-hide-all:before {
content: "\e967";
}
.icon-spinner2:before {
content: "\e959";
}
.icon-star-full:before {
content: "\e95d";
}
.icon-star-empty:before {
content: "\e95e";
}
.icon-twitter:before {
content: "\e95c";
}
.icon-hour-glass:before {
content: "\e954";
}
.icon-spinner:before {
content: "\e953";
}
.icon-clock:before {
content: "\e950";
}
.icon-bin2:before {
content: "\e902";
}
.icon-earth:before {
content: "\e9ca";
}
.icon-elapsed:before {
content: "\e943";
}
.icon-google:before {
content: "\e93b";
}
.icon-lock:before {
content: "\e934";
}
.icon-microsoft:before {
content: "\e940";
}
.icon-pause:before {
content: "\e92f";
}
.icon-play:before {
content: "\e930";
}
.icon-reset:before {
content: "\e92e";
}
.icon-settings2:before {
content: "\e92d";
}
.icon-timeout:before {
content: "\e944";
}
.icon-unlocked:before {
content: "\e933";
}
.icon-clone:before {
content: "\e96a";
}
.icon-control-Tags:before {
content: "\e963";
}
.icon-control-Checkboxes:before {
content: "\e962";
}
.icon-control-Html:before {
content: "\e960";
}
.icon-single-content:before {
content: "\e958";
}
.icon-multiple-content:before {
content: "\e957";
}
.icon-type-Array:before {
content: "\e956";
}
.icon-exclamation:before {
content: "\e955";
}
.icon-orleans:before {
content: "\e94b";
}
.icon-document-lock:before {
content: "\e949";
}
.icon-document-unpublish:before {
content: "\e93f";
}
.icon-angle-down:before {
content: "\e900";
}
.icon-angle-left:before {
content: "\e901";
}
.icon-angle-right:before {
content: "\e931";
}
.icon-angle-up:before {
content: "\e903";
}
.icon-api:before {
content: "\e945";
}
.icon-assets:before {
content: "\e948";
}
.icon-bug:before {
content: "\e93d";
}
.icon-caret-down:before {
content: "\e92c";
}
.icon-caret-left:before {
content: "\e92a";
}
.icon-caret-right:before {
content: "\e929";
}
.icon-caret-up:before {
content: "\e92b";
}
.icon-contents:before {
content: "\e946";
}
.icon-trigger-ContentChanged:before {
content: "\e946";
}
.icon-control-Date:before {
content: "\e936";
}
.icon-control-DateTime:before {
content: "\e937";
}
.icon-control-Markdown:before {
content: "\e938";
}
.icon-grid:before {
content: "\f00a";
}
.icon-list1:before {
content: "\f0c9";
}
.icon-user-o:before {
content: "\e932";
}
.icon-rules:before {
content: "\e947";
}

7
src/Squidex/appsettings.json

@ -334,5 +334,12 @@
*/
"clientId": "squidex-website:default",
"clientSecret": "QGgqxd7bDHBTEkpC6fj8sbdPWgZrPrPfr3xzb3LKoec="
},
"translations": {
/*
* The deepl api key if you want to support automated translations.
*/
"deeplAuthKey": ""
}
}

Loading…
Cancel
Save