Browse Source

Merge branch 'release/4.x'

# Conflicts:
#	backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
#	backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
#	backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs
pull/590/head
Sebastian 5 years ago
parent
commit
91cba54bc6
  1. 1
      .drone.yml
  2. 4
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
  3. 17
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
  4. 9
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
  5. 43
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  6. 32
      backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs
  7. 12
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  8. 1
      backend/i18n/frontend_en.json
  9. 123
      backend/i18n/frontend_it.json
  10. 3
      backend/i18n/frontend_nl.json
  11. 4
      backend/i18n/source/backend_en.json
  12. 75
      backend/i18n/source/backend_it.json
  13. 4
      backend/i18n/source/backend_nl.json
  14. 1
      backend/i18n/source/frontend_en.json
  15. 89
      backend/i18n/source/frontend_it.json
  16. 2
      backend/i18n/source/frontend_nl.json
  17. 9
      backend/i18n/translate.bat
  18. 10
      backend/i18n/translate.sh
  19. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowsConverter.cs
  20. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  21. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs
  22. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  23. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  24. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
  25. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  26. 11
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  27. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  28. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  29. 10
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  30. 49
      backend/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs
  31. 4
      backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  32. 2
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  33. 2
      backend/src/Squidex.Infrastructure.Amazon/Squidex.Infrastructure.Amazon.csproj
  34. 6
      backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
  35. 2
      backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
  36. 2
      backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj
  37. 4
      backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  38. 2
      backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
  39. 38
      backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs
  40. 2
      backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs
  41. 12
      backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs
  42. 26
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  43. 108
      backend/src/Squidex.Shared/Texts.it.resx
  44. 4
      backend/src/Squidex.Shared/Texts.nl.resx
  45. 4
      backend/src/Squidex.Shared/Texts.resx
  46. 2
      backend/src/Squidex.Web/Squidex.Web.csproj
  47. 16
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  48. 2
      backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs
  49. 9
      backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
  50. 9
      backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs
  51. 2
      backend/src/Squidex/Config/Domain/SerializationServices.cs
  52. 3
      backend/src/Squidex/Config/Domain/StoreServices.cs
  53. 34
      backend/src/Squidex/Squidex.csproj
  54. 6
      backend/src/Squidex/appsettings.json
  55. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs
  56. 23
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs
  57. 17
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs
  58. 153
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs
  59. 10
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  60. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs
  61. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  62. 6
      backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
  63. 6
      backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs
  64. 14
      backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  65. 10
      backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
  66. 3
      frontend/app/features/administration/pages/event-consumers/event-consumer.component.html
  67. 3
      frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  68. 2
      frontend/app/features/administration/services/event-consumers.service.spec.ts
  69. 2
      frontend/app/features/administration/services/event-consumers.service.ts
  70. 6
      frontend/app/features/content/pages/content/content-history-page.component.ts
  71. 1
      frontend/app/features/content/shared/forms/assets-editor.component.scss
  72. 3
      frontend/app/features/content/shared/references/references-editor.component.scss
  73. 36
      frontend/app/framework/angular/forms/control-errors.component.ts
  74. 2
      frontend/app/framework/angular/forms/editors/date-time-editor.component.html
  75. 10
      frontend/app/framework/angular/forms/editors/date-time-editor.component.ts
  76. 191
      frontend/app/framework/angular/forms/error-formatting.spec.ts
  77. 47
      frontend/app/framework/angular/forms/error-formatting.ts
  78. 5
      frontend/app/framework/angular/forms/form-error.component.html
  79. 9
      frontend/app/framework/services/localizer.service.spec.ts
  80. 63
      frontend/app/framework/services/localizer.service.ts
  81. 2
      frontend/app/shared/components/table-header.component.html
  82. 8
      frontend/app/shared/state/contents.forms.spec.ts
  83. 12
      frontend/app/shared/state/contents.forms.ts
  84. 64
      frontend/app/shared/state/contents.forms.visitors.ts
  85. 2
      frontend/app/shared/state/query.ts
  86. 3
      frontend/app/theme/_forms.scss
  87. 7
      frontend/app/theme/_panels.scss
  88. 1674
      frontend/package-lock.json
  89. 82
      frontend/package.json
  90. 3
      frontend/tsconfig.spec.json
  91. 344
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
  92. 70
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphInputType.cs
  93. 31
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GeolocationInputGraphType.cs
  94. 20
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldExtensions.cs
  95. 71
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs
  96. 47
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs

1
.drone.yml

