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. 10
      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. 6
      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. 10
      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. 163
      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 public sealed class FeaturesService
{ {
private const int FeatureVersion = 1; private const int FeatureVersion = 2;
private static readonly QueryContext Flatten = QueryContext.Default.Flatten(); private static readonly QueryContext Flatten = QueryContext.Default.Flatten();
private readonly SquidexClient<NewsEntity, FeatureDto> client; 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> /// <summary>
/// Get schemas. /// Get schemas.
/// </summary> /// </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> /// <returns>
/// 200 => Schemas returned. /// 200 => Schemas returned.
/// 404 => App not found. /// 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.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NodaTime; using NodaTime;
using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Areas.Api.Controllers.News.Service;
@ -18,6 +19,8 @@ using Squidex.Domain.Users;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
using Squidex.Shared.Users; using Squidex.Shared.Users;
@ -27,8 +30,21 @@ namespace Squidex.Config.Domain
{ {
public static class InfrastructureServices 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() services.AddHealthChecks()
.AddCheck<GCHealthCheck>("GC", tags: new[] { "node" }) .AddCheck<GCHealthCheck>("GC", tags: new[] { "node" })
.AddCheck<OrleansHealthCheck>("Orleans", tags: new[] { "cluster" }) .AddCheck<OrleansHealthCheck>("Orleans", tags: new[] { "cluster" })

2
src/Squidex/WebStartup.cs

@ -58,7 +58,7 @@ namespace Squidex
services.AddMyEventPublishersServices(config); services.AddMyEventPublishersServices(config);
services.AddMyEventStoreServices(config); services.AddMyEventStoreServices(config);
services.AddMyIdentityServer(); services.AddMyIdentityServer();
services.AddMyInfrastructureServices(); services.AddMyInfrastructureServices(config);
services.AddMyLoggingServices(config); services.AddMyLoggingServices(config);
services.AddMyMigrationServices(); services.AddMyMigrationServices();
services.AddMyMvc(); 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"> <tbody *ngFor="let userInfo of usersState.users | async; trackBy: trackByUser">
<tr [routerLink]="userInfo.user.id" routerLinkActive="active"> <tr [routerLink]="userInfo.user.id" routerLinkActive="active">
<td class="cell-user"> <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>
<td class="cell-auto"> <td class="cell-auto">
<span class="user-name table-cell">{{userInfo.user.displayName}}</span> <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> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="filtersLink">Filters</sqx-tooltip> <a class="panel-link" routerLink="filters" routerLinkActive="active" title="Filters" titlePosition="left">
<a class="panel-link" routerLink="filters" routerLinkActive="active" #filtersLink>
<i class="icon-filter"></i> <i class="icon-filter"></i>
</a> </a>
</div> </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.col-12]="!fieldFormCompare" [class.col-6]="fieldFormCompare">
<div class="table-items-row" [class.field-invalid]="isInvalid | async"> <div class="table-items-row" [class.field-invalid]="isInvalid | async">
<div class="languages-buttons"> <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 <sqx-field-languages
[field]="field" [field]="field"
[language]="language" [language]="language"

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

@ -12,12 +12,15 @@ import { combineLatest } from 'rxjs/operators';
import { import {
AppLanguageDto, AppLanguageDto,
AppsState,
EditContentForm, EditContentForm,
fieldInvariant, fieldInvariant,
invalid$, invalid$,
LocalStoreService, LocalStoreService,
RootFieldDto, RootFieldDto,
SchemaDto, SchemaDto,
TranslateDto,
TranslationsService,
Types, Types,
value$ value$
} from '@app/shared'; } from '@app/shared';
@ -59,17 +62,28 @@ export class ContentFieldComponent implements OnChanges {
public isInvalid: Observable<boolean>; public isInvalid: Observable<boolean>;
public isDifferent: Observable<boolean>; public isDifferent: Observable<boolean>;
public isTranslateable: boolean;
constructor( constructor(
private readonly localStore: LocalStoreService private readonly appsState: AppsState,
private readonly localStore: LocalStoreService,
private readonly translations: TranslationsService
) { ) {
} }
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['field']) {
this.showAllControls = this.localStore.getBoolean(this.configKey());
}
if (changes['fieldForm']) { if (changes['fieldForm']) {
this.isInvalid = invalid$(this.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) { if ((changes['fieldForm'] || changes['fieldFormCompare']) && this.fieldFormCompare) {
this.isDifferent = this.isDifferent =
value$(this.fieldForm).pipe( value$(this.fieldForm).pipe(
@ -77,10 +91,6 @@ export class ContentFieldComponent implements OnChanges {
(lhs, rhs) => !Types.jsJsonEquals(lhs, rhs))); (lhs, rhs) => !Types.jsJsonEquals(lhs, rhs)));
} }
if (changes['field']) {
this.showAllControls = this.localStore.getBoolean(this.configKey());
}
const control = this.findControl(this.fieldForm); const control = this.findControl(this.fieldForm);
if (this.selectedFormControl !== control) { 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) { private findControl(form: FormGroup) {
if (this.field.isLocalizable) { if (this.field.isLocalizable) {
return form.controls[this.language.iso2Code]; return form.controls[this.language.iso2Code];
@ -129,7 +179,7 @@ export class ContentFieldComponent implements OnChanges {
} }
public prefix(language: AppLanguageDto) { public prefix(language: AppLanguageDto) {
return `(${language.iso2Code}`; return `(${language.iso2Code})`;
} }
public trackByLanguage(index: number, language: AppLanguageDto) { 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> <ng-container content>
<div *ngFor="let event of events | async; trackBy: trackByEvent" class="event row no-gutters"> <div *ngFor="let event of events | async; trackBy: trackByEvent" class="event row no-gutters">
<div class="col-auto"> <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>
<div class="col pl-2"> <div class="col pl-2">
<div class="event-message"> <div class="event-message">

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

@ -126,19 +126,15 @@
<ng-container sidebar> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">commentsLink</sqx-tooltip> <a class="panel-link" routerLink="comments" routerLinkActive="active" title="Comments" titlePosition="left">
<a class="panel-link" routerLink="comments" routerLinkActive="active" #commentsLink>
<i class="icon-comments"></i> <i class="icon-comments"></i>
</a> </a>
<sqx-tooltip position="leftTop" [target]="historyLink">History</sqx-tooltip> <a class="panel-link" routerLink="history" routerLinkActive="active" title="History" titlePosition="left" #historyLink>
<a class="panel-link" routerLink="history" routerLinkActive="active" #historyLink>
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </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. The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</div> </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"> [languages]="languages">
</sqx-language-selector> </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. Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>

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

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

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

@ -32,7 +32,7 @@
</td> </td>
<td class="cell-user" *ngIf="!isCompact && patchForm.form.pristine" (click)="shouldStop($event)"> <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> </td>
<ng-container *ngIf="patchForm.form.dirty"> <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"> <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> <i class="icon-circle"></i>
</span> </span>
<sqx-tooltip [target]="statusIcon">{{displayStatus}}</sqx-tooltip>
</ng-container> </ng-container>
<ng-container *ngIf="scheduledTo"> <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> <i class="icon-clock"></i>
</span> </span>
<sqx-tooltip position="topRight" [target]="statusIcon">Will be set to '{{scheduledTo}}' at {{scheduledAt | sqxFullDateTime}}</sqx-tooltip>
</ng-container> </ng-container>
<span class="content-status-label" *ngIf="showLabel">{{displayStatus}}</span> <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 alignMiddle = true;
public get displayStatus() { 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> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="historyLink">Events</sqx-tooltip> <a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" title="History" titlePosition="left">
<a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" #historyLink>
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip> <a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left" #helpLink>
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<i class="icon-help"></i> <i class="icon-help"></i>
</a> </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. 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> </sqx-onboarding-tooltip>
</div> </div>

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

@ -7,7 +7,7 @@
<span class="field-name"> <span class="field-name">
<i class="field-icon icon-type-{{field.properties.fieldType}}"></i> <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 class="field-partitioning ml-2" *ngIf="field['isLocalizable']">localizable</span>
</span> </span>
</div> </div>

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

@ -48,11 +48,11 @@
</div> </div>
</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. Open the context menu to delete the schema or to create some scripts for content changes.
</sqx-onboarding-tooltip> </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. Note, that you have to publish the schema before you can add content to it.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>
@ -82,10 +82,8 @@
</ng-container> </ng-container>
<ng-container sidebar> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<sqx-tooltip position="leftTop" [target]="helpLink">Help</sqx-tooltip> <a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left">
<a class="panel-link" routerLink="help" routerLinkActive="active" #helpLink>
<i class="icon-help"></i> <i class="icon-help"></i>
</a> </a>
</div> </div>

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -5,7 +5,7 @@
autocorrect="off" autocorrect="off"
autocapitalize="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" <div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex" [class.active]="i === snapshot.suggestedIndex"
[container]="container" [container]="container"

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

@ -23,7 +23,7 @@
spellcheck="false"> spellcheck="false">
</div> </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" <div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex" [class.active]="i === snapshot.suggestedIndex"
[container]="container" [container]="container"

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

@ -15,10 +15,16 @@
</ng-container> </ng-container>
</sqx-modal-dialog> </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> <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> <button type="button" class="close" data-dismiss="alert" (closed)="close(notification)">&times;</button>
<span [innerHTML]="notification.message"></span> <span [innerHTML]="notification.message"></span>
</div> </div>
</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 '_mixins';
@import '_vars'; @import '_vars';
// sass-lint:disable single-line-per-selector
.notification-container { .notification-container {
& { & {
margin: .625rem; margin: .625rem;
@ -14,19 +16,65 @@
max-height: 20rem; max-height: 20rem;
} }
&-topright { &-top-right {
@include fixed(0, 0, auto, auto); @include fixed(0, 0, auto, auto);
} }
&-topleft { &-top-left {
@include fixed(0, auto, auto, 0); @include fixed(0, auto, auto, 0);
} }
&-bottomright { &-bottom-right {
@include fixed(auto, 0, 0, auto); @include fixed(auto, 0, 0, auto);
} }
&-bottomleft { &-bottom-left {
@include fixed(auto, auto, 0, 0); @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 * 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 { timer } from 'rxjs';
import { import {
@ -16,11 +16,14 @@ import {
Notification, Notification,
StatefulComponent StatefulComponent
} from '@app/framework/internal'; } from '@app/framework/internal';
import { Tooltip } from '@app/shared';
interface State { interface State {
dialogRequest?: DialogRequest | null; dialogRequest?: DialogRequest | null;
notifications: Notification[]; notifications: Notification[];
tooltip?: Tooltip | null;
} }
@Component({ @Component({
@ -33,9 +36,6 @@ interface State {
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DialogRendererComponent extends StatefulComponent<State> implements OnInit { export class DialogRendererComponent extends StatefulComponent<State> implements OnInit {
@Input()
public position = 'bottomright';
public dialogView = new DialogModel(); public dialogView = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
@ -75,6 +75,16 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
this.next(s => ({ ...s, dialogRequest })); 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() { public cancel() {

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

@ -5,35 +5,37 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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 { timer } from 'rxjs';
import { ResourceOwner } from '@app/framework/internal'; import { ResourceOwner } from '@app/framework/internal';
import { positionModal } from '@app/shared';
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';
@Directive({ @Directive({
selector: '[sqxModalTarget]' selector: '[sqxModalTarget]'
}) })
export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy, OnInit { export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy {
private targetElement: any; private targetElement: any;
@Input('sqxModalTarget') @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() @Input()
public offset = 2; public offset = 2;
@Input() @Input()
public position = POSITION_BOTTOMRIGHT; public position = 'bottom-right';
@Input() @Input()
public autoPosition = true; public autoPosition = true;
@ -45,22 +47,18 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit
super(); super();
} }
public ngOnInit() { private subscribe(element: any) {
if (this.target) { this.own(
this.targetElement = this.target; this.renderer.listen(element, 'resize', () => {
this.updatePosition();
}));
this.own( this.own(
this.renderer.listen(this.targetElement, 'resize', () => { this.renderer.listen(this.element.nativeElement, 'resize', () => {
this.updatePosition(); this.updatePosition();
})); }));
this.own( this.own(timer(100, 100).subscribe(() => this.updatePosition()));
this.renderer.listen(this.element.nativeElement, 'resize', () => {
this.updatePosition();
}));
this.own(timer(100, 100).subscribe(() => this.updatePosition()));
}
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -77,110 +75,35 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit
return; return;
} }
const viewportHeight = document.documentElement!.clientHeight;
const viewportWidth = document.documentElement!.clientWidth;
const modalRef = this.element.nativeElement; const modalRef = this.element.nativeElement;
const modalRect = this.element.nativeElement.getBoundingClientRect(); const modalRect = this.element.nativeElement.getBoundingClientRect();
const targetRect: ClientRect = this.targetElement.getBoundingClientRect(); const targetRect: ClientRect = this.targetElement.getBoundingClientRect();
const fix = this.autoPosition; let y = 0;
let x = 0;
let t = 0;
let l = 0;
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) { if (this.position === 'full') {
t = targetRect.top - this.offset; x = -this.offset + targetRect.left;
l = targetRect.left - this.offset; y = -this.offset + targetRect.top;
const w = targetRect.width + 2 * this.offset; const w = 2 * this.offset + targetRect.width;
const h = targetRect.height + 2 * this.offset; const h = 2 * this.offset + targetRect.height;
this.renderer.setStyle(modalRef, 'width', `${w}px`); this.renderer.setStyle(modalRef, 'width', `${w}px`);
this.renderer.setStyle(modalRef, 'height', `${h}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, 'top', `${y}px`);
this.renderer.setStyle(modalRef, 'left', `${l}px`); this.renderer.setStyle(modalRef, 'left', `${x}px`);
this.renderer.setStyle(modalRef, 'right', 'auto'); this.renderer.setStyle(modalRef, 'right', 'auto');
this.renderer.setStyle(modalRef, 'bottom', 'auto'); this.renderer.setStyle(modalRef, 'bottom', 'auto');
this.renderer.setStyle(modalRef, 'margin', '0'); 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-target.directive';
export * from './angular/modals/modal-view.directive'; export * from './angular/modals/modal-view.directive';
export * from './angular/modals/onboarding-tooltip.component'; 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/modals/root-view.component';
export * from './angular/pipes/colors.pipes'; 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/immutable-array';
export * from './utils/lazy'; export * from './utils/lazy';
export * from './utils/math-helper'; export * from './utils/math-helper';
export * from './utils/modal-positioner';
export * from './utils/modal-view'; export * from './utils/modal-view';
export * from './utils/pager'; export * from './utils/pager';
export * from './utils/permission'; export * from './utils/permission';

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

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

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

@ -9,7 +9,8 @@ import {
DialogRequest, DialogRequest,
DialogService, DialogService,
DialogServiceFactory, DialogServiceFactory,
Notification Notification,
Tooltip
} from './dialog.service'; } from './dialog.service';
describe('DialogService', () => { describe('DialogService', () => {
@ -66,8 +67,25 @@ describe('DialogService', () => {
expect(isNext).toBeTruthy(); 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', () => { it('should publish notification', () => {
const dialogService = new DialogService(); const dialogService = new DialogService();
const notification = Notification.error('Message'); const notification = Notification.error('Message');
let publishedNotification: Notification; 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 { export class Notification {
constructor( constructor(
public readonly message: string, public readonly message: string,
@ -55,11 +64,16 @@ export class Notification {
export class DialogService { export class DialogService {
private readonly requestStream$ = new Subject<DialogRequest>(); private readonly requestStream$ = new Subject<DialogRequest>();
private readonly notificationsStream$ = new Subject<Notification>(); private readonly notificationsStream$ = new Subject<Notification>();
private readonly tooltipStream$ = new Subject<Tooltip>();
public get dialogs(): Observable<DialogRequest> { public get dialogs(): Observable<DialogRequest> {
return this.requestStream$; return this.requestStream$;
} }
public get tooltips(): Observable<Tooltip> {
return this.tooltipStream$;
}
public get notifications(): Observable<Notification> { public get notifications(): Observable<Notification> {
return this.notificationsStream$; return this.notificationsStream$;
} }
@ -82,6 +96,10 @@ export class DialogService {
this.notificationsStream$.next(notification); this.notificationsStream$.next(notification);
} }
public tooltip(tooltip: Tooltip) {
this.tooltipStream$.next(tooltip);
}
public confirm(title: string, text: string): Observable<boolean> { public confirm(title: string, text: string): Observable<boolean> {
const request = new DialogRequest(title, text); 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}} <ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}}
</td> </td>
<td class="col-user" *ngIf="!isCompact"> <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>
<td class="col-actions text-right" *ngIf="!isCompact"> <td class="col-actions text-right" *ngIf="!isCompact">
<a class="btn btn-text-secondary" [href]="asset | sqxAssetUrl" sqxExternalLink="noicon" (click)="$event.stopPropagation()"> <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="comment row no-gutters">
<div class="col-auto"> <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>
<div class="col pl-2"> <div class="col pl-2">
<div class="comment-message"> <div class="comment-message">
<div class="user-row"> <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()!"> <button *ngIf="comment.user === userId" type="button" class="btn btn-sm btn-text-danger item-remove" (click)="deleting.emit()!">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>

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

@ -1,11 +1,11 @@
<ng-container *ngIf="events"> <ng-container *ngIf="events">
<div *ngFor="let event of events; trackBy: trackByEvent" class="event row no-gutters"> <div *ngFor="let event of events; trackBy: trackByEvent" class="event row no-gutters">
<div class="col-auto"> <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>
<div class="col pl-2"> <div class="col pl-2">
<div class="event-message"> <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> <span [innerHTML]="event | sqxHistoryMessage"></span>
</div> </div>
<div class="event-created">{{event.created | sqxFromNow}}</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"> <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> <span class="iso-code">{{language.iso2Code}}</span>
</button> </button>
</div> </div>
<div class="dropdown-options btn-group btn-group-{{size}}" *ngIf="isLargeMode"> <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}} {{selectedLanguage.iso2Code}}
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="button" @fade> <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> <i class="icon-caret-down"></i>
</a> </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! Click this icon to show the advanced search menu and to show the archive!
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>
@ -30,7 +30,7 @@
</ng-container> </ng-container>
</form> </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! Search for content using full text search over all fields and languages!
</sqx-onboarding-tooltip> </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/rules.service';
export * from './services/schemas.service'; export * from './services/schemas.service';
export * from './services/schemas.types'; export * from './services/schemas.types';
export * from './services/translations.service';
export * from './services/ui.service'; export * from './services/ui.service';
export * from './services/usages.service'; export * from './services/usages.service';
export * from './services/users-provider.service'; export * from './services/users-provider.service';

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

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

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

@ -10,11 +10,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { import { ApiUrlConfig, pretifyError } from '@app/framework';
ApiUrlConfig,
HTTP,
pretifyError
} from '@app/framework';
export class FeatureDto { export class FeatureDto {
constructor( constructor(
@ -43,11 +39,9 @@ export class NewsService {
public getFeatures(version: number): Observable<FeaturesDto> { public getFeatures(version: number): Observable<FeaturesDto> {
const url = this.apiUrl.buildUrl(`api/news/features?version=${version}`); 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 => { map(response => {
const body = response.payload.body; const items: any[] = response.features;
const items: any[] = body.features;
return new FeaturesDto( return new FeaturesDto(
items.map(item => { items.map(item => {
@ -56,7 +50,7 @@ export class NewsService {
item.text item.text
); );
}), }),
body.version response.version
); );
}), }),
pretifyError('Failed to load features. Please reload.')); 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'; 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, constructor(fieldId: number, name: string, properties: FieldPropertiesDto,
public readonly partitioning: string, public readonly partitioning: string,
isLocked: boolean = false, 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; font-size: 24px;
} }
.fs2 { .fs2 {
font-size: 28px; font-size: 32px;
} }
.fs3 { .fs3 {
font-size: 24px; font-size: 32px;
} }
.fs4 { .fs4 {
font-size: 20px; font-size: 20px;
} }
.fs5 { .fs5 {
font-size: 32px; font-size: 24px;
} }
.fs6 { .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="&#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="&#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="&#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="&#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="&#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" /> <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-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('fonts/icomoon.eot?rs9se1'); src: url('fonts/icomoon.eot?dvmkhg');
src: url('fonts/icomoon.eot?rs9se1#iefix') format('embedded-opentype'), src: url('fonts/icomoon.eot?dvmkhg#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?rs9se1') format('truetype'), url('fonts/icomoon.ttf?dvmkhg') format('truetype'),
url('fonts/icomoon.woff?rs9se1') format('woff'), url('fonts/icomoon.woff?dvmkhg') format('woff'),
url('fonts/icomoon.svg?rs9se1#icomoon') format('svg'); url('fonts/icomoon.svg?dvmkhg#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-translate:before {
content: "\e96f";
}
.icon-arrow_back:before { .icon-arrow_back:before {
content: "\e96e"; content: "\e96e";
} }
@ -54,195 +57,6 @@
.icon-download:before { .icon-download:before {
content: "\e93e"; 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 { .icon-prerender:before {
content: "\e94c"; content: "\e94c";
} }
@ -399,3 +213,192 @@
.icon-user:before { .icon-user:before {
content: "\e928"; 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", "clientId": "squidex-website:default",
"clientSecret": "QGgqxd7bDHBTEkpC6fj8sbdPWgZrPrPfr3xzb3LKoec=" "clientSecret": "QGgqxd7bDHBTEkpC6fj8sbdPWgZrPrPfr3xzb3LKoec="
},
"translations": {
/*
* The deepl api key if you want to support automated translations.
*/
"deeplAuthKey": ""
} }
} }

Loading…
Cancel
Save