@ -111,6 +111,7 @@ steps:
- push
branch:
- master
- release/*
- name: push_release
image: docker

4
backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs

@ -43,5 +43,9 @@ namespace Squidex.Extensions.Actions.Algolia
[DataType(DataType.MultilineText)]
[Formattable]
public string Document { get; set; }
[Display(Name = "Deletion", Description = "The condition when to delete the entry.")]
[DataType(DataType.Text)]
public string Delete { get; set; }
}
}

17
backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs

@ -13,6 +13,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
#pragma warning disable IDE0059 // Value assigned to symbol is never used
@ -21,8 +22,9 @@ namespace Squidex.Extensions.Actions.Algolia
public sealed class AlgoliaActionHandler : RuleActionHandler<AlgoliaAction, AlgoliaJob>
{
private readonly ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex> clients;
private readonly IScriptEngine scriptEngine;
public AlgoliaActionHandler(RuleEventFormatter formatter)
public AlgoliaActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine)
: base(formatter)
{
clients = new ClientPool<(string AppId, string ApiKey, string IndexName), ISearchIndex>(key =>
@ -31,13 +33,17 @@ namespace Squidex.Extensions.Actions.Algolia
return client.InitIndex(key.IndexName);
});
this.scriptEngine = scriptEngine;
}
protected override async Task<(string Description, AlgoliaJob Data)> CreateJobAsync(EnrichedEvent @event, AlgoliaAction action)
{
if (@event is EnrichedContentEvent contentEvent)
if (@event is IEnrichedEntityEvent entityEvent)
{
var contentId = contentEvent.Id.ToString();
var delete = @event.ShouldDelete(scriptEngine, action.Delete);
var contentId = entityEvent.Id.ToString();
var ruleDescription = string.Empty;
var ruleJob = new AlgoliaJob
@ -48,8 +54,7 @@ namespace Squidex.Extensions.Actions.Algolia
IndexName = await FormatAsync(action.IndexName, @event)
};
if (contentEvent.Type == EnrichedContentEventType.Deleted ||
contentEvent.Type == EnrichedContentEventType.Unpublished)
if (delete)
{
ruleDescription = $"Delete entry from Algolia index: {action.IndexName}";
}
@ -69,7 +74,7 @@ namespace Squidex.Extensions.Actions.Algolia
}
else
{
jsonString = ToJson(contentEvent);
jsonString = ToJson(@event);
}
json = JObject.Parse(jsonString);

9
backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs

@ -41,5 +41,14 @@ namespace Squidex.Extensions.Actions.ElasticSearch
[Display(Name = "Password", Description = "The optional password.")]
[DataType(DataType.Text)]
public string Password { get; set; }
[Display(Name = "Document", Description = "The optional custom document.")]
[DataType(DataType.MultilineText)]
[Formattable]
public string Document { get; set; }
[Display(Name = "Deletion", Description = "The condition when to delete the document.")]
[DataType(DataType.Text)]
public string Delete { get; set; }
}
}

43
backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs

@ -9,8 +9,10 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Elasticsearch.Net;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
#pragma warning disable IDE0059 // Value assigned to symbol is never used
@ -19,8 +21,9 @@ namespace Squidex.Extensions.Actions.ElasticSearch
public sealed class ElasticSearchActionHandler : RuleActionHandler<ElasticSearchAction, ElasticSearchJob>
{
private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients;
private readonly IScriptEngine scriptEngine;
public ElasticSearchActionHandler(RuleEventFormatter formatter)
public ElasticSearchActionHandler(RuleEventFormatter formatter, IScriptEngine scriptEngine)
: base(formatter)
{
clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key =>
@ -34,16 +37,19 @@ namespace Squidex.Extensions.Actions.ElasticSearch
return new ElasticLowLevelClient(config);
});
this.scriptEngine = scriptEngine;
}
protected override async Task<(string Description, ElasticSearchJob Data)> CreateJobAsync(EnrichedEvent @event, ElasticSearchAction action)
{
if (@event is EnrichedContentEvent contentEvent)
if (@event is IEnrichedEntityEvent entityEvent)
{
var contentId = contentEvent.Id.ToString();
var delete = @event.ShouldDelete(scriptEngine, action.Delete);
var ruleDescription = string.Empty;
var contentId = entityEvent.Id.ToString();
var ruleDescription = string.Empty;
var ruleJob = new ElasticSearchJob
{
IndexName = await FormatAsync(action.IndexName, @event),
@ -53,8 +59,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch
ContentId = contentId
};
if (contentEvent.Type == EnrichedContentEventType.Deleted ||
contentEvent.Type == EnrichedContentEventType.Unpublished)
if (delete)
{
ruleDescription = $"Delete entry index: {action.IndexName}";
}
@ -62,9 +67,31 @@ namespace Squidex.Extensions.Actions.ElasticSearch
{
ruleDescription = $"Upsert to index: {action.IndexName}";
var json = ToJson(contentEvent);
JObject json;
try
{
string jsonString;
if (!string.IsNullOrEmpty(action.Document))
{
jsonString = await FormatAsync(action.Document, @event);
jsonString = jsonString?.Trim();
}
else
{
jsonString = ToJson(@event);
}
json = JObject.Parse(jsonString);
}
catch (Exception ex)
{
json = new JObject(new JProperty("error", $"Invalid JSON: {ex.Message}"));
}
json.AddFirst(new JProperty("contentId", contentId));
ruleJob.Content = $"{{ \"objectId\": \"{contentId}\", {json.Substring(1)}";
ruleJob.Content = json.ToString();
}
return (ruleDescription, ruleJob);

32
backend/extensions/Squidex.Extensions/Actions/HttpHelper.cs → backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs

@ -10,12 +10,42 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure.Http;
namespace Squidex.Extensions.Actions
{
public static class HttpHelper
public static class RuleHelper
{
public static bool ShouldDelete(this EnrichedEvent @event, IScriptEngine scriptEngine, string expression)
{
if (!string.IsNullOrWhiteSpace(expression))
{
var vars = new ScriptVars
{
["event"] = @event
};
return scriptEngine.Evaluate(vars, expression);
}
return IsContentDeletion(@event) || IsAssetDeletion(@event);
}
public static bool IsContentDeletion(this EnrichedEvent @event)
{
return @event is EnrichedContentEvent contentEvent &&
(contentEvent.Type == EnrichedContentEventType.Deleted ||
contentEvent.Type == EnrichedContentEventType.Unpublished);
}
public static bool IsAssetDeletion(this EnrichedEvent @event)
{
return @event is EnrichedAssetEvent assetEvent &&
(assetEvent.Type == EnrichedAssetEventType.Deleted);
}
public static async Task<Result> OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null, CancellationToken ct = default)
{
HttpResponseMessage response = null;

12
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -8,15 +8,15 @@
<ProjectReference Include="..\..\src\Squidex.Web\Squidex.Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="6.6.0" />
<PackageReference Include="Algolia.Search" Version="6.8.0" />
<PackageReference Include="Confluent.Apache.Avro" Version="1.7.7.7" />
<PackageReference Include="Confluent.Kafka" Version="1.4.3" />
<PackageReference Include="Confluent.Kafka" Version="1.5.1" />
<PackageReference Include="Confluent.SchemaRegistry.Serdes" Version="1.3.0" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Datadog.Trace" Version="1.17.0" />
<PackageReference Include="Elasticsearch.Net" Version="7.7.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.6.4" />
<PackageReference Include="Datadog.Trace" Version="1.19.2" />
<PackageReference Include="Elasticsearch.Net" Version="7.9.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NodaTime" Version="3.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

1
backend/i18n/frontend_en.json

@ -481,6 +481,7 @@
"dashboard.trafficSummaryCard": "API Traffic Summary",
"dashboard.welcomeText": "Welcome to **{app}** dashboard.",
"dashboard.welcomeTitle": "Hi {user}",
"eventConsumers.count": "Count",
"eventConsumers.loadFailed": "Failed to load event consumers. Please reload.",
"eventConsumers.pageTitle": "Event Consumers",
"eventConsumers.position": "Position",

123
backend/i18n/frontend_it.json

@ -10,8 +10,8 @@
"apps.appNameHint": "Puoi utilizzare solo lettere, numeri e trattini e non più di 40 caratteri.",
"apps.appNameValidationMessage": "Il nome può contenere lettere minuscole (a-z), numeri e trattini all'interno.",
"apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.",
"apps.appsButtonCreate": "Panoramica delle App",
"apps.appsButtonFallbackTitle": "Panoramica delle App",
"apps.appsButtonCreate": "Nuova App",
"apps.appsButtonFallbackTitle": "Lista App",
"apps.archieve": "Archivia l'App",
"apps.archieveConfirmText": "Rimuovi il pattern",
"apps.archieveConfirmTitle": "Sei sicuro di voler archiviare questa app?",
@ -20,14 +20,14 @@
"apps.create": "Crea un'App",
"apps.createBlankApp": "Nuova App.",
"apps.createBlankAppDescription": "Crea una app vuota senza contenuti o schema.",
"apps.createBlogApp": "Nuovo esempio di blog",
"apps.createBlogApp": "Nuovo blog",
"apps.createBlogAppDescription": "Inizia con il nostro blog già pronto per l'uso.",
"apps.createFailed": "Non è stato possibile creare l'app. Per favore ricarica.",
"apps.createIdentityApp": "New Identity App",
"apps.createIdentityApp": "Nuova Identity App",
"apps.createIdentityAppDescription": "Crea un app per Squidex Identity.",
"apps.createIdentityAppV2": "Nuova Identity App V2",
"apps.createIdentityAppV2Description": "Creare un app per Squidex Identity V2.",
"apps.createProfileApp": "Nuovo esempio di Profilo",
"apps.createProfileApp": "Nuovo Profilo",
"apps.createProfileAppDescription": "Crea la tua pagina del profilo.",
"apps.createWithTemplate": "Create un esempio di {template}",
"apps.empty": "Non stai ancora collaborando su nessuna app",
@ -81,8 +81,8 @@
"assets.protected": "Protetto",
"assets.refreshTooltip": "Aggiorna le risorse (CTRL + SHIFT + R)",
"assets.reloaded": "Risorse ricaricate.",
"assets.removeConfirmText": "Do you really want to remove the asset?",
"assets.removeConfirmTitle": "Remove asset",
"assets.removeConfirmText": "Sei sicuro di voler cancellare la risorsa?",
"assets.removeConfirmTitle": "Risorsa cancellata",
"assets.renameFolder": "Rinomina la cartella",
"assets.replaceConfirmText": "Sei sicuro di voler sostituire la risorsa con una nuova versione?",
"assets.replaceConfirmTitle": "Sostituisco la risorsa?",
@ -90,8 +90,8 @@
"assets.searchByName": "Ricerca per nome",
"assets.searchByTags": "Ricerca per tag",
"assets.selectMany": "Seleziona le risorse",
"assets.specialFolder.parent": "<Parent>",
"assets.specialFolder.root": "Assets",
"assets.specialFolder.parent": "<Livello superiore>",
"assets.specialFolder.root": "Risorse",
"assets.tabFocusPoint": "Punto focale",
"assets.tabHistory": "Cronologia",
"assets.tabImage": "Immagine",
@ -138,8 +138,8 @@
"clients.addFailed": "Non è stato possibile aggiungere un client. Per favore ricarica.",
"clients.allowAnonymous": "Consenti l'accesso anonimo.",
"clients.allowAnonymousHint": "Consenti l'accesso alle API senza token di accesso a tutte le risorse che sono configurate per il ruolo di questo client. E' possibile avere un solo client impostato con accesso anonimo.",
"clients.apiCallsLimit": "Max API Calls",
"clients.apiCallsLimitHint": "Limit the number of API calls this client can make per month to protect your API contingent for other clients that are more important.",
"clients.apiCallsLimit": "Numero Max di chiamate alle API",
"clients.apiCallsLimitHint": "Limita il numero di chiamate al mese effettuabili dal client alle API calls per riservare ai client più importanti il numero di chiamate API disponili.",
"clients.clientIdValidationMessage": "Il nome deve contenere solo lettere, numeri, trattini e spaziNa.",
"clients.clientNamePlaceholder": "Inserisci il nome del client",
"clients.connect": "Connettere",
@ -203,19 +203,19 @@
"common.clientSecret": "Secret Client",
"common.clipboardAdded": "Il valore è stato aggiunto nei tuoi appunti.",
"common.clone": "Clona",
"common.cluster": "Raggruppamento",
"common.clusterPageTitle": "Raggruppamento",
"common.cluster": "Cluster",
"common.clusterPageTitle": "Cluster",
"common.comments": "Commenti",
"common.confirm": "Conferma",
"common.consumers": "Utenti",
"common.content": "Contentenuto",
"common.contents": "Contentenuti",
"common.consumers": "Servizi",
"common.content": "Contenuto",
"common.contents": "Contenuti",
"common.continue": "Continua",
"common.contributors": "Collaboratori",
"common.create": "Crea",
"common.created": "Creato",
"common.date": "Data",
"common.dateTimeEditor.local": "Local",
"common.dateTimeEditor.local": "Locale",
"common.dateTimeEditor.now": "Data e Ora attuale",
"common.dateTimeEditor.nowTooltip": "Imposta la data e l'ora attuale (UTC)",
"common.dateTimeEditor.today": "Oggi",
@ -275,16 +275,16 @@
"common.preview": "Anteprima",
"common.product": "Squidex Headless CMS",
"common.project": "Progetto",
"common.queryOperators.contains": "contains",
"common.queryOperators.empty": "is empty",
"common.queryOperators.endsWith": "ends with",
"common.queryOperators.eq": "is equals to",
"common.queryOperators.ge": "is greater than or equals to",
"common.queryOperators.gt": "is greater than",
"common.queryOperators.le": "is less than pr equals to",
"common.queryOperators.lt": "is less than",
"common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with",
"common.queryOperators.contains": "contiene",
"common.queryOperators.empty": "è vuoto",
"common.queryOperators.endsWith": "finisce con",
"common.queryOperators.eq": "è uguale a",
"common.queryOperators.ge": "è maggiore di o uguale a",
"common.queryOperators.gt": "è maggiore di",
"common.queryOperators.le": "è minore di o uguale a",
"common.queryOperators.lt": "è minore di",
"common.queryOperators.ne": "è uguale a",
"common.queryOperators.startsWith": "inizia con",
"common.refresh": "Aggiorna",
"common.rename": "Rinomina",
"common.requiredHint": "obbligatorio",
@ -359,19 +359,19 @@
"contents.draftStatus": "Nuova versione",
"contents.editPageTitle": "Modifica contenuto",
"contents.editTitle": "Modifica il contenuto",
"contents.invariantFieldDescription": "The '{fieldName}' field of the content item.",
"contents.invariantFieldDescription": "Il campo '{fieldName}' del contenuto.",
"contents.languageModeAll": "Tutte le lingue",
"contents.languageModeSingle": "Una sola lingua",
"contents.lastModifiedByFieldDescription": "L'utente che ha modificato l'elememto l'ultima voltaThe user who modified the content item the last time.",
"contents.lastModifiedByFieldDescription": "L'utente che ha modificato l'elemento l'ultima volta.",
"contents.lastModifiedFieldDescription": "La data e l'ora dell'ultima modifica del contenuto.",
"contents.lastUpdatedLabel": "Ultimo aggiornamento",
"contents.loadContent": "Carica",
"contents.loadContentFailed": "Non è stato possibile caricare il contenuto. Per favore ricarica.",
"contents.loadDataFailed": "Non è stato possibile caricare i dati. Per favore ricarica.",
"contents.loadFailed": "Non è stato possibile caricare i contenuti. Per favore ricarica.",
"contents.loadVersionFailed": "Non è stato possibile version a new version. Per favore ricarica.",
"contents.localizedFieldDescription": "The '{fieldName}' field of the content item (localized).",
"contents.newStatusFieldDescription": "The new status of the content item.",
"contents.loadVersionFailed": "Non è stato possibile creare una nuova versione. Per favore ricarica.",
"contents.localizedFieldDescription": "Il campo '{fieldName}' del contenuto (localizzato).",
"contents.newStatusFieldDescription": "Nuovo stato per l'elemento del contenuto.",
"contents.noReference": "- Nessun collegamento -",
"contents.pendingChangesTextToChange": "Non hai salvato le modifiche.\n\nSe cambi lo stato perderai le modifiche.\n\n**Sei sicuro di voler continuare?**",
"contents.pendingChangesTextToClose": "Non hai salvato le modifiche.\n\nChiudendo il contenuto corrente perderai tutte le modifiche.\n\n**Sei sicuro di voler continuare?**",
@ -383,8 +383,8 @@
"contents.referencesSelectSchema": "Seleziona {schema}",
"contents.refreshTooltip": "Aggiorna i contenuti (CTRL + SHIFT + R)",
"contents.reloaded": "Contenuti aggiornati.",
"contents.removeConfirmText": "Do you really want to remove the content?",
"contents.removeConfirmTitle": "Remove content",
"contents.removeConfirmText": "Sei sicuro di voler rimuovere il contenuto?",
"contents.removeConfirmTitle": "Cancella il contenuto",
"contents.saveAndPublish": "Salva e pubblica",
"contents.scheduledAt": "alle",
"contents.scheduledAtLabel": "alle",
@ -397,16 +397,16 @@
"contents.statusQueries": "Stato Query",
"contents.stockPhotoEmpty": "Nessuna selezione",
"contents.stockPhotoSearch": "Cerca foto su Unsplash",
"contents.tableHeaders.created": "Created",
"contents.tableHeaders.createdBy": "Created By",
"contents.tableHeaders.createdByShort": "By",
"contents.tableHeaders.created": "Creato",
"contents.tableHeaders.createdBy": "Creato da",
"contents.tableHeaders.createdByShort": "Da",
"contents.tableHeaders.id": "Id",
"contents.tableHeaders.lastModified": "Updated",
"contents.tableHeaders.lastModifiedBy": "Updated By",
"contents.tableHeaders.lastModifiedByShort": "By",
"contents.tableHeaders.nextStatus": "Next Status",
"contents.tableHeaders.status": "Status",
"contents.tableHeaders.version": "Version",
"contents.tableHeaders.lastModified": "Modificato",
"contents.tableHeaders.lastModifiedBy": "Modificato da",
"contents.tableHeaders.lastModifiedByShort": "Da",
"contents.tableHeaders.nextStatus": "Stato successivo",
"contents.tableHeaders.status": "Stato",
"contents.tableHeaders.version": "Versione",
"contents.unsavedChangesText": "Non hai salvato le modifiche. Vuoi salvarle adesso?",
"contents.unsavedChangesTitle": "Modifiche non salvate",
"contents.updated": "Contenuto aggiornato con successo.",
@ -473,14 +473,15 @@
"dashboard.schemasCard": "Schemi",
"dashboard.schemasCardDescription": "Panoramica del modello dei dati di questa app.",
"dashboard.stackedChart": "Istogramma in pila",
"dashboard.supportCard": "Feedback & Assistenza",
"dashboard.supportCard": "Feedback & Supporto",
"dashboard.supportCardDescription": "Fornisci feedback e richiedi funzionalità per aiutarci a migliorare Squidex..",
"dashboard.trafficChart": "Diagramma del traffico delle API",
"dashboard.trafficHeader": "Traffico (MB)",
"dashboard.trafficLimitLabel": "Limite mensile",
"dashboard.trafficSummaryCard": "Riepilogo del traffico delle API",
"dashboard.welcomeText": "Benvenuto su **{app}** dashboard.",
"dashboard.welcomeText": "Benvenuto sulla dashboard **{app}**.",
"dashboard.welcomeTitle": "Ciao {user}",
"eventConsumers.count": "Conteggio",
"eventConsumers.loadFailed": "Non è stato possibile caricare event consumers. Per favore ricarica.",
"eventConsumers.pageTitle": "Eventi degli utenti",
"eventConsumers.position": "Posizione",
@ -543,8 +544,8 @@
"roles.addFailed": "Non è stato possibile aggiungere il ruolo. Per favore ricarica.",
"roles.default.owner": "Hai come amministratore tutte le funzionalità, compreso cancellare le app.",
"roles.default.reader": "Hai un'utenza in sola lettura sia per i contenuti che per le risorse.",
"roles.defaults.developer": "Hai un'utenza che può visualizzare le API, modificare le risorse, i contenuti, gli schema, le regole, i workflows e i pattern.",
"roles.defaults.editor": "Hai un'utenzaCan che può modificare le risorse, i conteuti e visualizzare i workflow.",
"roles.defaults.developer": "Hai un'utenza che può visualizzare le API, modificare le risorse, i contenuti, gli schema, le regole, i workflow e i pattern.",
"roles.defaults.editor": "Hai un'utenza che può modificare le risorse, i conteuti e visualizzare i workflow.",
"roles.deleteConfirmText": "Cancella il ruolo",
"roles.deleteConfirmTitle": "Sei sicuro di voler eliminare il ruolo?",
"roles.loadFailed": "Non è stato possibile caricare i ruoli. Per favore ricarica.",
@ -554,7 +555,7 @@
"roles.revokeFailed": "Non è stato possibile rimuovere il ruolo. Per favore ricarica.",
"roles.roleNamePlaceholder": "Inserisci il nome del ruolo",
"roles.updateFailed": "Non è stato possibile aggiornare il ruolo. Per favore ricarica.",
"rules.actionEdit": "Mdifica l'Azione",
"rules.actionEdit": "Modifica l'Azione",
"rules.cancelFailed": "Non è stato possibile eliminare la regola. Per favore ricarica.",
"rules.create": "Crea un nuova Regola",
"rules.createFailed": "Non è stato possibile creare una nuova regola. Per favore ricarica.",
@ -649,7 +650,7 @@
"schemas.field.localizable": "Consente la localizzazione",
"schemas.field.localizableHint": "Puoi impostare il campo per consentire la localizzazione, ossia che dipende dalla lingua che utilizzi come ad esempio i nomi delle città.",
"schemas.field.localizableMarker": "consente la localizzazione",
"schemas.field.lock": "Blocca e impedisce i cambiamenti",
"schemas.field.lock": "Blocca e impedisci i cambiamenti",
"schemas.field.lockConfirmText": "Attenzione: Bloccare un campo è un'azione irreversibile! Se blocchi il campo non potrai più sbloccarlo o cancellarlo o cambiarlo. Sei sicuro di voler bloccare il campo?",
"schemas.field.lockedMarker": "Bloccato",
"schemas.field.nameHint": "Il nome del campo nelle chiamate API response.",
@ -702,9 +703,9 @@
"schemas.fieldTypes.references.countMin": "Numero Min Elementi",
"schemas.fieldTypes.references.description": "Link ad altri elementi del contenuto.",
"schemas.fieldTypes.references.resolveHint": "Mostra il nome dell'elemento collegato (reference) nella lista dei contenuti quando il numero massimo di elementi è impostato a 1.",
"schemas.fieldTypes.string.characters": "Characters",
"schemas.fieldTypes.string.charactersMax": "Max Characters",
"schemas.fieldTypes.string.charactersMin": "Min Characters",
"schemas.fieldTypes.string.characters": "Caratteri",
"schemas.fieldTypes.string.charactersMax": "Max numero di Caratteri",
"schemas.fieldTypes.string.charactersMin": "Min numero di Caratteri",
"schemas.fieldTypes.string.contentType": "Content Type",
"schemas.fieldTypes.string.description": "Titoli, nomi, paragrafi.",
"schemas.fieldTypes.string.length": "Lunghezza",
@ -712,13 +713,13 @@
"schemas.fieldTypes.string.lengthMin": "Lunghezza Min",
"schemas.fieldTypes.string.pattern": "Regex Pattern",
"schemas.fieldTypes.string.patternMessage": "Messaggio del Pattern",
"schemas.fieldTypes.string.suggestions": "Suggestions",
"schemas.fieldTypes.string.words": "Words",
"schemas.fieldTypes.string.wordsMax": "Max Words",
"schemas.fieldTypes.string.wordsMin": "Min Words",
"schemas.fieldTypes.tags.count": "Items",
"schemas.fieldTypes.tags.countMax": "Max Items",
"schemas.fieldTypes.tags.countMin": "Min Items",
"schemas.fieldTypes.string.suggestions": "Suggerimenti",
"schemas.fieldTypes.string.words": "Parole",
"schemas.fieldTypes.string.wordsMax": "Numero max di Parole",
"schemas.fieldTypes.string.wordsMin": "Numero min di Parole",
"schemas.fieldTypes.tags.count": "Elementi",
"schemas.fieldTypes.tags.countMax": "Numero max di Elementi",
"schemas.fieldTypes.tags.countMin": "Numero min di Elementi",
"schemas.fieldTypes.tags.description": "Formato speciale per i tag.",
"schemas.fieldTypes.ui.description": "Separatore per il pannello delle modifiche della UI.",
"schemas.hideFieldFailed": "Non è stato possibile nascondere il campo. Per favore ricarica.",
@ -764,7 +765,7 @@
"schemas.synchronizeFailed": "Non è stato possibile sincronizzare lo schema. Per favore ricarica.",
"schemas.tabFields": "Campi",
"schemas.tabJson": "Json",
"schemas.tabMore": "Di più",
"schemas.tabMore": "Altro",
"schemas.tabScripts": "Script",
"schemas.tabUI": "UI",
"schemas.ui": "Campi assegnati",
@ -783,7 +784,7 @@
"search.advancedTour": "Fai clic su questa icona per visualizzare il menu della ricerca avanzata!",
"search.customQuery": "Query personalizzata",
"search.fullTextTour": "Cerca contenuti utilizzando la ricerca testuale su tutti i campi e le lingue!",
"search.help": "Ulteriori informazioni sui filtri su [Documentation](https://https://docs.squidex.io/04-guides/02-api.html).",
"search.help": "Ulteriori informazioni sui filtri su [Documentazione](https://https://docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "Le mie query",
"search.nameQuery": "Dai un nome alla query",
"search.queriesEmpty": "Ricerca per {types} e utilizza l'icona <i class=\"icon-star-empty\"></i> nella ricerca per salvare la query per tutti i collaboratori.",

3
backend/i18n/frontend_nl.json

@ -380,7 +380,7 @@
"contents.referencesCreatePublish": "Maken en publiceren",
"contents.referencesLink": "Link geselecteerde inhoud ({count})",
"contents.referencesSelectExisting": "Selecteer bestaande",
"contents.referencesSelectSchema": "Selecteer {scheme}",
"contents.referencesSelectSchema": "Selecteer {schema}",
"contents.refreshTooltip": "Ververs inhoud (CTRL + SHIFT + R)",
"contents.reloaded": "Inhoud opnieuw geladen.",
"contents.removeConfirmText": "Wil je de inhoud echt verwijderen?",
@ -481,6 +481,7 @@
"dashboard.trafficSummaryCard": "API Verkeer Samenvatting",
"dashboard.welcomeText": "Welkom bij **{app}** dashboard.",
"dashboard.welcomeTitle": "Hallo {user}",
"eventConsumers.count": "Count",
"eventConsumers.loadFailed": "Kan gebeurtenisgebruikers niet laden. Laad opnieuw.",
"eventConsumers.pageTitle": "Evenementconsumenten",
"eventConsumers.position": "Positie",

4
backend/i18n/source/backend_en.json

@ -92,9 +92,9 @@
"common.notFoundValue": "- not found -",
"common.numDays": "Num days",
"common.odataFailure": "Failed to parse query: {message}",
"common.odataFilterNotValid": "OData $filter clause not valid: {ex.Message}",
"common.odataFilterNotValid": "OData $filter clause not valid: {message}",
"common.odataNotSupported": "OData operation is not supported.",
"common.odataSearchNotValid": "OData $search clause not valid: {ex.Message}",
"common.odataSearchNotValid": "OData $search clause not valid: {message}",
"common.oldPassword": "Old password",
"common.other": "Other",
"common.partitioning": "Partitioning",

75
backend/i18n/source/backend_it.json

@ -1,7 +1,12 @@
{
"annotations_AbsoluteUrl": "Il campo {name|lower} deve essere un URL assoluto.",
"annotations_Compare": "Il campo {name|lower} deve essere uguale a {other|lower}.",
"annotations_EmailAddress": "Il campo {name|lower} non è un indirizzo email valido.",
"annotations_Range": "Il campo {name|lower} deve essere tra {min} e {max}.",
"annotations_RegularExpression": "Il campo {name|lower} non è.",
"annotations_Required": "Il campo è {name|lower} obbligatorio.",
"annotations_StringLength": "The field {name|lower} must be a string with a maximum length of {max}.",
"annotations_StringLengthMinimum": "The field {name|lower} must be a string with a minimum length of {min} and a maximum length of {max}.",
"apps.alreadyArchieved": "La App è stata già archiviata.",
"apps.clients.idAlreadyExists": "Un client con lo stesso id esiste già.",
"apps.contributors.cannotChangeYourself": "Non puoi cambiare il tuo ruolo.",
@ -9,7 +14,7 @@
"apps.contributors.onlyOneOwner": "Non è possibile rimuovere l'unico owner.",
"apps.languages.fallbackNotFound": "La App non ha configurato una lingua alternativa'{fallback}'.",
"apps.languages.languageAlreadyAdded": "La lingua è stata già inserita.",
"apps.languages.masterLanguageNoFallbacks": "La lingua master language non ha lingue alternative.",
"apps.languages.masterLanguageNoFallbacks": "La lingua master non ha lingue alternative.",
"apps.languages.masterLanguageNotOptional": "La lingua master non può essere opzionale.",
"apps.languages.masterLanguageNotRemovable": "La lingua master non può essere rimossa.",
"apps.nameAlreadyExists": "Esiste già un'app con lo stesso nome.",
@ -40,6 +45,7 @@
"common.clientd": "ID Client",
"common.clientId": "ID Client",
"common.clientSecret": "Secret Client",
"common.contentType": "Content type",
"common.contributorId": "ID o Email del collaboratore",
"common.data": "Data",
"common.defaultValue": "Valore predefinito",
@ -66,25 +72,29 @@
"common.language": "Codice della lingua",
"common.login": "Accedi",
"common.logout": "Esci",
"common.maxCharacters": "Numero massimo di caratteri",
"common.maxHeight": "Altezza massima",
"common.maxItems": "Numeri massimo di elementi",
"common.maxLength": "Lunghezza massima",
"common.maxSize": "Dimensione massima",
"common.maxValue": "Valore massimo",
"common.maxWidth": "Larghezza massima",
"common.maxWords": "Numero massimo di parole",
"common.minCharacters": "Numero minimo di caratteri",
"common.minHeight": "Altezza minima",
"common.minItems": "Numero minimo elementi",
"common.minLength": "Lunghezza minima",
"common.minSize": "Dimensione minima",
"common.minValue": "Valore minimo",
"common.minWidth": "Larghezza massima",
"common.minWords": "Numero minimo di parole",
"common.name": "Nome",
"common.notFoundValue": "- non trovato -",
"common.numDays": "Num. giorni",
"common.odataFailure": "Fallito parsando la query: {message}",
"common.odataFilterNotValid": "OData $filter condizione non valida: {ex.Message}",
"common.odataFilterNotValid": "OData $filter condizione non valida: {message}",
"common.odataNotSupported": "OData operazione non supportata.",
"common.odataSearchNotValid": "OData $search condizione non valida: {ex.Message}",
"common.odataSearchNotValid": "OData $search condizione non valida: {message}",
"common.oldPassword": "Vecchia password",
"common.other": "Altro",
"common.partitioning": "Partizionamento",
@ -144,26 +154,49 @@
"contents.validation.itemCount": "Deve avere esattamente {count} elemento(i).",
"contents.validation.itemCountBetween": "Deve essere tra {min} e {max} elemento(i).",
"contents.validation.max": "Deve esseer minore o uguale a {max}.",
"contents.validation.maxCharacters": "Il testo non deve avere più di {max} carattere(i).",
"contents.validation.maximumHeight": "L'altezza {height}px deve essere inferiore a {max}px.",
"contents.validation.maximumSize": "La dimensione {size} deve essere inferiore a {max}.",
"contents.validation.maximumWidth": "La larghezza {width}px deve essere inferiore a {max}px.",
"contents.validation.maxItems": "Non deve avere più di {max} elemento(i).",
"contents.validation.maxLength": "Non deve avere più di {max} carattere(i).",
"contents.validation.maxWords": "Non deve avere più di {max} parola(e).",
"contents.validation.min": "Deve essere maggiore o uguale a {min}.",
"contents.validation.minimumHeight": "L'altezza {height}px deve essere maggiore di {min}px.",
"contents.validation.minimumSize": "La dimensione {size} deve essere maggiore di {min}.",
"contents.validation.minimumWidth": "La larghezza {width}px deve essere maggiore di {min}px.",
"contents.validation.minItems": "Deve avere almento {min} elemento(i).",
"contents.validation.minLength": "Deve avere almeno {min} carattere(i).",
"contents.validation.minNormalCharacters": "Deve avere almeno un testo di {min} carattere(i).",
"contents.validation.minWords": "Deve avere almeno {min} parola(e).",
"contents.validation.mustBeEmpty": "Il valore non deve essere definito.",
"contents.validation.normalCharacterCount": "Deve essere esattamente un testo di {count} carattere(i).",
"contents.validation.normalCharactersBetween": "Deve essere un testo tra {min} e {max} carattere(i).",
"contents.validation.notAllowed": "Non è un valore consentito.",
"contents.validation.pattern": "Deve seguire il pattern.",
"contents.validation.regexTooSlow": "La Regex è troppo lenta.",
"contents.validation.required": "Il campo è obbligatorio.",
"contents.validation.unique": "Esiste un altro contenuto con lo stesso valore.",
"contents.validation.unknownField": "Non è noto {fieldType}.",
"contents.validation.wordCount": "Deve avere esattamente {count} parola(e).",
"contents.validation.wordsBetween": "Deve essere tra {min} e {max} parola(e).",
"contents.workflowErorPublishing": "Il workflow del contenuto impedisce la pubblicazione.",
"contents.workflowErrorUpdate": "Il workflow non consente le modifiche per lo stato {status}",
"dotnet_identity_DefaultEror": "Si è verificato un errore sconosciuto.",
"dotnet_identity_DuplicateEmail": "Email già in uso.",
"dotnet_identity_DuplicateUserName": "Nome utente già in uso.",
"dotnet_identity_InvalidEmail": "L'Email non è valida.",
"dotnet_identity_InvalidUserName": "Il nome utente '{0}' non è valido, può contenere solo lettere e numeri.",
"dotnet_identity_LoginAlreadyAssociated": "Esiste già un utente con queste credenziali di accesso.",
"dotnet_identity_PasswordMismatch": "Password errata.",
"dotnet_identity_PasswordRequiresDigit": "La Password devono contenere almeno un numero ('0'-'9').",
"dotnet_identity_PasswordRequiresLower": "La passowrd deve avere almeno una lettera minuscola ('a'-'z').",
"dotnet_identity_PasswordRequiresNonAlphanumeric": "La passowrd deve avere almeno un carattere non alfanumerico.",
"dotnet_identity_PasswordRequiresUniqueChars": "La password deve essere composta almeno da {0} caratteri differenti.",
"dotnet_identity_PasswordRequiresUpper": "La password deve avere almeno una lettera maiuscola ('A'-'Z').",
"dotnet_identity_PasswordTooShort": "La password è troppo corta.",
"dotnet_identity_PwnedError": "Questa password è apparsa in un database di password compromesso e non dovrebbe più essere utilizzata. Se l'hai usata, cambiala!",
"dotnet_identity_UserLockedOut": "L'utente è bloccato.",
"exception.invalidJsonQuery": "La query Json non è valida: {message}",
"exception.invalidJsonQueryJson": "La query Json non è valida: {message}",
"exceptions.domainObjectDeleted": "L'entità ({id}) è stata cancellata.",
@ -178,17 +211,17 @@
"history.apps.languagedRemoved": "rimossa lingua {[Language]}",
"history.apps.languagedSetToMaster": "cambiata la lingua master in {[Language]}",
"history.apps.languagedUpdated": "aggiornata la lingua {[Language]}",
"history.apps.patternAdded": "adggiunto pattern {[Name]}",
"history.apps.patternDeleted": "eliminato pattern {[PatternId]}",
"history.apps.patternUpdated": "modificato pattern {[Name]}",
"history.apps.planChanged": "cambiato il piano in {[Plan]}",
"history.apps.planReset": "riconfigurato il piano",
"history.apps.roleAdded": "aggiunto il ruolo {[Name]}",
"history.apps.roleDeleted": "eliminato role {[Name]}",
"history.apps.roleUpdated": "aggiornato role {[Name]}",
"history.assets.replaced": "risorsa sostituita.",
"history.assets.updated": "risorsa aggiornata.",
"history.assets.uploaded": "risorsa caricata.",
"history.apps.patternAdded": "ha aggiunto pattern {[Name]}",
"history.apps.patternDeleted": "ha eliminato pattern {[PatternId]}",
"history.apps.patternUpdated": "ha modificato pattern {[Name]}",
"history.apps.planChanged": "ha cambiato il piano in {[Plan]}",
"history.apps.planReset": "ha riconfigurato il piano",
"history.apps.roleAdded": "ha aggiunto il ruolo {[Name]}",
"history.apps.roleDeleted": "ha eliminato role {[Name]}",
"history.apps.roleUpdated": "ha aggiornato role {[Name]}",
"history.assets.replaced": "ha sostituito la risorsa.",
"history.assets.updated": "ha aggiornato la risorsa.",
"history.assets.uploaded": "ha caricato la risorsa.",
"history.contents.created": "creato il contenuto {[Schema]}.",
"history.contents.deleted": "cancellato il contenuto {[Schema]}.",
"history.contents.draftCreated": "creata una nuova bozza.",
@ -205,12 +238,12 @@
"history.schemas.fieldLocked": "bloccato il campo {[Field]} dallo schema {[Name]}.",
"history.schemas.fieldShown": "mostrato il campo {[Field]} dallo schema {[Name]}.",
"history.schemas.fieldsReordered": "riordinati i campi dello schema {[Name]}.",
"history.schemas.fieldUpdated": "aggiornato il campo {[Field]} dello schema {[Name]}.",
"history.schemas.published": "pubblicato lo schema {[Name]}.",
"history.schemas.scriptsConfigured": "configurato lo script per lo schema {[Name]}.",
"history.schemas.unpublished": "rimosso dalla pubblicazione lo schema {[Name]}.",
"history.schemas.updated": "aggiornato lo schema {[Name]}.",
"history.statusChanged": "cambiato lo stato del contenuto {[Schema]} in {[Status]}.",
"history.schemas.fieldUpdated": "ha aggiornato il campo {[Field]} dello schema {[Name]}.",
"history.schemas.published": "ha pubblicato lo schema {[Name]}.",
"history.schemas.scriptsConfigured": "ha configurato lo script per lo schema {[Name]}.",
"history.schemas.unpublished": "ha rimosso dalla pubblicazione lo schema {[Name]}.",
"history.schemas.updated": "ha aggiornato lo schema {[Name]}.",
"history.statusChanged": "ha cambiato lo stato del contenuto {[Schema]} in {[Status]}.",
"login.githubPrivateEmail": "Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github.",
"rules.alreadyDeleted": "La regola è stata già cancellata.",
"rules.ruleAlreadyRunning": "E' in esecuzione un'altra regola.",
@ -281,7 +314,7 @@
"users.profile.generateClient": "Creato",
"users.profile.generateClientDone": "Secret client generato con successo.",
"users.profile.headline": "Profile modificato",
"users.profile.hideProfile": "Non mostrare il mio profilo agli altri utentiDo not show my profile to other users",
"users.profile.hideProfile": "Non mostrare il mio profilo agli altri utenti",
"users.profile.loginsTitle": "Login",
"users.profile.passwordTitle": "Password",
"users.profile.pii": "Informazioni personali",

4
backend/i18n/source/backend_nl.json

@ -87,9 +87,9 @@
"common.notFoundValue": "- niet gevonden -",
"common.numDays": "Aantal dagen",
"common.odataFailure": "Ontleden zoekopdracht: {message}",
"common.odataFilterNotValid": "OData $ filterclausule niet geldig: {ex.Message}",
"common.odataFilterNotValid": "OData $ filterclausule niet geldig: {message}",
"common.odataNotSupported": "OData-bewerking wordt niet ondersteund.",
"common.odataSearchNotValid": "OData $ zoekclausule niet geldig: {ex.Message}",
"common.odataSearchNotValid": "OData $ zoekclausule niet geldig: {message}",
"common.oldPassword": "Oud wachtwoord",
"common.other": "Anders",
"common.partitioning": "Partitioneren",

1
backend/i18n/source/frontend_en.json

@ -481,6 +481,7 @@
"dashboard.trafficSummaryCard": "API Traffic Summary",
"dashboard.welcomeText": "Welcome to **{app}** dashboard.",
"dashboard.welcomeTitle": "Hi {user}",
"eventConsumers.count": "Count",
"eventConsumers.loadFailed": "Failed to load event consumers. Please reload.",
"eventConsumers.pageTitle": "Event Consumers",
"eventConsumers.position": "Position",

89
backend/i18n/source/frontend_it.json

@ -10,8 +10,8 @@
"apps.appNameHint": "Puoi utilizzare solo lettere, numeri e trattini e non più di 40 caratteri.",
"apps.appNameValidationMessage": "Il nome può contenere lettere minuscole (a-z), numeri e trattini all'interno.",
"apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.",
"apps.appsButtonCreate": "Panoramica delle App",
"apps.appsButtonFallbackTitle": "Panoramica delle App",
"apps.appsButtonCreate": "Nuova App",
"apps.appsButtonFallbackTitle": "Lista App",
"apps.archieve": "Archivia l'App",
"apps.archieveConfirmText": "Rimuovi il pattern",
"apps.archieveConfirmTitle": "Sei sicuro di voler archiviare questa app?",
@ -20,14 +20,14 @@
"apps.create": "Crea un'App",
"apps.createBlankApp": "Nuova App.",
"apps.createBlankAppDescription": "Crea una app vuota senza contenuti o schema.",
"apps.createBlogApp": "Nuovo esempio di blog",
"apps.createBlogApp": "Nuovo blog",
"apps.createBlogAppDescription": "Inizia con il nostro blog già pronto per l'uso.",
"apps.createFailed": "Non è stato possibile creare l'app. Per favore ricarica.",
"apps.createIdentityApp": "New Identity App",
"apps.createIdentityApp": "Nuova Identity App",
"apps.createIdentityAppDescription": "Crea un app per Squidex Identity.",
"apps.createIdentityAppV2": "Nuova Identity App V2",
"apps.createIdentityAppV2Description": "Creare un app per Squidex Identity V2.",
"apps.createProfileApp": "Nuovo esempio di Profilo",
"apps.createProfileApp": "Nuovo Profilo",
"apps.createProfileAppDescription": "Crea la tua pagina del profilo.",
"apps.createWithTemplate": "Create un esempio di {template}",
"apps.empty": "Non stai ancora collaborando su nessuna app",
@ -81,6 +81,8 @@
"assets.protected": "Protetto",
"assets.refreshTooltip": "Aggiorna le risorse (CTRL + SHIFT + R)",
"assets.reloaded": "Risorse ricaricate.",
"assets.removeConfirmText": "Sei sicuro di voler cancellare la risorsa?",
"assets.removeConfirmTitle": "Risorsa cancellata",
"assets.renameFolder": "Rinomina la cartella",
"assets.replaceConfirmText": "Sei sicuro di voler sostituire la risorsa con una nuova versione?",
"assets.replaceConfirmTitle": "Sostituisco la risorsa?",
@ -88,6 +90,8 @@
"assets.searchByName": "Ricerca per nome",
"assets.searchByTags": "Ricerca per tag",
"assets.selectMany": "Seleziona le risorse",
"assets.specialFolder.parent": "<Livello superiore>",
"assets.specialFolder.root": "Risorse",
"assets.tabFocusPoint": "Punto focale",
"assets.tabHistory": "Cronologia",
"assets.tabImage": "Immagine",
@ -134,6 +138,8 @@
"clients.addFailed": "Non è stato possibile aggiungere un client. Per favore ricarica.",
"clients.allowAnonymous": "Consenti l'accesso anonimo.",
"clients.allowAnonymousHint": "Consenti l'accesso alle API senza token di accesso a tutte le risorse che sono configurate per il ruolo di questo client. E' possibile avere un solo client impostato con accesso anonimo.",
"clients.apiCallsLimit": "Numero Max di chiamate alle API",
"clients.apiCallsLimitHint": "Limita il numero di chiamate al mese effettuabili dal client alle API calls per riservare ai client più importanti il numero di chiamate API disponili.",
"clients.clientIdValidationMessage": "Il nome deve contenere solo lettere, numeri, trattini e spaziNa.",
"clients.clientNamePlaceholder": "Inserisci il nome del client",
"clients.connect": "Connettere",
@ -197,22 +203,24 @@
"common.clientSecret": "Secret Client",
"common.clipboardAdded": "Il valore è stato aggiunto nei tuoi appunti.",
"common.clone": "Clona",
"common.cluster": "Raggruppamento",
"common.clusterPageTitle": "Raggruppamento",
"common.cluster": "Cluster",
"common.clusterPageTitle": "Cluster",
"common.comments": "Commenti",
"common.confirm": "Conferma",
"common.consumers": "Utenti",
"common.content": "Contentenuto",
"common.contents": "Contentenuti",
"common.consumers": "Servizi",
"common.content": "Contenuto",
"common.contents": "Contenuti",
"common.continue": "Continua",
"common.contributors": "Collaboratori",
"common.create": "Crea",
"common.created": "Creato",
"common.date": "Data",
"common.dateTimeEditor.local": "Locale",
"common.dateTimeEditor.now": "Data e Ora attuale",
"common.dateTimeEditor.nowTooltip": "Imposta la data e l'ora attuale (UTC)",
"common.dateTimeEditor.today": "Oggi",
"common.dateTimeEditor.todayTooltip": "Imposta la data di oggi (UTC)",
"common.dateTimeEditor.utc": "UTC",
"common.delete": "Cancella",
"common.description": "Descrizione",
"common.displayName": "Nome visualizzato",
@ -267,6 +275,16 @@
"common.preview": "Anteprima",
"common.product": "Squidex Headless CMS",
"common.project": "Progetto",
"common.queryOperators.contains": "contiene",
"common.queryOperators.empty": "è vuoto",
"common.queryOperators.endsWith": "finisce con",
"common.queryOperators.eq": "è uguale a",
"common.queryOperators.ge": "è maggiore di o uguale a",
"common.queryOperators.gt": "è maggiore di",
"common.queryOperators.le": "è minore di o uguale a",
"common.queryOperators.lt": "è minore di",
"common.queryOperators.ne": "è uguale a",
"common.queryOperators.startsWith": "inizia con",
"common.refresh": "Aggiorna",
"common.rename": "Rinomina",
"common.requiredHint": "obbligatorio",
@ -341,16 +359,19 @@
"contents.draftStatus": "Nuova versione",
"contents.editPageTitle": "Modifica contenuto",
"contents.editTitle": "Modifica il contenuto",
"contents.invariantFieldDescription": "Il campo '{fieldName}' del contenuto.",
"contents.languageModeAll": "Tutte le lingue",
"contents.languageModeSingle": "Una sola lingua",
"contents.lastModifiedByFieldDescription": "L'utente che ha modificato l'elememto l'ultima voltaThe user who modified the content item the last time.",
"contents.lastModifiedByFieldDescription": "L'utente che ha modificato l'elemento l'ultima volta.",
"contents.lastModifiedFieldDescription": "La data e l'ora dell'ultima modifica del contenuto.",
"contents.lastUpdatedLabel": "Ultimo aggiornamento",
"contents.loadContent": "Carica",
"contents.loadContentFailed": "Non è stato possibile caricare il contenuto. Per favore ricarica.",
"contents.loadDataFailed": "Non è stato possibile caricare i dati. Per favore ricarica.",
"contents.loadFailed": "Non è stato possibile caricare i contenuti. Per favore ricarica.",
"contents.loadVersionFailed": "Non è stato possibile version a new version. Per favore ricarica.",
"contents.loadVersionFailed": "Non è stato possibile creare una nuova versione. Per favore ricarica.",
"contents.localizedFieldDescription": "Il campo '{fieldName}' del contenuto (localizzato).",
"contents.newStatusFieldDescription": "Nuovo stato per l'elemento del contenuto.",
"contents.noReference": "- Nessun collegamento -",
"contents.pendingChangesTextToChange": "Non hai salvato le modifiche.\n\nSe cambi lo stato perderai le modifiche.\n\n**Sei sicuro di voler continuare?**",
"contents.pendingChangesTextToClose": "Non hai salvato le modifiche.\n\nChiudendo il contenuto corrente perderai tutte le modifiche.\n\n**Sei sicuro di voler continuare?**",
@ -362,6 +383,8 @@
"contents.referencesSelectSchema": "Seleziona {schema}",
"contents.refreshTooltip": "Aggiorna i contenuti (CTRL + SHIFT + R)",
"contents.reloaded": "Contenuti aggiornati.",
"contents.removeConfirmText": "Sei sicuro di voler rimuovere il contenuto?",
"contents.removeConfirmTitle": "Cancella il contenuto",
"contents.saveAndPublish": "Salva e pubblica",
"contents.scheduledAt": "alle",
"contents.scheduledAtLabel": "alle",
@ -374,6 +397,16 @@
"contents.statusQueries": "Stato Query",
"contents.stockPhotoEmpty": "Nessuna selezione",
"contents.stockPhotoSearch": "Cerca foto su Unsplash",
"contents.tableHeaders.created": "Creato",
"contents.tableHeaders.createdBy": "Creato da",
"contents.tableHeaders.createdByShort": "Da",
"contents.tableHeaders.id": "Id",
"contents.tableHeaders.lastModified": "Modificato",
"contents.tableHeaders.lastModifiedBy": "Modificato da",
"contents.tableHeaders.lastModifiedByShort": "Da",
"contents.tableHeaders.nextStatus": "Stato successivo",
"contents.tableHeaders.status": "Stato",
"contents.tableHeaders.version": "Versione",
"contents.unsavedChangesText": "Non hai salvato le modifiche. Vuoi salvarle adesso?",
"contents.unsavedChangesTitle": "Modifiche non salvate",
"contents.updated": "Contenuto aggiornato con successo.",
@ -440,14 +473,15 @@
"dashboard.schemasCard": "Schemi",
"dashboard.schemasCardDescription": "Panoramica del modello dei dati di questa app.",
"dashboard.stackedChart": "Istogramma in pila",
"dashboard.supportCard": "Feedback & Assistenza",
"dashboard.supportCard": "Feedback & Supporto",
"dashboard.supportCardDescription": "Fornisci feedback e richiedi funzionalità per aiutarci a migliorare Squidex..",
"dashboard.trafficChart": "Diagramma del traffico delle API",
"dashboard.trafficHeader": "Traffico (MB)",
"dashboard.trafficLimitLabel": "Limite mensile",
"dashboard.trafficSummaryCard": "Riepilogo del traffico delle API",
"dashboard.welcomeText": "Benvenuto su **{app}** dashboard.",
"dashboard.welcomeText": "Benvenuto sulla dashboard **{app}**.",
"dashboard.welcomeTitle": "Ciao {user}",
"eventConsumers.count": "Conteggio",
"eventConsumers.loadFailed": "Non è stato possibile caricare event consumers. Per favore ricarica.",
"eventConsumers.pageTitle": "Eventi degli utenti",
"eventConsumers.position": "Posizione",
@ -510,8 +544,8 @@
"roles.addFailed": "Non è stato possibile aggiungere il ruolo. Per favore ricarica.",
"roles.default.owner": "Hai come amministratore tutte le funzionalità, compreso cancellare le app.",
"roles.default.reader": "Hai un'utenza in sola lettura sia per i contenuti che per le risorse.",
"roles.defaults.developer": "Hai un'utenza che può visualizzare le API, modificare le risorse, i contenuti, gli schema, le regole, i workflows e i pattern.",
"roles.defaults.editor": "Hai un'utenzaCan che può modificare le risorse, i conteuti e visualizzare i workflow.",
"roles.defaults.developer": "Hai un'utenza che può visualizzare le API, modificare le risorse, i contenuti, gli schema, le regole, i workflow e i pattern.",
"roles.defaults.editor": "Hai un'utenza che può modificare le risorse, i conteuti e visualizzare i workflow.",
"roles.deleteConfirmText": "Cancella il ruolo",
"roles.deleteConfirmTitle": "Sei sicuro di voler eliminare il ruolo?",
"roles.loadFailed": "Non è stato possibile caricare i ruoli. Per favore ricarica.",
@ -521,7 +555,7 @@
"roles.revokeFailed": "Non è stato possibile rimuovere il ruolo. Per favore ricarica.",
"roles.roleNamePlaceholder": "Inserisci il nome del ruolo",
"roles.updateFailed": "Non è stato possibile aggiornare il ruolo. Per favore ricarica.",
"rules.actionEdit": "Mdifica l'Azione",
"rules.actionEdit": "Modifica l'Azione",
"rules.cancelFailed": "Non è stato possibile eliminare la regola. Per favore ricarica.",
"rules.create": "Crea un nuova Regola",
"rules.createFailed": "Non è stato possibile creare una nuova regola. Per favore ricarica.",
@ -616,7 +650,7 @@
"schemas.field.localizable": "Consente la localizzazione",
"schemas.field.localizableHint": "Puoi impostare il campo per consentire la localizzazione, ossia che dipende dalla lingua che utilizzi come ad esempio i nomi delle città.",
"schemas.field.localizableMarker": "consente la localizzazione",
"schemas.field.lock": "Blocca e impedisce i cambiamenti",
"schemas.field.lock": "Blocca e impedisci i cambiamenti",
"schemas.field.lockConfirmText": "Attenzione: Bloccare un campo è un'azione irreversibile! Se blocchi il campo non potrai più sbloccarlo o cancellarlo o cambiarlo. Sei sicuro di voler bloccare il campo?",
"schemas.field.lockedMarker": "Bloccato",
"schemas.field.nameHint": "Il nome del campo nelle chiamate API response.",
@ -669,16 +703,23 @@
"schemas.fieldTypes.references.countMin": "Numero Min Elementi",
"schemas.fieldTypes.references.description": "Link ad altri elementi del contenuto.",
"schemas.fieldTypes.references.resolveHint": "Mostra il nome dell'elemento collegato (reference) nella lista dei contenuti quando il numero massimo di elementi è impostato a 1.",
"schemas.fieldTypes.string.characters": "Caratteri",
"schemas.fieldTypes.string.charactersMax": "Max numero di Caratteri",
"schemas.fieldTypes.string.charactersMin": "Min numero di Caratteri",
"schemas.fieldTypes.string.contentType": "Content Type",
"schemas.fieldTypes.string.description": "Titoli, nomi, paragrafi.",
"schemas.fieldTypes.string.length": "Lunghezza",
"schemas.fieldTypes.string.lengthMax": "Lunghezza Max",
"schemas.fieldTypes.string.lengthMin": "Lunghezza Min",
"schemas.fieldTypes.string.pattern": "Regex Pattern",
"schemas.fieldTypes.string.patternMessage": "Messaggio del Pattern",
"schemas.fieldTypes.string.suggestions": "Suggestions",
"schemas.fieldTypes.tags.count": "Items",
"schemas.fieldTypes.tags.countMax": "Max Items",
"schemas.fieldTypes.tags.countMin": "Min Items",
"schemas.fieldTypes.string.suggestions": "Suggerimenti",
"schemas.fieldTypes.string.words": "Parole",
"schemas.fieldTypes.string.wordsMax": "Numero max di Parole",
"schemas.fieldTypes.string.wordsMin": "Numero min di Parole",
"schemas.fieldTypes.tags.count": "Elementi",
"schemas.fieldTypes.tags.countMax": "Numero max di Elementi",
"schemas.fieldTypes.tags.countMin": "Numero min di Elementi",
"schemas.fieldTypes.tags.description": "Formato speciale per i tag.",
"schemas.fieldTypes.ui.description": "Separatore per il pannello delle modifiche della UI.",
"schemas.hideFieldFailed": "Non è stato possibile nascondere il campo. Per favore ricarica.",
@ -724,7 +765,7 @@
"schemas.synchronizeFailed": "Non è stato possibile sincronizzare lo schema. Per favore ricarica.",
"schemas.tabFields": "Campi",
"schemas.tabJson": "Json",
"schemas.tabMore": "Di più",
"schemas.tabMore": "Altro",
"schemas.tabScripts": "Script",
"schemas.tabUI": "UI",
"schemas.ui": "Campi assegnati",
@ -743,7 +784,7 @@
"search.advancedTour": "Fai clic su questa icona per visualizzare il menu della ricerca avanzata!",
"search.customQuery": "Query personalizzata",
"search.fullTextTour": "Cerca contenuti utilizzando la ricerca testuale su tutti i campi e le lingue!",
"search.help": "Ulteriori informazioni sui filtri su [Documentation](https://https://docs.squidex.io/04-guides/02-api.html).",
"search.help": "Ulteriori informazioni sui filtri su [Documentazione](https://https://docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "Le mie query",
"search.nameQuery": "Dai un nome alla query",
"search.queriesEmpty": "Ricerca per {types} e utilizza l'icona <i class=\"icon-star-empty\"></i> nella ricerca per salvare la query per tutti i collaboratori.",

2
backend/i18n/source/frontend_nl.json

@ -368,7 +368,7 @@
"contents.referencesCreatePublish": "Maken en publiceren",
"contents.referencesLink": "Link geselecteerde inhoud ({count})",
"contents.referencesSelectExisting": "Selecteer bestaande",
"contents.referencesSelectSchema": "Selecteer {scheme}",
"contents.referencesSelectSchema": "Selecteer {schema}",
"contents.refreshTooltip": "Ververs inhoud (CTRL + SHIFT + R)",
"contents.reloaded": "Inhoud opnieuw geladen.",
"contents.removeConfirmText": "Wil je de inhoud echt verwijderen?",

9
backend/i18n/translate.bat

@ -1,7 +1,8 @@
cd translator\Squidex.Translator
dotnet run translate check-backend D:\Squidex
dotnet run translate check-frontend D:\Squidex
dotnet run translate check-backend ..\..\..\..
dotnet run translate check-frontend ..\..\..\..
dotnet run translate gen-frontend ..\..\..\..
dotnet run translate gen-backend ..\..\..\..
dotnet run translate gen-frontend D:\Squidex
dotnet run translate gen-backend D:\Squidex

10
backend/i18n/translate.sh

@ -1,10 +1,8 @@
#!/bin/bash
PATH=${1:-/Squidex}
cd translator/Squidex.Translator
/usr/local/share/dotnet/dotnet run translate check-backend $1
/usr/local/share/dotnet/dotnet run translate check-frontend $1
/usr/local/share/dotnet/dotnet run translate check-backend ../../../..
/usr/local/share/dotnet/dotnet run translate check-frontend ../../../..
/usr/local/share/dotnet/dotnet run translate gen-frontend $1
/usr/local/share/dotnet/dotnet run translate gen-backend $1
/usr/local/share/dotnet/dotnet run translate gen-frontend ../../../..
/usr/local/share/dotnet/dotnet run translate gen-backend ../../../..

2
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowsConverter.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Json.Newtonsoft;
namespace Squidex.Domain.Apps.Core.Contents.Json
{
public sealed class WorkflowConverter : JsonClassConverter<Workflows>
public sealed class WorkflowsConverter : JsonClassConverter<Workflows>
{
protected override void WriteValue(JsonWriter writer, Workflows value, JsonSerializer serializer)
{

4
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -10,12 +10,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Fody" Version="6.2.0">
<PackageReference Include="Fody" Version="6.2.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SquidexFreezable.Fody" Version="2.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.1.7" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" />

3
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs

@ -42,8 +42,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules
userEvent.Actor = squidexEvent.Actor;
}
if (userEvent.Actor != null)
{
userEvent.User = await FindUserAsync(userEvent.Actor);
}
}
enrichedEvent.AppId = @event.Payload.AppId;
}

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

@ -18,11 +18,11 @@
<ItemGroup>
<PackageReference Include="Fluid.Core.Squidex" Version="1.0.0-beta" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.24" />
<PackageReference Include="Jint" Version="3.0.0-beta-1828" />
<PackageReference Include="Markdig" Version="0.20.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.6.4" />
<PackageReference Include="NJsonSchema" Version="10.1.18" />
<PackageReference Include="Jint" Version="3.0.0-beta-1884" />
<PackageReference Include="Markdig" Version="0.21.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.1" />
<PackageReference Include="NJsonSchema" Version="10.1.26" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" />

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -17,7 +17,7 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.10.4" />
<PackageReference Include="MongoDB.Driver" Version="2.11.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs

@ -98,7 +98,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
.Hints("The title of the post."))
.AddString("Text", f => f
.AsRichText()
.Length(100)
.Required()
.Hints("The text of the post."))
.AddString("Slug", f => f
@ -124,7 +123,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
.Hints("The title of the page."))
.AddString("Text", f => f
.AsRichText()
.Length(100)
.Required()
.Hints("The text of the page."))
.AddString("Slug", f => f

4
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs

@ -40,13 +40,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
Guard.NotNull(tagService, nameof(tagService));
this.jsonSerializer = jsonSerializer;
this.options = options.Value;
this.tagService = tagService;
this.options = options.Value;
}
public virtual async ValueTask<ClrQuery> ParseQueryAsync(Context context, Q q)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(q, nameof(q));
using (Profiler.TraceMethod<AssetQueryParser>())
{

11
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -25,6 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public sealed class GraphQLModel : IGraphModel
{
private static readonly IDocumentExecuter Executor = new DocumentExecuter();
private readonly Dictionary<DomainId, ContentGraphType> contentTypes = new Dictionary<DomainId, ContentGraphType>();
private readonly PartitionResolver partitionResolver;
private readonly IObjectGraphType assetType;
@ -110,12 +111,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName));
}
public IObjectGraphType GetAssetType()
public IGraphType GetAssetType()
{
return assetType;
}
public IObjectGraphType GetContentType(DomainId schemaId)
public IGraphType GetContentType(DomainId schemaId)
{
return contentTypes.GetOrDefault(schemaId);
}
@ -124,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
Guard.NotNull(context, nameof(context));
var result = await new DocumentExecuter().ExecuteAsync(execution =>
var result = await Executor.ExecuteAsync(execution =>
{
context.Setup(execution);
@ -133,7 +134,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
execution.Query = query.Query;
}).ConfigureAwait(false);
return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray());
var errors = result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray();
return (result.Data, errors);
}
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs

@ -20,9 +20,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IFieldPartitioning ResolvePartition(Partitioning key);
IObjectGraphType GetAssetType();
IGraphType GetAssetType();
IObjectGraphType GetContentType(DomainId schemaId);
IGraphType GetContentType(DomainId schemaId);
IGraphType? GetInputGraphType(ISchemaEntity schema, IField field, string fieldName);

5
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -42,7 +42,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions<ContentOptions> options)
: base(cache)
{
Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
Guard.NotNull(options, nameof(options));
this.jsonSerializer = jsonSerializer;
this.options = options.Value;
}
@ -50,6 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(q, nameof(q));
using (Profiler.TraceMethod<ContentQueryParser>())
{

10
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -18,9 +18,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Elasticsearch.Net" Version="7.7.1" />
<PackageReference Include="Elasticsearch.Net" Version="7.9.0" />
<PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Fody" Version="6.2.0">
<PackageReference Include="Fody" Version="6.2.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -29,12 +29,12 @@
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00005" />
<PackageReference Include="Lucene.Net.Queries" Version="4.8.0-beta00005" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00005" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.2.0">
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.2.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="3.2.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="3.2.2" />
<PackageReference Include="NodaTime" Version="3.0.0" />
<PackageReference Include="Notifo.SDK" Version="1.0.0-alpha8" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

49
backend/src/Squidex.Domain.Users.MongoDb/Infrastructure/MongoPersistedGrantStore.cs

@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
@ -52,9 +53,9 @@ namespace Squidex.Domain.Users.MongoDb.Infrastructure
return await Collection.Find(x => x.SubjectId == subjectId).ToListAsync();
}
public Task StoreAsync(PersistedGrant grant)
public async Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)
{
return Collection.ReplaceOneAsync(x => x.Key == grant.Key, grant, UpsertReplace);
return await Collection.Find(CreateFilter(filter)).ToListAsync();
}
public Task<PersistedGrant> GetAsync(string key)
@ -62,19 +63,51 @@ namespace Squidex.Domain.Users.MongoDb.Infrastructure
return Collection.Find(x => x.Key == key).FirstOrDefaultAsync();
}
public Task RemoveAllAsync(string subjectId, string clientId, string type)
public Task RemoveAllAsync(PersistedGrantFilter filter)
{
return Collection.DeleteManyAsync(x => x.SubjectId == subjectId && x.ClientId == clientId && x.Type == type);
return Collection.DeleteManyAsync(CreateFilter(filter));
}
public Task RemoveAllAsync(string subjectId, string clientId)
public Task RemoveAsync(string key)
{
return Collection.DeleteManyAsync(x => x.SubjectId == subjectId && x.ClientId == clientId);
return Collection.DeleteManyAsync(x => x.Key == key);
}
public Task RemoveAsync(string key)
public Task StoreAsync(PersistedGrant grant)
{
return Collection.DeleteManyAsync(x => x.Key == key);
return Collection.ReplaceOneAsync(x => x.Key == grant.Key, grant, UpsertReplace);
}
private static FilterDefinition<PersistedGrant> CreateFilter(PersistedGrantFilter filter)
{
var filters = new List<FilterDefinition<PersistedGrant>>();
if (!string.IsNullOrWhiteSpace(filter.ClientId))
{
filters.Add(Filter.Eq(x => x.ClientId, filter.ClientId));
}
if (!string.IsNullOrWhiteSpace(filter.SessionId))
{
filters.Add(Filter.Eq(x => x.SessionId, filter.SessionId));
}
if (!string.IsNullOrWhiteSpace(filter.SubjectId))
{
filters.Add(Filter.Eq(x => x.SubjectId, filter.SubjectId));
}
if (!string.IsNullOrWhiteSpace(filter.Type))
{
filters.Add(Filter.Eq(x => x.Type, filter.Type));
}
if (filters.Count > 0)
{
return Filter.And(filters);
}
return new BsonDocument();
}
}
}

4
backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -18,9 +18,9 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="3.1.3" />
<PackageReference Include="IdentityServer4" Version="4.0.4" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="MongoDB.Driver" Version="2.10.4" />
<PackageReference Include="MongoDB.Driver" Version="2.11.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.7.0" />

2
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -16,7 +16,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.7" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SharpPwned.NET" Version="1.0.8" />

2
backend/src/Squidex.Infrastructure.Amazon/Squidex.Infrastructure.Amazon.csproj

@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.3.111.9" />
<PackageReference Include="AWSSDK.S3" Version="3.5.0.9" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup>

6
backend/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj

@ -6,11 +6,11 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.DocumentDB.ChangeFeedProcessor" Version="2.3.0" />
<PackageReference Include="Microsoft.Azure.DocumentDB.Core" Version="2.11.0" />
<PackageReference Include="Microsoft.Azure.DocumentDB.ChangeFeedProcessor" Version="2.3.2" />
<PackageReference Include="Microsoft.Azure.DocumentDB.Core" Version="2.11.6" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="Microsoft.Azure.Storage.Blob" Version="11.1.7" />
<PackageReference Include="Microsoft.Azure.Storage.Blob" Version="11.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />

2
backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj

@ -10,7 +10,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EventStore.Client" Version="5.0.8" />
<PackageReference Include="EventStore.Client" Version="20.6.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup>

2
backend/src/Squidex.Infrastructure.GoogleCloud/Squidex.Infrastructure.GoogleCloud.csproj

@ -10,7 +10,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Cloud.Storage.V1" Version="3.1.0" />
<PackageReference Include="Google.Cloud.Storage.V1" Version="3.3.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

4
backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -13,8 +13,8 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.10.4" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.10.4" />
<PackageReference Include="MongoDB.Driver" Version="2.11.1" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.11.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.11.1" />

2
backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj

@ -10,7 +10,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="6.1.0" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

38
backend/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs

@ -7,6 +7,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
@ -19,20 +20,28 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
{
public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator
{
public Task CreateThumbnailAsync(Stream source, Stream destination, ResizeOptions options)
private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(Math.Max(Environment.ProcessorCount / 4, 1));
public async Task CreateThumbnailAsync(Stream source, Stream destination, ResizeOptions options)
{
Guard.NotNull(source, nameof(source));
Guard.NotNull(destination, nameof(destination));
Guard.NotNull(options, nameof(options));
if (!options.IsValid)
{
source.CopyTo(destination);
return Task.CompletedTask;
return;
}
var w = options.Width ?? 0;
var h = options.Height ?? 0;
await semaphoreSlim.WaitAsync();
try
{
using (var image = Image.Load(source, out var format))
{
var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format);
@ -83,12 +92,17 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
image.Save(destination, encoder);
}
return Task.CompletedTask;
}
finally
{
semaphoreSlim.Release();
}
}
public Task<ImageInfo?> GetImageInfoAsync(Stream source)
{
Guard.NotNull(source, nameof(source));
ImageInfo? result = null;
try
@ -108,7 +122,14 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
return Task.FromResult(result);
}
public Task<ImageInfo> FixOrientationAsync(Stream source, Stream destination)
public async Task<ImageInfo> FixOrientationAsync(Stream source, Stream destination)
{
Guard.NotNull(source, nameof(source));
Guard.NotNull(destination, nameof(destination));
await semaphoreSlim.WaitAsync();
try
{
using (var image = Image.Load(source, out var format))
{
@ -123,7 +144,12 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
image.Save(destination, encoder);
return Task.FromResult(GetImageInfo(image));
return GetImageInfo(image);
}
}
finally
{
semaphoreSlim.Release();
}
}

2
backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs

@ -11,6 +11,8 @@ namespace Squidex.Infrastructure.EventSourcing
{
public bool IsStopped { get; set; }
public int Count { get; set; }
public string Name { get; set; }
public string Error { get; set; }

12
backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs

@ -14,6 +14,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{
public bool IsStopped { get; set; }
public int Count { get; set; }
public string? Error { get; set; }
public string? Position { get; set; }
@ -32,9 +34,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{
}
public EventConsumerState(string? position)
public EventConsumerState(string? position, int count)
{
Position = position;
Count = count;
}
public EventConsumerState Reset()
@ -44,17 +48,17 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
public EventConsumerState Handled(string position)
{
return new EventConsumerState(position);
return new EventConsumerState(position, Count + 1);
}
public EventConsumerState Stopped(Exception? ex = null)
{
return new EventConsumerState(Position) { IsStopped = true, Error = ex?.ToString() };
return new EventConsumerState(Position, Count) { IsStopped = true, Error = ex?.ToString() };
}
public EventConsumerState Started()
{
return new EventConsumerState(Position) { IsStopped = false };
return new EventConsumerState(Position, Count) { IsStopped = false };
}
public EventConsumerInfo ToInfo(string name)

26
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -15,28 +15,28 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="FluentFTP" Version="32.4.3" />
<PackageReference Include="Fody" Version="6.2.0">
<PackageReference Include="FluentFTP" Version="32.4.4" />
<PackageReference Include="Fody" Version="6.2.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="3.1.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.6.4" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.2.0">
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="3.1.7" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.1" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.2.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="3.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.2.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="3.2.2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.2.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NJsonSchema" Version="10.1.18" />
<PackageReference Include="NJsonSchema" Version="10.1.26" />
<PackageReference Include="NodaTime" Version="3.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-rc0003" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" />
<PackageReference Include="Squidex.Text" Version="1.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" />

108
backend/src/Squidex.Shared/Texts.it.resx

@ -71,22 +71,22 @@
<value>Il campo {0} deve essere uguale a {1}.</value>
</data>
<data name="annotations_EmailAddress" xml:space="preserve">
<value>The field {name|lower} is not a valid email address.</value>
<value>Il campo {name|lower} non è un indirizzo email valido.</value>
</data>
<data name="dotnet_annotations_EmailAddress" xml:space="preserve">
<value>The field {0} is not a valid email address.</value>
<value>Il campo {0} non è un indirizzo email valido.</value>
</data>
<data name="annotations_Range" xml:space="preserve">
<value>The field {name|lower} must be between {min} and {max}.</value>
<value>Il campo {name|lower} deve essere tra {min} e {max}.</value>
</data>
<data name="dotnet_annotations_Range" xml:space="preserve">
<value>The field {0} must be between {1} and {2}.</value>
<value>Il campo {0} deve essere tra {1} e {2}.</value>
</data>
<data name="annotations_RegularExpression" xml:space="preserve">
<value>The field {name|lower} is not.</value>
<value>Il campo {name|lower} non è.</value>
</data>
<data name="dotnet_annotations_RegularExpression" xml:space="preserve">
<value>The field {0} is not.</value>
<value>Il campo {0} non è.</value>
</data>
<data name="annotations_Required" xml:space="preserve">
<value>Il campo è {name|lower} obbligatorio.</value>
@ -128,7 +128,7 @@
<value>La lingua è stata già inserita.</value>
</data>
<data name="apps.languages.masterLanguageNoFallbacks" xml:space="preserve">
<value>La lingua master language non ha lingue alternative.</value>
<value>La lingua master non ha lingue alternative.</value>
</data>
<data name="apps.languages.masterLanguageNotOptional" xml:space="preserve">
<value>La lingua master non può essere opzionale.</value>
@ -302,7 +302,7 @@
<value>Esci</value>
</data>
<data name="common.maxCharacters" xml:space="preserve">
<value>Max characters</value>
<value>Numero massimo di caratteri</value>
</data>
<data name="common.maxHeight" xml:space="preserve">
<value>Altezza massima</value>
@ -323,10 +323,10 @@
<value>Larghezza massima</value>
</data>
<data name="common.maxWords" xml:space="preserve">
<value>Max words</value>
<value>Numero massimo di parole</value>
</data>
<data name="common.minCharacters" xml:space="preserve">
<value>Min characters</value>
<value>Numero minimo di caratteri</value>
</data>
<data name="common.minHeight" xml:space="preserve">
<value>Altezza minima</value>
@ -347,7 +347,7 @@
<value>Larghezza massima</value>
</data>
<data name="common.minWords" xml:space="preserve">
<value>Min words</value>
<value>Numero minimo di parole</value>
</data>
<data name="common.name" xml:space="preserve">
<value>Nome</value>
@ -362,13 +362,13 @@
<value>Fallito parsando la query: {message}</value>
</data>
<data name="common.odataFilterNotValid" xml:space="preserve">
<value>OData $filter condizione non valida: {ex.Message}</value>
<value>OData $filter condizione non valida: {message}</value>
</data>
<data name="common.odataNotSupported" xml:space="preserve">
<value>OData operazione non supportata.</value>
</data>
<data name="common.odataSearchNotValid" xml:space="preserve">
<value>OData $search condizione non valida: {ex.Message}</value>
<value>OData $search condizione non valida: {message}</value>
</data>
<data name="common.oldPassword" xml:space="preserve">
<value>Vecchia password</value>
@ -548,7 +548,7 @@
<value>Deve esseer minore o uguale a {max}.</value>
</data>
<data name="contents.validation.maxCharacters" xml:space="preserve">
<value>Must not have more than {max} text character(s).</value>
<value>Il testo non deve avere più di {max} carattere(i).</value>
</data>
<data name="contents.validation.maximumHeight" xml:space="preserve">
<value>L'altezza {height}px deve essere inferiore a {max}px.</value>
@ -566,7 +566,7 @@
<value>Non deve avere più di {max} carattere(i).</value>
</data>
<data name="contents.validation.maxWords" xml:space="preserve">
<value>Must not have more than {max} word(s).</value>
<value>Non deve avere più di {max} parola(e).</value>
</data>
<data name="contents.validation.min" xml:space="preserve">
<value>Deve essere maggiore o uguale a {min}.</value>
@ -587,19 +587,19 @@
<value>Deve avere almeno {min} carattere(i).</value>
</data>
<data name="contents.validation.minNormalCharacters" xml:space="preserve">
<value>Must have at least {min} text character(s).</value>
<value>Deve avere almeno un testo di {min} carattere(i).</value>
</data>
<data name="contents.validation.minWords" xml:space="preserve">
<value>Must have at least {min} word(s).</value>
<value>Deve avere almeno {min} parola(e).</value>
</data>
<data name="contents.validation.mustBeEmpty" xml:space="preserve">
<value>Il valore non deve essere definito.</value>
</data>
<data name="contents.validation.normalCharacterCount" xml:space="preserve">
<value>Must have exactly {count} text character(s).</value>
<value>Deve essere esattamente un testo di {count} carattere(i).</value>
</data>
<data name="contents.validation.normalCharactersBetween" xml:space="preserve">
<value>Must have between {min} and {max} text character(s).</value>
<value>Deve essere un testo tra {min} e {max} carattere(i).</value>
</data>
<data name="contents.validation.notAllowed" xml:space="preserve">
<value>Non è un valore consentito.</value>
@ -620,10 +620,10 @@
<value>Non è noto {fieldType}.</value>
</data>
<data name="contents.validation.wordCount" xml:space="preserve">
<value>Must have exactly {count} word(s).</value>
<value>Deve avere esattamente {count} parola(e).</value>
</data>
<data name="contents.validation.wordsBetween" xml:space="preserve">
<value>Must have between {min} and {max} word(s).</value>
<value>Deve essere tra {min} e {max} parola(e).</value>
</data>
<data name="contents.workflowErorPublishing" xml:space="preserve">
<value>Il workflow del contenuto impedisce la pubblicazione.</value>
@ -632,49 +632,49 @@
<value>Il workflow non consente le modifiche per lo stato {status}</value>
</data>
<data name="dotnet_identity_DefaultEror" xml:space="preserve">
<value>An unknown failure has occurred.</value>
<value>Si è verificato un errore sconosciuto.</value>
</data>
<data name="dotnet_identity_DuplicateEmail" xml:space="preserve">
<value>Email is already taken.</value>
<value>Email già in uso.</value>
</data>
<data name="dotnet_identity_DuplicateUserName" xml:space="preserve">
<value>User name is already taken.</value>
<value>Nome utente già in uso.</value>
</data>
<data name="dotnet_identity_InvalidEmail" xml:space="preserve">
<value>Email is invalid.</value>
<value>L'Email non è valida.</value>
</data>
<data name="dotnet_identity_InvalidUserName" xml:space="preserve">
<value>User name '{0}' is invalid, can only contain letters or digits.</value>
<value>Il nome utente '{0}' non è valido, può contenere solo lettere e numeri.</value>
</data>
<data name="dotnet_identity_LoginAlreadyAssociated" xml:space="preserve">
<value>A user with this login already exists.</value>
<value>Esiste già un utente con queste credenziali di accesso.</value>
</data>
<data name="dotnet_identity_PasswordMismatch" xml:space="preserve">
<value>Incorrect password.</value>
<value>Password errata.</value>
</data>
<data name="dotnet_identity_PasswordRequiresDigit" xml:space="preserve">
<value>Passwords must have at least one digit ('0'-'9').</value>
<value>La Password devono contenere almeno un numero ('0'-'9').</value>
</data>
<data name="dotnet_identity_PasswordRequiresLower" xml:space="preserve">
<value>Passwords must have at least one lowercase ('a'-'z').</value>
<value>La passowrd deve avere almeno una lettera minuscola ('a'-'z').</value>
</data>
<data name="dotnet_identity_PasswordRequiresNonAlphanumeric" xml:space="preserve">
<value>Passwords must have at least one non alphanumeric character.</value>
<value>La passowrd deve avere almeno un carattere non alfanumerico.</value>
</data>
<data name="dotnet_identity_PasswordRequiresUniqueChars" xml:space="preserve">
<value>Passwords must use at least {0} different characters.</value>
<value>La password deve essere composta almeno da {0} caratteri differenti.</value>
</data>
<data name="dotnet_identity_PasswordRequiresUpper" xml:space="preserve">
<value>Passwords must have at least one uppercase ('A'-'Z').</value>
<value>La password deve avere almeno una lettera maiuscola ('A'-'Z').</value>
</data>
<data name="dotnet_identity_PasswordTooShort" xml:space="preserve">
<value>Passwords is too short.</value>
<value>La password è troppo corta.</value>
</data>
<data name="dotnet_identity_PwnedError" xml:space="preserve">
<value>This password has previously appeared in a data breach and should never be used. If you have ever used it anywhere before, change it!</value>
<value>Questa password è apparsa in un database di password compromesso e non dovrebbe più essere utilizzata. Se l'hai usata, cambiala!</value>
</data>
<data name="dotnet_identity_UserLockedOut" xml:space="preserve">
<value>User is locked out.</value>
<value>L'utente è bloccato.</value>
</data>
<data name="exception.invalidJsonQuery" xml:space="preserve">
<value>La query Json non è valida: {message}</value>
@ -719,37 +719,37 @@
<value>aggiornata la lingua {[Language]}</value>
</data>
<data name="history.apps.patternAdded" xml:space="preserve">
<value>adggiunto pattern {[Name]}</value>
<value>ha aggiunto pattern {[Name]}</value>
</data>
<data name="history.apps.patternDeleted" xml:space="preserve">
<value>eliminato pattern {[PatternId]}</value>
<value>ha eliminato pattern {[PatternId]}</value>
</data>
<data name="history.apps.patternUpdated" xml:space="preserve">
<value>modificato pattern {[Name]}</value>
<value>ha modificato pattern {[Name]}</value>
</data>
<data name="history.apps.planChanged" xml:space="preserve">
<value>cambiato il piano in {[Plan]}</value>
<value>ha cambiato il piano in {[Plan]}</value>
</data>
<data name="history.apps.planReset" xml:space="preserve">
<value>riconfigurato il piano</value>
<value>ha riconfigurato il piano</value>
</data>
<data name="history.apps.roleAdded" xml:space="preserve">
<value>aggiunto il ruolo {[Name]}</value>
<value>ha aggiunto il ruolo {[Name]}</value>
</data>
<data name="history.apps.roleDeleted" xml:space="preserve">
<value>eliminato role {[Name]}</value>
<value>ha eliminato role {[Name]}</value>
</data>
<data name="history.apps.roleUpdated" xml:space="preserve">
<value>aggiornato role {[Name]}</value>
<value>ha aggiornato role {[Name]}</value>
</data>
<data name="history.assets.replaced" xml:space="preserve">
<value>risorsa sostituita.</value>
<value>ha sostituito la risorsa.</value>
</data>
<data name="history.assets.updated" xml:space="preserve">
<value>risorsa aggiornata.</value>
<value>ha aggiornato la risorsa.</value>
</data>
<data name="history.assets.uploaded" xml:space="preserve">
<value>risorsa caricata.</value>
<value>ha caricato la risorsa.</value>
</data>
<data name="history.contents.created" xml:space="preserve">
<value>creato il contenuto {[Schema]}.</value>
@ -800,22 +800,22 @@
<value>riordinati i campi dello schema {[Name]}.</value>
</data>
<data name="history.schemas.fieldUpdated" xml:space="preserve">
<value>aggiornato il campo {[Field]} dello schema {[Name]}.</value>
<value>ha aggiornato il campo {[Field]} dello schema {[Name]}.</value>
</data>
<data name="history.schemas.published" xml:space="preserve">
<value>pubblicato lo schema {[Name]}.</value>
<value>ha pubblicato lo schema {[Name]}.</value>
</data>
<data name="history.schemas.scriptsConfigured" xml:space="preserve">
<value>configurato lo script per lo schema {[Name]}.</value>
<value>ha configurato lo script per lo schema {[Name]}.</value>
</data>
<data name="history.schemas.unpublished" xml:space="preserve">
<value>rimosso dalla pubblicazione lo schema {[Name]}.</value>
<value>ha rimosso dalla pubblicazione lo schema {[Name]}.</value>
</data>
<data name="history.schemas.updated" xml:space="preserve">
<value>aggiornato lo schema {[Name]}.</value>
<value>ha aggiornato lo schema {[Name]}.</value>
</data>
<data name="history.statusChanged" xml:space="preserve">
<value>cambiato lo stato del contenuto {[Schema]} in {[Status]}.</value>
<value>ha cambiato lo stato del contenuto {[Schema]} in {[Status]}.</value>
</data>
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github.</value>
@ -1028,7 +1028,7 @@
<value>Profile modificato</value>
</data>
<data name="users.profile.hideProfile" xml:space="preserve">
<value>Non mostrare il mio profilo agli altri utentiDo not show my profile to other users</value>
<value>Non mostrare il mio profilo agli altri utenti</value>
</data>
<data name="users.profile.loginsTitle" xml:space="preserve">
<value>Login</value>

4
backend/src/Squidex.Shared/Texts.nl.resx

@ -362,13 +362,13 @@
<value>Ontleden zoekopdracht: {message}</value>
</data>
<data name="common.odataFilterNotValid" xml:space="preserve">
<value>OData $ filterclausule niet geldig: {ex.Message}</value>
<value>OData $ filterclausule niet geldig: {message}</value>
</data>
<data name="common.odataNotSupported" xml:space="preserve">
<value>OData-bewerking wordt niet ondersteund.</value>
</data>
<data name="common.odataSearchNotValid" xml:space="preserve">
<value>OData $ zoekclausule niet geldig: {ex.Message}</value>
<value>OData $ zoekclausule niet geldig: {message}</value>
</data>
<data name="common.oldPassword" xml:space="preserve">
<value>Oud wachtwoord</value>

4
backend/src/Squidex.Shared/Texts.resx

@ -362,13 +362,13 @@
<value>Failed to parse query: {message}</value>
</data>
<data name="common.odataFilterNotValid" xml:space="preserve">
<value>OData $filter clause not valid: {ex.Message}</value>
<value>OData $filter clause not valid: {message}</value>
</data>
<data name="common.odataNotSupported" xml:space="preserve">
<value>OData operation is not supported.</value>
</data>
<data name="common.odataSearchNotValid" xml:space="preserve">
<value>OData $search clause not valid: {ex.Message}</value>
<value>OData $search clause not valid: {message}</value>
</data>
<data name="common.oldPassword" xml:space="preserve">
<value>Old password</value>

2
backend/src/Squidex.Web/Squidex.Web.csproj

@ -12,7 +12,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="6.2.0">
<PackageReference Include="Fody" Version="6.2.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

16
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -52,7 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/graphql/")]
[ApiPermission]
[ApiPermissionOrAnonymous]
[ApiCosts(2)]
public async Task<IActionResult> GetGraphQL(string app, [FromQuery] GraphQLGetDto? queries = null)
{
@ -84,7 +84,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPost]
[Route("content/{app}/graphql/")]
[ApiPermission]
[ApiPermissionOrAnonymous]
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQL(string app, [FromBody] GraphQLPostDto query)
{
@ -116,7 +116,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpPost]
[Route("content/{app}/graphql/batch")]
[ApiPermission]
[ApiPermissionOrAnonymous]
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQLBatch(string app, [FromBody] GraphQLPostDto[] batch)
{
@ -149,7 +149,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpGet]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids)
{
@ -178,7 +178,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpPost]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query)
{
@ -209,7 +209,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpGet]
[Route("content/{app}/{name}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] string? ids = null, [FromQuery] string? q = null)
{
@ -241,7 +241,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpPost]
[Route("content/{app}/{name}/query")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetContentsPost(string app, string name, [FromBody] QueryDto query)
{
@ -273,7 +273,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpGet]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermission]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, string id)
{

2
backend/src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs

@ -17,6 +17,8 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
public bool IsResetting { get; set; }
public int Count { get; set; }
public string Name { get; set; }
public string? Error { get; set; }

9
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs

@ -37,9 +37,6 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddDataProtection().SetApplicationName("Squidex");
services.AddSingleton(GetApiResources());
services.AddSingleton(GetIdentityResources());
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders();
@ -63,13 +60,13 @@ namespace Squidex.Areas.IdentityServer.Config
options.UserInteraction.ErrorUrl = "/error/";
})
.AddAspNetIdentity<IdentityUser>()
.AddInMemoryApiResources(GetApiResources())
.AddInMemoryApiScopes(GetApiScopes())
.AddInMemoryIdentityResources(GetIdentityResources());
}
private static IEnumerable<ApiResource> GetApiResources()
private static IEnumerable<ApiScope> GetApiScopes()
{
yield return new ApiResource(Constants.ApiScope)
yield return new ApiScope(Constants.ApiScope)
{
UserClaims = new List<string>
{

9
backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer4;
using IdentityServer4.Models;
@ -109,9 +108,9 @@ namespace Squidex.Areas.IdentityServer.Config
Constants.RoleScope,
Constants.PermissionsScope
},
Claims = new List<Claim>
Claims = new List<ClientClaim>
{
new Claim(OpenIdClaims.Subject, user.Id)
new ClientClaim(OpenIdClaims.Subject, user.Id)
}
};
}
@ -228,9 +227,9 @@ namespace Squidex.Areas.IdentityServer.Config
Constants.RoleScope,
Constants.PermissionsScope
},
Claims = new List<Claim>
Claims = new List<ClientClaim>
{
new Claim(SquidexClaimTypes.Permissions, Permissions.All)
new ClientClaim(SquidexClaimTypes.Permissions, Permissions.All)
}
};
}

2
backend/src/Squidex/Config/Domain/SerializationServices.cs

@ -54,7 +54,7 @@ namespace Squidex.Config.Domain
new SchemaConverter(),
new StatusConverter(),
new StringEnumConverter(),
new WorkflowConverter(),
new WorkflowsConverter(),
new WorkflowStepConverter());
settings.NullValueHandling = NullValueHandling.Ignore;

3
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -122,9 +122,6 @@ namespace Squidex.Config.Domain
.As<IContentRepository>().As<ISnapshotStore<ContentState, DomainId>>();
services.AddSingletonAs<MongoTextIndexerState>()
.AsSelf();
services.AddSingletonAs(c => new CachingTextIndexerState(c.GetRequiredService<MongoTextIndexerState>()))
.As<ITextIndexerState>();
var registration = services.FirstOrDefault(x => x.ServiceType == typeof(IPersistedGrantStore));

34
backend/src/Squidex/Squidex.csproj

@ -34,32 +34,32 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="3.1.1" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="3.1.3" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="IdentityServer4" Version="3.1.3" />
<PackageReference Include="IdentityServer4" Version="4.0.4" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.4" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="3.1.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="3.1.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.7" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="1.1.1" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.4" />
<PackageReference Include="Microsoft.OData.Core" Version="7.6.4" />
<PackageReference Include="Microsoft.Orleans.Core" Version="3.2.0" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="3.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.2.0" />
<PackageReference Include="MongoDB.Driver" Version="2.10.4" />
<PackageReference Include="Namotion.Reflection" Version="1.0.12" />
<PackageReference Include="Microsoft.OData.Core" Version="7.7.1" />
<PackageReference Include="Microsoft.Orleans.Core" Version="3.2.2" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="3.2.2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="3.2.2" />
<PackageReference Include="MongoDB.Driver" Version="2.11.1" />
<PackageReference Include="Namotion.Reflection" Version="1.0.13" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NJsonSchema" Version="10.1.18" />
<PackageReference Include="NSwag.AspNetCore" Version="13.6.0" />
<PackageReference Include="NJsonSchema" Version="10.1.26" />
<PackageReference Include="NSwag.AspNetCore" Version="13.7.0" />
<PackageReference Include="OpenCover" Version="4.7.922" PrivateAssets="all" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="3.1.7" />
<PackageReference Include="OrleansDashboard" Version="3.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.6.1" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="5.1.0" />
<PackageReference Include="ReportGenerator" Version="4.6.6" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="5.4.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Runtime" Version="4.3.1" />

6
backend/src/Squidex/appsettings.json

@ -37,9 +37,9 @@
"strongETag": false,
/*
* Restrict the surrogate keys to 17KB.
* Restrict the surrogate keys to the number of characters.
*/
"maxSurrogateKeysSize": 8000,
"maxSurrogateKeysSize": 0,
"replicated": {
/*
@ -51,7 +51,7 @@
"languages": {
/*
* Use custom langauges where the key is the language code and the value is the english name.
* Use custom languages where the key is the language code and the value is the english name.
*/
"custom": ""
},

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs

@ -203,7 +203,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
}
[Fact]
public void Should_same_langauges_if_removing_single_language()
public void Should_same_languages_if_removing_single_language()
{
var config_1 = config_0.Remove(Language.EN);

23
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs

@ -5,11 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Contents.Json;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Core.Model.Contents
@ -18,6 +21,26 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
{
[Fact]
public void Should_serialize_and_deserialize()
{
var workflow = new Workflow(
Status.Draft, new Dictionary<Status, WorkflowStep>
{
[Status.Draft] = new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Published] = WorkflowTransition.When("Expression", "Role1", "Role2")
},
"#00ff00",
NoUpdate.When("Expression", "Role1", "Role2"))
}, new List<DomainId> { DomainId.NewGuid() }, "MyName");
var serialized = workflow.SerializeAndDeserialize();
serialized.Should().BeEquivalentTo(workflow);
}
[Fact]
public void Should_serialize_and_deserialize_default()
{
var workflow = Workflow.Default;

17
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.TestHelpers;
@ -19,21 +18,11 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
[Fact]
public void Should_serialize_and_deserialize()
{
var workflow = new Workflow(
Status.Draft, new Dictionary<Status, WorkflowStep>
{
[Status.Draft] = new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Published] = WorkflowTransition.When("Expression", "Role1", "Role2")
},
"#00ff00",
NoUpdate.When("Expression", "Role1", "Role2"))
}, new List<DomainId> { DomainId.NewGuid() }, "MyName");
var workflows = Workflows.Empty.Add(DomainId.NewGuid(), "my-workflow");
var serialized = workflow.SerializeAndDeserialize();
var serialized = workflows.SerializeAndDeserialize();
serialized.Should().BeEquivalentTo(workflow);
serialized.Should().BeEquivalentTo(workflows);
}
}
}

153
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs

@ -0,0 +1,153 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Shared.Users;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.HandleRules
{
public class EventEnricherTests
{
private readonly IUserResolver userResolver = A.Fake<IUserResolver>();
private readonly EventEnricher sut;
public EventEnricherTests()
{
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
sut = new EventEnricher(cache, userResolver);
}
[Fact]
public async Task Should_enrich_with_timestamp()
{
var timestamp = SystemClock.Instance.GetCurrentInstant().WithoutMs();
var @event =
Envelope.Create<AppEvent>(new ContentCreated())
.SetTimestamp(timestamp);
var enrichedEvent = new EnrichedContentEvent();
await sut.EnrichAsync(enrichedEvent, @event);
Assert.Equal(timestamp, enrichedEvent.Timestamp);
}
[Fact]
public async Task Should_enrich_with_appId()
{
var appId = NamedId.Of(DomainId.NewGuid(), "my-app");
var @event =
Envelope.Create<AppEvent>(new ContentCreated
{
AppId = appId
});
var enrichedEvent = new EnrichedContentEvent();
await sut.EnrichAsync(enrichedEvent, @event);
Assert.Equal(appId, enrichedEvent.AppId);
}
[Fact]
public async Task Should_not_enrich_with_user_if_token_is_null()
{
RefToken actor = null!;
var @event =
Envelope.Create<AppEvent>(new ContentCreated
{
Actor = actor
});
var enrichedEvent = new EnrichedContentEvent();
await sut.EnrichAsync(enrichedEvent, @event);
Assert.Null(enrichedEvent.User);
A.CallTo(() => userResolver.FindByIdAsync(A<string>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_enrich_with_user()
{
var actor = new RefToken(RefTokenType.Client, "me");
var user = A.Dummy<IUser>();
A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier))
.Returns(user);
var @event =
Envelope.Create<AppEvent>(new ContentCreated
{
Actor = actor
});
var enrichedEvent = new EnrichedContentEvent();
await sut.EnrichAsync(enrichedEvent, @event);
Assert.Equal(user, enrichedEvent.User);
A.CallTo(() => userResolver.FindByIdAsync(A<string>._))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_enrich_with_user_and_cache()
{
var actor = new RefToken(RefTokenType.Client, "me");
var user = A.Dummy<IUser>();
A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier))
.Returns(user);
var @event1 =
Envelope.Create<AppEvent>(new ContentCreated
{
Actor = actor
});
var @event2 =
Envelope.Create<AppEvent>(new ContentCreated
{
Actor = actor
});
var enrichedEvent1 = new EnrichedContentEvent();
var enrichedEvent2 = new EnrichedContentEvent();
await sut.EnrichAsync(enrichedEvent1, @event1);
await sut.EnrichAsync(enrichedEvent2, @event2);
Assert.Equal(user, enrichedEvent1.User);
Assert.Equal(user, enrichedEvent2.User);
A.CallTo(() => userResolver.FindByIdAsync(A<string>._))
.MustHaveHappenedOnceExactly();
}
}
}

10
backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -12,16 +12,16 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="6.0.1" />
<PackageReference Include="FakeItEasy" Version="6.2.1" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

2
backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs

@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
new SchemaConverter(),
new StatusConverter(),
new StringEnumConverter(),
new WorkflowConverter(),
new WorkflowsConverter(),
new WorkflowStepConverter()),
TypeNameHandling = typeNameHandling

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -17,18 +17,18 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Tests\Squidex.Domain.Apps.Core.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="6.0.1" />
<PackageReference Include="FakeItEasy" Version="6.2.1" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="GraphQL" Version="3.0.0" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="Lorem.Universal.Net" Version="3.0.64" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

6
backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj

@ -13,14 +13,14 @@
<ProjectReference Include="..\..\src\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="6.0.1" />
<PackageReference Include="FakeItEasy" Version="6.2.1" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

6
backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs

@ -186,7 +186,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(eventSubscription, @event);
AssetGrainState(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null });
AssetGrainState(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null, Count = 1 });
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly);
@ -208,7 +208,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(eventSubscription, @event);
AssetGrainState(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null });
AssetGrainState(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null, Count = 1 });
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly);
@ -230,7 +230,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(eventSubscription, @event);
AssetGrainState(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null });
AssetGrainState(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null, Count = 1 });
A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly);

14
backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -20,13 +20,13 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="6.0.1" />
<PackageReference Include="FakeItEasy" Version="6.2.1" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Google.Cloud.Storage.V1" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="3.2.1" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.2.0">
<PackageReference Include="Google.Cloud.Storage.V1" Version="3.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="3.2.2" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="3.2.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@ -34,7 +34,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

10
backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj

@ -11,14 +11,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="6.0.1" />
<PackageReference Include="IdentityServer4" Version="3.1.3" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.1.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="FakeItEasy" Version="6.2.1" />
<PackageReference Include="IdentityServer4" Version="4.0.4" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

3
frontend/app/features/administration/pages/event-consumers/event-consumer.component.html

@ -6,6 +6,9 @@
{{eventConsumer.name}}
</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.count}}</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
</td>

3
frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -22,6 +22,9 @@
<th class="cell-auto">
{{ 'common.name' | sqxTranslate }}
</th>
<th class="cell-auto-right">
{{ 'eventConsumers.count' | sqxTranslate }}
</th>
<th class="cell-auto-right">
{{ 'eventConsumers.position' | sqxTranslate }}
</th>

2
frontend/app/features/administration/services/event-consumers.service.spec.ts

@ -134,6 +134,7 @@ describe('EventConsumersService', () => {
return {
name: `event-consumer${id}`,
position: `position${id}`,
count: id,
isStopped: true,
isResetting: true,
error: `failure${id}`,
@ -151,6 +152,7 @@ export function createEventConsumer(id: number, suffix = '') {
return new EventConsumerDto(links,
`event-consumer${id}`,
id,
true,
true,
`failure${id}${suffix}`,

2
frontend/app/features/administration/services/event-consumers.service.ts

@ -30,6 +30,7 @@ export class EventConsumerDto {
constructor(links: ResourceLinks,
public readonly name: string,
public readonly count: number,
public readonly isStopped?: boolean,
public readonly isResetting?: boolean,
public readonly error?: string,
@ -104,6 +105,7 @@ function parseEventConsumer(response: any): EventConsumerDto {
return new EventConsumerDto(
response._links,
response.name,
response.count,
response.isStopped,
response.isResetting,
response.error,

6
frontend/app/features/content/pages/content/content-history-page.component.ts

@ -79,7 +79,11 @@ export class ContentHistoryPageComponent extends ResourceOwner implements OnInit
}
public createDraft() {
this.contentsState.createDraft(this.content);
this.contentPage.checkPendingChangesBeforeChangingStatus().pipe(
filter(x => !!x),
switchMap(d => this.contentsState.createDraft(this.content)),
onErrorResumeNext())
.subscribe();
}
public delete() {

1
frontend/app/features/content/shared/forms/assets-editor.component.scss

@ -40,6 +40,7 @@
font-weight: normal;
padding: 5px .5rem;
text-align: center;
text-decoration: none;
transition: border-color .4s ease;
}

3
frontend/app/features/content/shared/references/references-editor.component.scss

@ -25,10 +25,11 @@
border: 2px dashed darken($color-border, 10%);
color: darken($color-border, 30%);
cursor: pointer;
font-size: 1.2rem;
font-size: 1rem;
font-weight: normal;
padding: 1rem;
text-align: center;
text-decoration: none;
transition: border-color .4s ease;
}

36
frontend/app/framework/angular/forms/control-errors.component.ts

@ -9,6 +9,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnC
import { AbstractControl, FormArray, FormGroupDirective } from '@angular/forms';
import { fadeAnimation, LocalizerService, StatefulComponent, Types } from '@app/framework/internal';
import { merge } from 'rxjs';
import { formatError } from './error-formatting';
interface State {
// The error messages to show.
@ -121,40 +122,7 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
if (this.control && this.control.invalid && this.isTouched && this.control.errors) {
for (const key in <any>this.control.errors) {
if (this.control.errors.hasOwnProperty(key)) {
let type = key.toLowerCase();
if (Types.isString(this.control.value)) {
if (type === 'minlength') {
type = 'minlengthstring';
}
if (type === 'maxlength') {
type = 'maxlengthstring';
}
if (type === 'exactlylength') {
type = 'exactlylengthstring';
}
if (type === 'betweenlength') {
type = 'betweenlengthstring';
}
}
const error = this.control.errors[key];
let message: string | null = null;
if (Types.isString(error['message'])) {
message = this.localizer.get(error['message']);
}
if (!message) {
const args = { ...error, field: this.displayFieldName };
message = this.localizer.getOrKey(`validation.${type}`, args);
}
const message = formatError(this.localizer, this.displayFieldName, key, this.control.errors[key], this.control.value);
if (message) {
errors.push(message);

2
frontend/app/framework/angular/forms/editors/date-time-editor.component.html

@ -27,7 +27,7 @@
</button>
</div>
<div class="form-group" *ngIf="!isDateTimeMode && shouldShowDateButtons">
<button type="button" class="btn btn-text-secondary" [disabled]="snapshot.isDisabled" (click)="writeNow()" title="i18n:common.dateTimeEditor.todayTooltip">
<button type="button" class="btn btn-text-secondary" [disabled]="snapshot.isDisabled" (click)="writeToday()" title="i18n:common.dateTimeEditor.todayTooltip">
{{ 'common.dateTimeEditor.today' | sqxTranslate }}
</button>
</div>

10
frontend/app/framework/angular/forms/editors/date-time-editor.component.ts

@ -142,6 +142,16 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
this.updateControls();
}
public writeToday() {
this.dateTime = new DateTime(DateHelper.getLocalDate(DateTime.today().raw));
this.updateControls();
this.callChangeFormatted();
this.callTouched();
return false;
}
public writeNow() {
this.dateTime = DateTime.now();

191
frontend/app/framework/angular/forms/error-formatting.spec.ts

@ -0,0 +1,191 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { LocalizerService } from './../../services/localizer.service';
import { formatError } from './error-formatting';
import { ValidatorsEx } from './validators';
describe('formatErrors', () => {
const localizer = new LocalizerService({
'users.passwordConfirmValidationMessage': 'Passwords must be the same.',
'validation.between': '{field} must be between \'{min}\' and \'{max}\'.',
'validation.betweenlength': '{field|upper} must have between {minlength} and {maxlength} item(s).',
'validation.betweenlengthstring': '{field|upper} must have between {minlength} and {maxlength} character(s).',
'validation.email': '{field|upper} must be an email address.',
'validation.exactly': '{field|upper} must be exactly \'{expected}\'.',
'validation.exactlylength': '{field|upper} must have exactly {expected} item(s).',
'validation.exactlylengthstring': '{field|upper} must have exactly {expected} character(s).',
'validation.match': '{message}',
'validation.max': '{field|upper} must be less or equal to \'{max}\'.',
'validation.maxlength': '{field|upper} must not have more than {requiredlength} item(s).',
'validation.maxlengthstring': '{field|upper} must not have more than {requiredlength} character(s).',
'validation.min': '{field|upper} must be greater or equal to \'{min}\'.',
'validation.minlength': '{field|upper} must have at least {requiredlength} item(s).',
'validation.minlengthstring': '{field|upper} must have at least {requiredlength} character(s).',
'validation.pattern': '{field|upper} does not match to the pattern.',
'validation.patternmessage': '{message}',
'validation.required': '{field|upper} is required.',
'validation.requiredTrue': '{field|upper} is required.',
'validation.uniquestrings': '{field|upper} must not contain duplicate values.',
'validation.validarrayvalues': '{field|upper} contains an invalid value: {invalidvalue}.',
'validation.validdatetime': '{field|upper} is not a valid date time.',
'validation.validvalues': '{field|upper} is not a valid value.'
});
it('should format min', () => {
const error = validate(1, Validators.min(2));
expect(error).toEqual('MY_FIELD must be greater or equal to \'2\'.');
});
it('should format max', () => {
const error = validate(3, Validators.max(2));
expect(error).toEqual('MY_FIELD must be less or equal to \'2\'.');
});
it('should format required', () => {
const error = validate(undefined, Validators.required);
expect(error).toEqual('MY_FIELD is required.');
});
it('should format requiredTrue', () => {
const error = validate(undefined, Validators.requiredTrue);
expect(error).toEqual('MY_FIELD is required.');
});
it('should format email', () => {
const error = validate('invalid', Validators.email);
expect(error).toEqual('MY_FIELD must be an email address.');
});
it('should format minLength string', () => {
const error = validate('x', Validators.minLength(2));
expect(error).toEqual('MY_FIELD must have at least 2 character(s).');
});
it('should format maxLength string', () => {
const error = validate('xxx', Validators.maxLength(2));
expect(error).toEqual('MY_FIELD must not have more than 2 character(s).');
});
it('should format minLength array', () => {
const error = validate([1], Validators.minLength(2));
expect(error).toEqual('MY_FIELD must have at least 2 item(s).');
});
it('should format maxLength array', () => {
const error = validate([1, 1, 1], Validators.maxLength(2));
expect(error).toEqual('MY_FIELD must not have more than 2 item(s).');
});
it('should format match', () => {
const error = validate('123', Validators.pattern('[A-Z]'));
expect(error).toEqual('MY_FIELD does not match to the pattern.');
});
it('should format match with message', () => {
const error = validate('123', ValidatorsEx.pattern('[A-Z]', 'Custom Message'));
expect(error).toEqual('Custom Message');
});
it('should format between exactly', () => {
const error = validate(2, ValidatorsEx.between(3, 3));
expect(error).toEqual('MY_FIELD must be exactly \'3\'.');
});
it('should format between range', () => {
const error = validate(2, ValidatorsEx.between(3, 5));
expect(error).toEqual('MY_FIELD must be between \'3\' and \'5\'.');
});
it('should format betweenLength string exactly', () => {
const error = validate('xx', ValidatorsEx.betweenLength(3, 3));
expect(error).toEqual('MY_FIELD must have exactly 3 character(s).');
});
it('should format betweenLength string range', () => {
const error = validate('xx', ValidatorsEx.betweenLength(3, 5));
expect(error).toEqual('MY_FIELD must have between 3 and 5 character(s).');
});
it('should format betweenLength array exactly', () => {
const error = validate([1], ValidatorsEx.betweenLength(3, 3));
expect(error).toEqual('MY_FIELD must have exactly 3 item(s).');
});
it('should format betweenLength array range', () => {
const error = validate([1, 1], ValidatorsEx.betweenLength(3, 5));
expect(error).toEqual('MY_FIELD must have between 3 and 5 item(s).');
});
it('should format validDateTime', () => {
const error = validate('invalid', ValidatorsEx.validDateTime());
expect(error).toEqual('MY_FIELD is not a valid date time.');
});
it('should format validValues', () => {
const error = validate(5, ValidatorsEx.validValues([1, 2, 3]));
expect(error).toEqual('MY_FIELD is not a valid value.');
});
it('should format validArrayValues', () => {
const error = validate([2, 4], ValidatorsEx.validArrayValues([1, 2, 3]));
expect(error).toEqual('MY_FIELD contains an invalid value: 4.');
});
it('should format uniqueStrings', () => {
const error = validate(['1', '2', '2', '3'], ValidatorsEx.uniqueStrings());
expect(error).toEqual('MY_FIELD must not contain duplicate values.');
});
it('should format match', () => {
const formControl1 = new FormControl(1);
const formControl2 = new FormControl(2);
const formGroup = new FormGroup({
field1: formControl1,
field2: formControl2
});
const formError = ValidatorsEx.match('field2', 'i18n:users.passwordConfirmValidationMessage')!(formControl1)!;
const formMessage = formatError(localizer, 'MY_FIELD', Object.keys(formError)[0], Object.values(formError)[0], undefined);
expect(formMessage).toEqual('Passwords must be the same.');
formGroup.reset();
});
function validate(value: any, validator: ValidatorFn) {
const formControl = new FormControl(value);
const formError = validator(formControl)!;
const formMessage = formatError(localizer, 'MY_FIELD', Object.keys(formError)[0], Object.values(formError)[0], value);
return formMessage;
}
});

47
frontend/app/framework/angular/forms/error-formatting.ts

@ -0,0 +1,47 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights r vbeserved
*/
import { Types } from '@app/framework/internal';
import { LocalizerService } from '@app/shared';
export function formatError(localizer: LocalizerService, field: string, type: string, properties: any, value: any, errors?: any) {
type = type.toLowerCase();
if (Types.isString(value)) {
if (type === 'minlength') {
type = 'minlengthstring';
}
if (type === 'maxlength') {
type = 'maxlengthstring';
}
if (type === 'exactlylength') {
type = 'exactlylengthstring';
}
if (type === 'betweenlength') {
type = 'betweenlengthstring';
}
}
let message: string | null = properties['message'];
if (!Types.isString(message) && errors) {
message = errors[type];
}
if (!Types.isString(message)) {
message = `validation.${type}`;
}
const args = { ...properties, field };
message = localizer.getOrKey(message, args);
return message;
}

5
frontend/app/framework/angular/forms/form-error.component.html

@ -1,9 +1,10 @@
<ng-container *ngIf="show">
<div [class.form-bubble]="bubble">
<div class="form-alert form-alert-error" [class.closeable]="closeable">
<a class="form-alert-close" (click)="close()">
<span class="form-alert-close" (click)="close()">
<i class="icon-close"></i>
</a>
</span>
<div [innerHTML]="error | sqxTranslate | sqxMarkdown"></div>
</div>
</div>

9
frontend/app/framework/services/localizer.service.spec.ts

@ -12,6 +12,7 @@ describe('LocalizerService', () => {
simple: 'Simple Result',
withLowerVar: 'Var: {var|lower}.',
withUpperVar: 'Var: {var|upper}.',
withMultiple: 'Text1: {text1}, Text2: {Text2}.',
withVar: 'Var: {var}.'
};
@ -82,4 +83,12 @@ describe('LocalizerService', () => {
expect(result).toEqual('Var: Upper.');
});
it('should return text with multiple variables', () => {
const localizer = new LocalizerService(translations);
const result = localizer.get('withMultiple', { Text1: 'Hello', Text2: 'World' });
expect(result).toEqual('Text1: Hello, Text2: World.');
});
});

63
frontend/app/framework/services/localizer.service.ts

@ -6,6 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { compareStrings } from '../utils/array-helper';
export const LocalizerServiceFactory = (translations: Object) => {
return new LocalizerService(translations);
@ -56,46 +57,62 @@ export class LocalizerService {
return text;
}
private replaceVariables(text: string, args: ReadonlyArray<object>): string {
while (true) {
const indexOfStart = text.indexOf('{');
private replaceVariables(text: string, args: object): string {
text = text.replace(/{[^}]*}/g, (matched: string) => {
const inner = matched.substr(1, matched.length - 2);
if (indexOfStart < 0) {
break;
}
let replaceValue: string;
const indexOfEnd = text.indexOf('}');
if (matched.includes('|')) {
const splittedValue = inner.split('|');
const replace = text.substring(indexOfStart, indexOfEnd + 1);
const key = splittedValue[0];
text = text.replace(replace, (matched: string) => {
let replaceValue: string;
replaceValue = this.getVar(args, key);
if (matched.includes('|')) {
const splittedValue = matched.split('|');
if (replaceValue) {
const transforms = splittedValue.slice(1);
replaceValue = this.handlePipeOption(args[splittedValue[0].substr(1)], splittedValue[1].slice(0, -1));
replaceValue = this.transform(replaceValue, transforms);
}
} else {
const key = matched.substring(1, matched.length - 1);
replaceValue = args[key];
replaceValue = this.getVar(args, inner);
}
return replaceValue;
});
}
return text;
}
private handlePipeOption(value: string, pipeOption: string) {
switch (pipeOption) {
private getVar(args: object, key: string) {
let value = args[key];
if (!value) {
for (const name in args) {
if (args.hasOwnProperty(name) && compareStrings(key, name) === 0) {
value = args[name];
break;
}
}
}
return value;
}
private transform(value: string, transforms: ReadonlyArray<string>) {
for (const transform of transforms) {
switch (transform) {
case 'lower':
return value.charAt(0).toLowerCase() + value.slice(1);
value = value.charAt(0).toLowerCase() + value.slice(1);
break;
case 'upper':
return value.charAt(0).toUpperCase() + value.slice(1);
default:
return value;
value = value.charAt(0).toUpperCase() + value.slice(1);
break;
}
}
return value;
}
}

2
frontend/app/shared/components/table-header.component.html

@ -3,7 +3,7 @@
<i *ngIf="order === 'ascending'" class="icon-caret-down"></i>
<i *ngIf="order === 'descending'" class="icon-caret-up"></i>
{{text}}
{{text | sqxTranslate}}
</span>
</a>

8
frontend/app/shared/state/contents.forms.spec.ts

@ -259,12 +259,18 @@ describe('DateTimeField', () => {
expect(FieldFormatter.format(dateField, '2017-12-12')).toBe('12/12/2017');
});
it('should format to date', () => {
it('should format datetime to date', () => {
const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) });
expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('12/12/2017');
});
it('should format date to date', () => {
const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) });
expect(FieldFormatter.format(dateField, '2017-12-12T00:00:00Z')).toBe('12/12/2017');
});
it('should format to date time', () => {
const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime' }) });

12
frontend/app/shared/state/contents.forms.ts

@ -15,7 +15,7 @@ import { LanguageDto } from './../services/languages.service';
import { FieldDto, NestedFieldDto, RootFieldDto, SchemaDetailsDto, TableField } from './../services/schemas.service';
import { fieldInvariant } from './../services/schemas.types';
import { CompiledRule, FieldSection, Hidden, PartitionConfig } from './contents.forms-helpers';
import { FieldDefaultValue, FieldsValidators, FieldUpdateOn } from './contents.forms.visitors';
import { FieldDefaultValue, FieldsValidators } from './contents.forms.visitors';
export { FieldSection } from './contents.forms-helpers';
@ -48,7 +48,7 @@ export class PatchContentForm extends Form<FormGroup, any> {
for (const field of this.editableFields) {
const validators = FieldsValidators.create(field, this.language.isOptional);
this.form.setControl(field.name, new FormControl(undefined, { updateOn: FieldUpdateOn.get(field), validators }));
this.form.setControl(field.name, new FormControl(undefined, { validators }));
}
}
@ -86,9 +86,7 @@ export class EditContentForm extends Form<FormGroup, any> {
constructor(languages: ReadonlyArray<AppLanguageDto>, schema: SchemaDetailsDto,
private readonly user: any = {}
) {
super(new FormGroup({}, {
updateOn: 'blur'
}));
super(new FormGroup({}));
const compiledPartitions = new PartitionConfig(languages);
const compiledConditions = schema.fieldRules.map(x => new CompiledRule(x));
@ -339,7 +337,7 @@ export class FieldValueForm extends AbstractContentForm<RootFieldDto, FormContro
const validators = FieldsValidators.create(field, isOptional);
return new FormControl(value, { updateOn: FieldUpdateOn.get(field), validators });
return new FormControl(value, { validators });
}
}
@ -517,7 +515,7 @@ export class FieldArrayItemValueForm extends AbstractContentForm<NestedFieldDto,
const validators = FieldsValidators.create(field, isOptional);
return new FormControl(value, { updateOn: FieldUpdateOn.get(field), validators });
return new FormControl(value, { validators });
}
}

64
frontend/app/shared/state/contents.forms.visitors.ts

@ -130,7 +130,7 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
const parsed = DateTime.parseISO(this.value);
if (properties.editor === 'Date') {
return parsed.toStringFormat('P');
return parsed.toStringFormatUTC('P');
} else {
return parsed.toStringFormat('Ppp');
}
@ -404,65 +404,3 @@ export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
return null;
}
}
type UpdateOn = 'change' | 'blur';
export class FieldUpdateOn implements FieldPropertiesVisitor<UpdateOn> {
private static INSTANCE = new FieldUpdateOn();
private constructor(
) {
}
public static get(field: FieldDto): UpdateOn {
if (field.properties.editorUrl) {
return 'change';
}
return field.properties.accept(FieldUpdateOn.INSTANCE);
}
public visitDateTime(_: DateTimeFieldPropertiesDto): any {
return 'blur';
}
public visitArray(_: ArrayFieldPropertiesDto): any {
return 'blur';
}
public visitAssets(_: AssetsFieldPropertiesDto): any {
return 'blur';
}
public visitBoolean(_: BooleanFieldPropertiesDto): any {
return 'change';
}
public visitGeolocation(_: GeolocationFieldPropertiesDto): any {
return 'blur';
}
public visitJson(_: JsonFieldPropertiesDto): any {
return 'blur';
}
public visitNumber(properties: NumberFieldPropertiesDto): any {
return properties.editor === 'Radio' || properties.editor === 'Dropdown' ? 'change' : 'blur';
}
public visitReferences(properties: ReferencesFieldPropertiesDto): any {
return properties.editor === 'Dropdown' || properties.editor === 'Checkboxes' ? 'change' : 'blur';
}
public visitString(properties: StringFieldPropertiesDto): any {
return properties.editor === 'Radio' || properties.editor === 'Dropdown' ? 'change' : 'blur';
}
public visitTags(properties: TagsFieldPropertiesDto): any {
return properties.editor === 'Checkboxes' || properties.editor === 'Dropdown' ? 'change' : 'blur';
}
public visitUI(_: UIFieldPropertiesDto): any {
return null;
}
}

2
frontend/app/shared/state/query.ts

@ -219,7 +219,7 @@ const StringOperators: ReadonlyArray<FilterOperator> = [
];
const ArrayOperators: ReadonlyArray<FilterOperator> = [
{ name: 'i18n:common.queryOperators.empty', value: 'is empty', noValue: true }
{ name: 'i18n:common.queryOperators.empty', value: 'empty', noValue: true }
];
const TypeBoolean: QueryFieldModel = {

3
frontend/app/theme/_forms.scss

@ -117,8 +117,9 @@
.form-alert {
@include absolute(.25rem, 0, auto, auto);
font-size: 1rem;
font-size: .9rem;
font-weight: normal;
line-height: 1.75;
max-width: 600px;
min-width: 200px;
padding: 1rem;

7
frontend/app/theme/_panels.scss

@ -334,11 +334,14 @@
&-item {
& {
@include border-radius;
color: inherit;
display: block;
font-size: .9rem;
font-weight: normal;
margin-bottom: .25rem;
padding: .25rem 1rem;
text-align: left;
text-decoration: none;
}
&-remove {
@ -352,6 +355,9 @@
&.active,
&:hover {
background: $color-theme-secondary;
color: inherit;
text-align: left;
text-decoration: none;
}
&.inactive {
@ -359,6 +365,7 @@
}
&:hover {
.sidebar-item-remove {
visibility: visible;
}

1674
frontend/package-lock.json

File diff suppressed because it is too large

82
frontend/package.json

@ -17,31 +17,29 @@
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
},
"dependencies": {
"@angular/animations": "10.0.11",
"@angular/cdk": "10.1.3",
"@angular/common": "10.0.11",
"@angular/core": "10.0.11",
"@angular/forms": "10.0.11",
"@angular/localize": "10.0.11",
"@angular/platform-browser": "10.0.11",
"@angular/platform-browser-dynamic": "10.0.11",
"@angular/platform-server": "10.0.11",
"@angular/router": "10.0.11",
"@angular/animations": "10.1.1",
"@angular/cdk": "10.2.0",
"@angular/common": "10.1.1",
"@angular/core": "10.1.1",
"@angular/forms": "10.1.1",
"@angular/localize": "10.1.1",
"@angular/platform-browser": "10.1.1",
"@angular/platform-browser-dynamic": "10.1.1",
"@angular/platform-server": "10.1.1",
"@angular/router": "10.1.1",
"@egjs/hammerjs": "2.0.17",
"@ngx-translate/core": "13.0.0",
"@ngx-translate/http-loader": "6.0.0",
"@types/codemirror": "0.0.97",
"@types/codemirror": "0.0.98",
"ace-builds": "1.4.12",
"angular-gridster2": "10.1.4",
"angular-gridster2": "10.1.5",
"angular-mentions": "1.2.0",
"angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0",
"bootstrap": "4.5.2",
"core-js": "3.6.5",
"cropperjs": "2.0.0-alpha.1",
"date-fns": "2.15.0",
"date-fns": "2.16.1",
"font-awesome": "4.7.0",
"graphiql": "1.0.3",
"graphiql": "1.0.4",
"graphql": "15.3.0",
"image-focus": "1.1.2",
"keycharm": "0.3.1",
@ -54,46 +52,46 @@
"progressbar.js": "1.1.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"rxjs": "6.6.2",
"rxjs": "6.6.3",
"simplemde": "1.11.2",
"slugify": "1.4.5",
"tinymce": "5.4.2",
"tslib": "2.0.1",
"vis-data": "7.0.0",
"vis-network": "8.2.0",
"vis-network": "8.3.2",
"vis-util": "4.3.4",
"zone.js": "0.11.1"
},
"devDependencies": {
"@angular-devkit/build-optimizer": "0.1000.7",
"@angular/compiler": "10.0.11",
"@angular/compiler-cli": "10.0.11",
"@ngtools/webpack": "10.0.7",
"@types/core-js": "2.5.3",
"@types/jasmine": "3.5.13",
"@angular-devkit/build-optimizer": "0.1001.1",
"@angular/compiler": "10.1.1",
"@angular/compiler-cli": "10.1.1",
"@ngtools/webpack": "10.1.1",
"@types/core-js": "2.5.4",
"@types/jasmine": "3.5.14",
"@types/marked": "1.1.0",
"@types/mersenne-twister": "1.1.2",
"@types/mousetrap": "^1.6.3",
"@types/node": "14.6.0",
"@types/react": "16.9.46",
"@types/node": "14.10.1",
"@types/react": "16.9.49",
"@types/react-dom": "16.9.8",
"@types/simplemde": "1.11.7",
"@types/tinymce": "4.5.24",
"browserslist": "4.14.0",
"caniuse-lite": "1.0.30001117",
"browserslist": "4.14.2",
"caniuse-lite": "1.0.30001129",
"circular-dependency-plugin": "5.2.0",
"codelyzer": "6.0.0",
"copy-webpack-plugin": "6.0.3",
"css-loader": "4.2.1",
"copy-webpack-plugin": "6.1.0",
"css-loader": "4.3.0",
"cssnano": "4.1.10",
"entities": "2.0.3",
"file-loader": "6.0.0",
"html-loader": "1.2.1",
"html-webpack-plugin": "4.3.0",
"file-loader": "6.1.0",
"html-loader": "1.3.0",
"html-webpack-plugin": "4.4.1",
"ignore-loader": "0.1.2",
"istanbul-instrumenter-loader": "3.0.1",
"jasmine-core": "3.6.0",
"karma": "5.1.1",
"karma": "5.2.2",
"karma-chrome-launcher": "3.1.0",
"karma-cli": "2.0.0",
"karma-coverage-istanbul-reporter": "3.0.3",
@ -103,11 +101,11 @@
"karma-mocha-reporter": "2.2.5",
"karma-sourcemap-loader": "0.3.8",
"karma-webpack": "4.0.2",
"mini-css-extract-plugin": "0.10.0",
"mini-css-extract-plugin": "0.11.2",
"node-sass": "4.14.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"optimize-css-assets-webpack-plugin": "5.0.4",
"postcss-import": "12.0.1",
"postcss-loader": "3.0.0",
"postcss-loader": "4.0.1",
"postcss-preset-env": "6.7.0",
"raw-loader": "4.0.1",
"resize-observer-polyfill": "1.5.1",
@ -115,18 +113,18 @@
"rxjs-tslint": "0.1.8",
"sass-lint": "1.13.1",
"sass-lint-webpack": "1.0.3",
"sass-loader": "9.0.3",
"sass-loader": "10.0.2",
"style-loader": "1.2.1",
"sugarss": "2.0.0",
"terser-webpack-plugin": "4.1.0",
"ts-loader": "8.0.2",
"terser-webpack-plugin": "4.2.0",
"ts-loader": "8.0.3",
"tsconfig-paths-webpack-plugin": "3.3.0",
"tslint": "6.1.3",
"tslint-immutable": "6.0.1",
"tslint-webpack-plugin": "2.1.0",
"typemoq": "2.1.0",
"typescript": "3.9",
"underscore": "1.10.2",
"typescript": "4.0",
"underscore": "1.11.0",
"webpack": "4.44.1",
"webpack-bundle-analyzer": "3.8.0",
"webpack-cli": "3.3.12",

3
frontend/tsconfig.spec.json

@ -1,5 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node", "jasmine"]
},
"include": [
"app/**/*.d.ts",
"app/**/*.spec.ts"

344
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs

@ -1,344 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class AppMutationsGraphType : ObjectGraphType
{
public AppMutationsGraphType(IGraphModel model, IEnumerable<ISchemaEntity> schemas)
{
foreach (var schema in schemas)
{
var schemaId = schema.NamedId();
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
var contentType = model.GetContentType(schema.Id);
var contentDataType = model.GetContentDataType(schema.Id);
var resultType = new ContentDataChangedResultGraphType(schemaType, schemaName, contentDataType);
var inputType = new ContentDataGraphInputType(model, schema);
AddContentCreate(schemaId, schemaType, schemaName, inputType, contentDataType, contentType);
AddContentUpdate(schemaType, schemaName, inputType, resultType);
AddContentPatch(schemaType, schemaName, inputType, resultType);
AddContentPublish(schemaType, schemaName);
AddContentUnpublish(schemaType, schemaName);
AddContentArchive(schemaType, schemaName);
AddContentRestore(schemaType, schemaName);
AddContentDelete(schemaType, schemaName);
}
Description = "The app mutations.";
}
private void AddContentCreate(NamedId<Guid> schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType contentDataType, IGraphType contentType)
{
AddField(new FieldType
{
Name = $"create{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(contentType),
Resolver = ResolveAsync(async (c, publish) =>
{
var argPublish = c.GetArgument<bool>("publish");
var contentData = GetContentData(c);
var command = new CreateContent { SchemaId = schemaId, Data = contentData, Publish = argPublish };
var commandContext = await publish(command);
var result = commandContext.Result<EntityCreatedResult<NamedContentData>>();
var response = ContentEntity.Create(command, result);
return (IContentEntity)ContentEntity.Create(command, result);
}),
Description = $"Creates an {schemaName} content."
});
}
private void AddContentUpdate(string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType resultType)
{
AddField(new FieldType
{
Name = $"update{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(resultType),
Resolver = ResolveAsync(async (c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
var command = new UpdateContent { ContentId = contentId, Data = contentData };
var commandContext = await publish(command);
var result = commandContext.Result<ContentDataChangedResult>();
return result;
}),
Description = $"Update an {schemaName} content by id."
});
}
private void AddContentPatch(string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType resultType)
{
AddField(new FieldType
{
Name = $"patch{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(resultType),
Resolver = ResolveAsync(async (c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
var command = new PatchContent { ContentId = contentId, Data = contentData };
var commandContext = await publish(command);
var result = commandContext.Result<ContentDataChangedResult>();
return result;
}),
Description = $"Patch a {schemaName} content."
});
}
private void AddContentPublish(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"publish{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Published };
return publish(command);
}),
Description = $"Publish a {schemaName} content."
});
}
private void AddContentUnpublish(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"unpublish{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft };
return publish(command);
}),
Description = $"Unpublish a {schemaName} content."
});
}
private void AddContentArchive(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"archive{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Archived };
return publish(command);
}),
Description = $"Archive a {schemaName} content."
});
}
private void AddContentRestore(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"restore{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft };
return publish(command);
}),
Description = $"Restore a {schemaName} content."
});
}
private void AddContentDelete(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"delete{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new DeleteContent { ContentId = contentId };
return publish(command);
}),
Description = $"Delete an {schemaName} content."
});
}
private static QueryArguments CreateIdArguments(string schemaName)
{
return new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
}
private static IFieldResolver ResolveAsync<T>(Func<ResolveFieldContext, Func<SquidexCommand, Task<CommandContext>>, Task<T>> action)
{
return new FuncFieldResolver<Task<T>>(async c =>
{
var e = (GraphQLExecutionContext)c.UserContext;
try
{
return await action(c, command =>
{
command.ExpectedVersion = c.GetArgument("expectedVersion", EtagVersion.Any);
return e.CommandBus.PublishAsync(command);
});
}
catch (ValidationException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
throw;
}
catch (DomainException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
throw;
}
});
}
private static NamedContentData GetContentData(ResolveFieldContext c)
{
return JObject.FromObject(c.GetArgument<object>("data")).ToObject<NamedContentData>();
}
}
}

70
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphInputType.cs

@ -1,70 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentDataGraphInputType : InputObjectGraphType
{
public ContentDataGraphInputType(IGraphModel model, ISchemaEntity schema)
{
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
Name = $"{schemaType}InputDto";
foreach (var field in schema.SchemaDef.Fields.Where(x => !x.IsHidden))
{
var inputType = model.GetInputGraphType(field);
if (inputType != null)
{
if (field.RawProperties.IsRequired)
{
inputType = new NonNullGraphType(inputType);
}
var fieldName = field.RawProperties.Label.WithFallback(field.Name);
var fieldGraphType = new InputObjectGraphType
{
Name = $"{schemaType}Data{field.Name.ToPascalCase()}InputDto"
};
var partition = model.ResolvePartition(field.Partitioning);
foreach (var partitionItem in partition)
{
fieldGraphType.AddField(new FieldType
{
Name = partitionItem.Key,
Resolver = null,
ResolvedType = inputType,
Description = field.RawProperties.Hints
});
}
fieldGraphType.Description = $"The input structure of the {fieldName} of a {schemaName} content type.";
AddField(new FieldType
{
Name = field.Name.ToCamelCase(),
Resolver = null,
ResolvedType = fieldGraphType,
Description = $"The {fieldName} field."
});
}
}
Description = $"The structure of a {schemaName} content type.";
}
}
}

31
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GeolocationInputGraphType.cs

@ -1,31 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class GeolocationInputGraphType : InputObjectGraphType
{
public GeolocationInputGraphType()
{
Name = "GeolocationInputDto";
AddField(new FieldType
{
Name = "latitude",
ResolvedType = AllTypes.NonNullFloat
});
AddField(new FieldType
{
Name = "longitude",
ResolvedType = AllTypes.NonNullFloat
});
}
}
}

20
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldExtensions.cs

@ -1,20 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static class InputFieldExtensions
{
public static IGraphType GetInputGraphType(this IField field)
{
return field.Accept(InputFieldVisitor.Default);
}
}
}

71
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs

@ -1,71 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class InputFieldVisitor : IFieldVisitor<IGraphType>
{
public static readonly InputFieldVisitor Default = new InputFieldVisitor();
private InputFieldVisitor()
{
}
public IGraphType Visit(IArrayField field)
{
return AllTypes.NoopArray;
}
public IGraphType Visit(IField<AssetsFieldProperties> field)
{
return AllTypes.References;
}
public IGraphType Visit(IField<BooleanFieldProperties> field)
{
return AllTypes.Boolean;
}
public IGraphType Visit(IField<DateTimeFieldProperties> field)
{
return AllTypes.Date;
}
public IGraphType Visit(IField<GeolocationFieldProperties> field)
{
return AllTypes.GeolocationInput;
}
public IGraphType Visit(IField<JsonFieldProperties> field)
{
return AllTypes.Json;
}
public IGraphType Visit(IField<NumberFieldProperties> field)
{
return AllTypes.Float;
}
public IGraphType Visit(IField<ReferencesFieldProperties> field)
{
return AllTypes.References;
}
public IGraphType Visit(IField<StringFieldProperties> field)
{
return AllTypes.String;
}
public IGraphType Visit(IField<TagsFieldProperties> field)
{
return AllTypes.Tags;
}
}
}

47
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs

@ -1,47 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class NestedInputGraphType : InputObjectGraphType
{
public NestedInputGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field)
{
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
var fieldType = field.TypeName();
var fieldName = field.DisplayName();
Name = $"{schemaType}{fieldName}ChildDto";
foreach (var nestedField in field.Fields.Where(x => !x.IsHidden))
{
var fieldInfo = model.GetGraphType(schema, nestedField);
if (fieldInfo.ResolveType != null)
{
AddField(new FieldType
{
Name = nestedField.Name.ToCamelCase(),
Resolver = null,
ResolvedType = fieldInfo.ResolveType,
Description = $"The {fieldName}/{nestedField.DisplayName()} nested field."
});
}
}
Description = $"The structure of a {schemaName}.{fieldName} nested schema.";
}
}
}
Loading…
Cancel
Save