Browse Source

I18n (#554)

* First files and keys.

* Progress with translation.

* More progress.

* Temp

* Simplify validation and error messages to make i18n easier.

* Texts improved.

* Just code formatting.

* Much more translations.

* Cleanup.

* Texts fixed.

* More translations and test fixes.

* Translations fixed.

* Build fix.

* More progress with texts.

* Texts fixed.

* Translations migrated.

* Put texts to resources.

* Progress with typescript.

* Name of pipe fixed.

* TranslationService => Localizer.

* Do not translate recursive.

* Localization improvements.

* I18n martijn (#550)

* prepared the appsettings and the window options for the Google Analtyics Id setting

* removed white spaces

* refactored the analytics.service, also made use of the settings from the appsettings.json. The analtycsid from the appsettings will now be used.

* setting back the localhost statements for the gtag config

* Refactored the analytics.service again

* Made the uiOptions options, for the sake of testing...

* fixed a spelling mistake

* Removed the empty line after the if statement.

* Removed the analyticsIdConfig class. Fixed the hostname check for localhost

* i dont know why this didnt merge correctly

* started with the i18n translations

* created a custom translate pipe

* Started with the implementation of ngx-translate

* forgot the package-lock

* End of day commit

* Added a shell for the ngx-translate (maybe remove it in the future)

* TranslationService => LocalizerService in the frontend

* Fixes for the stylelint, and adding the localizer service to the app.module.ts

* Fixes for the stylelint

* Fixes for the remove translationsService

* Design for replacing the variables in a translate text

* Little bit of translation progress

* Changed the infrastructue for the translates

* added the localizerService to the prettifyError handler

* made the translate function better

* The localizer is now a singleton and did some fixes

* Implemented custom pipe options

* Cleanup

* Removed some testdata

* Cleanup

* Started with Code review feedback.
Started with translate variables as an object instead of an var. Fixed tests

* Removed the regex for finden text that needs to be replaced

Co-authored-by: Martijn Dijkgraaf <>

* More texts

* Progress

* More translations

* More texts

* More texts

* Simplified translations

* Cleanup

* More fixes

* Cleanup

* Consistency fixes

* More texts

* More fixes

* Prefixes unified.

* Embed texts.

* Dialog fixed.

* Tests fixed.

* Styling fix.

* Just a cleanup.

* Inline markdown

* Minor fix.

* Small bugfixes

* Indexes improvements.

* Added translator.

* Notifo fix.

Co-authored-by: MartijnDijkgraaf <martijndijkgraaf96@gmail.com>
pull/556/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
ce1a1386fc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs
  2. 2
      backend/src/Squidex.Infrastructure/Translations/ILocalizer.cs
  3. 8
      backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs
  4. 10
      backend/src/Squidex.Web/Services/StringLocalizer.cs
  5. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  6. 2
      backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs
  7. 6
      backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  8. 38
      backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs
  9. 835
      backend/src/Squidex/Areas/Frontend/Resources/texts.en
  10. 2
      backend/src/Squidex/Config/Web/WebExtensions.cs
  11. 13
      backend/src/Squidex/Squidex.csproj
  12. 25
      backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs
  13. 6
      frontend/app/app.component.html
  14. 13
      frontend/app/app.module.ts
  15. 10
      frontend/app/features/administration/administration-area.component.html
  16. 2
      frontend/app/features/administration/pages/cluster/cluster-page.component.html
  17. 6
      frontend/app/features/administration/pages/event-consumers/event-consumer.component.html
  18. 16
      frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  19. 16
      frontend/app/features/administration/pages/restore/restore-page.component.html
  20. 2
      frontend/app/features/administration/pages/restore/restore-page.component.ts
  21. 40
      frontend/app/features/administration/pages/users/user-page.component.html
  22. 6
      frontend/app/features/administration/pages/users/user.component.html
  23. 20
      frontend/app/features/administration/pages/users/users-page.component.html
  24. 8
      frontend/app/features/administration/services/event-consumers.service.ts
  25. 12
      frontend/app/features/administration/services/users.service.ts
  26. 2
      frontend/app/features/administration/state/event-consumers.state.ts
  27. 2
      frontend/app/features/administration/state/users.forms.ts
  28. 2
      frontend/app/features/administration/state/users.state.ts
  29. 10
      frontend/app/features/api/api-area.component.html
  30. 2
      frontend/app/features/api/pages/graphql/graphql-page.component.html
  31. 66
      frontend/app/features/apps/pages/apps-page.component.html
  32. 4
      frontend/app/features/apps/pages/news-dialog.component.html
  33. 92
      frontend/app/features/apps/pages/onboarding-dialog.component.html
  34. 2
      frontend/app/features/assets/pages/asset-tags.component.html
  35. 12
      frontend/app/features/assets/pages/assets-filters-page.component.html
  36. 24
      frontend/app/features/assets/pages/assets-page.component.html
  37. 6
      frontend/app/features/content/pages/content/content-event.component.html
  38. 4
      frontend/app/features/content/pages/content/content-field.component.html
  39. 54
      frontend/app/features/content/pages/content/content-history-page.component.html
  40. 2
      frontend/app/features/content/pages/content/content-history-page.component.ts
  41. 36
      frontend/app/features/content/pages/content/content-page.component.html
  42. 20
      frontend/app/features/content/pages/content/content-page.component.ts
  43. 6
      frontend/app/features/content/pages/content/field-languages.component.html
  44. 13
      frontend/app/features/content/pages/contents/contents-filters-page.component.html
  45. 34
      frontend/app/features/content/pages/contents/contents-page.component.html
  46. 9
      frontend/app/features/content/pages/contents/custom-view-editor.component.html
  47. 8
      frontend/app/features/content/pages/schemas/schemas-page.component.html
  48. 4
      frontend/app/features/content/shared/content-status.component.html
  49. 14
      frontend/app/features/content/shared/due-time-selector.component.html
  50. 15
      frontend/app/features/content/shared/forms/array-editor.component.html
  51. 14
      frontend/app/features/content/shared/forms/array-item.component.html
  52. 53
      frontend/app/features/content/shared/forms/assets-editor.component.html
  53. 15
      frontend/app/features/content/shared/forms/stock-photo-editor.component.html
  54. 10
      frontend/app/features/content/shared/list/content-list-field.component.html
  55. 17
      frontend/app/features/content/shared/list/content.component.html
  56. 2
      frontend/app/features/content/shared/preview-button.component.html
  57. 14
      frontend/app/features/content/shared/references/content-creator.component.html
  58. 2
      frontend/app/features/content/shared/references/content-creator.component.ts
  59. 21
      frontend/app/features/content/shared/references/content-selector.component.html
  60. 12
      frontend/app/features/content/shared/references/references-editor.component.html
  61. 4
      frontend/app/features/dashboard/pages/cards/api-calls-card.component.html
  62. 10
      frontend/app/features/dashboard/pages/cards/api-calls-summary-card.component.html
  63. 6
      frontend/app/features/dashboard/pages/cards/api-card.component.html
  64. 8
      frontend/app/features/dashboard/pages/cards/api-performance-card.component.html
  65. 8
      frontend/app/features/dashboard/pages/cards/api-traffic-card.component.html
  66. 2
      frontend/app/features/dashboard/pages/cards/asset-uploads-count-card.component.html
  67. 2
      frontend/app/features/dashboard/pages/cards/asset-uploads-size-card.component.html
  68. 10
      frontend/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.html
  69. 2
      frontend/app/features/dashboard/pages/cards/content-summary-card.component.html
  70. 6
      frontend/app/features/dashboard/pages/cards/github-card.component.html
  71. 2
      frontend/app/features/dashboard/pages/cards/history-card.component.html
  72. 10
      frontend/app/features/dashboard/pages/cards/schema-card.component.html
  73. 6
      frontend/app/features/dashboard/pages/cards/support-card.component.html
  74. 22
      frontend/app/features/dashboard/pages/dashboard-config.component.html
  75. 26
      frontend/app/features/dashboard/pages/dashboard-config.component.ts
  76. 65
      frontend/app/features/dashboard/pages/dashboard-page.component.html
  77. 26
      frontend/app/features/rules/pages/events/rule-events-page.component.html
  78. 2
      frontend/app/features/rules/pages/rules/rule-element.component.html
  79. 53
      frontend/app/features/rules/pages/rules/rule-wizard.component.html
  80. 38
      frontend/app/features/rules/pages/rules/rule.component.html
  81. 29
      frontend/app/features/rules/pages/rules/rules-page.component.html
  82. 26
      frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html
  83. 10
      frontend/app/features/schemas/pages/schema/export/schema-export-form.component.html
  84. 35
      frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html
  85. 67
      frontend/app/features/schemas/pages/schema/fields/field.component.html
  86. 22
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html
  87. 6
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html
  88. 4
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html
  89. 10
      frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.html
  90. 14
      frontend/app/features/schemas/pages/schema/fields/schema-fields.component.html
  91. 6
      frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html
  92. 16
      frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html
  93. 42
      frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html
  94. 16
      frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html
  95. 2
      frontend/app/features/schemas/pages/schema/fields/types/boolean-validation.component.html
  96. 10
      frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html
  97. 8
      frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.html
  98. 4
      frontend/app/features/schemas/pages/schema/fields/types/geolocation-ui.component.html
  99. 22
      frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html
  100. 14
      frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.html

14
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs

@ -48,16 +48,24 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
this.indexer = indexer; this.indexer = indexer;
} }
protected override Task PrepareAsync(CancellationToken ct = default) protected override async Task PrepareAsync(CancellationToken ct = default)
{ {
var index = var indexBySchemaWithRefs =
new CreateIndexModel<MongoContentEntity>(Index new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId) .Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted) .Ascending(x => x.IsDeleted)
.Ascending(x => x.ReferencedIds) .Ascending(x => x.ReferencedIds)
.Descending(x => x.LastModified)); .Descending(x => x.LastModified));
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); await Collection.Indexes.CreateOneAsync(indexBySchemaWithRefs, cancellationToken: ct);
var indexBySchema =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted)
.Descending(x => x.LastModified));
await Collection.Indexes.CreateOneAsync(indexBySchema, cancellationToken: ct);
} }
public async Task<IResultList<IContentEntity>> DoAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, SearchScope scope) public async Task<IResultList<IContentEntity>> DoAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, SearchScope scope)

2
backend/src/Squidex.Infrastructure/Translations/ILocalizer.cs

@ -11,6 +11,6 @@ namespace Squidex.Infrastructure.Translations
{ {
public interface ILocalizer public interface ILocalizer
{ {
(string Result, bool NotFound) Get(CultureInfo culture, string key, string fallback, object? args = null); (string Result, bool Found) Get(CultureInfo culture, string key, string fallback, object? args = null);
} }
} }

8
backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs

@ -41,7 +41,7 @@ namespace Squidex.Infrastructure.Translations
#endif #endif
} }
public (string Result, bool NotFound) Get(CultureInfo culture, string key, string fallback, object? args = null) public (string Result, bool Found) Get(CultureInfo culture, string key, string fallback, object? args = null)
{ {
Guard.NotNull(culture, nameof(culture)); Guard.NotNull(culture, nameof(culture));
Guard.NotNullOrEmpty(key, nameof(key)); Guard.NotNullOrEmpty(key, nameof(key));
@ -51,7 +51,7 @@ namespace Squidex.Infrastructure.Translations
if (translation == null) if (translation == null)
{ {
return (fallback, true); return (fallback, false);
} }
if (args != null) if (args != null)
@ -153,10 +153,10 @@ namespace Squidex.Infrastructure.Translations
sb.Append(span); sb.Append(span);
return (sb.ToString(), false); return (sb.ToString(), true);
} }
return (translation, false); return (translation, true);
} }
private string? GetCore(CultureInfo culture, string key) private string? GetCore(CultureInfo culture, string key)

10
backend/src/Squidex.Web/Services/StringLocalizer.cs

@ -38,14 +38,14 @@ namespace Squidex.Web.Services
TranslateProperty(name, arguments, currentCulture); TranslateProperty(name, arguments, currentCulture);
var (result, notFound) = translationService.Get(currentCulture, $"aspnet_{name}", name); var (result, found) = translationService.Get(currentCulture, $"aspnet_{name}", name);
if (arguments != null && !notFound) if (arguments != null && found)
{ {
result = string.Format(currentCulture, result, arguments); result = string.Format(currentCulture, result, arguments);
} }
return new LocalizedString(name, result, notFound); return new LocalizedString(name, result, !found);
} }
} }
@ -55,9 +55,9 @@ namespace Squidex.Web.Services
{ {
var key = $"common.{arguments[0].ToString()?.ToCamelCase()}"; var key = $"common.{arguments[0].ToString()?.ToCamelCase()}";
var (result, notFound) = translationService.Get(currentCulture, key, name); var (result, found) = translationService.Get(currentCulture, key, name);
if (!notFound) if (found)
{ {
arguments[0] = result; arguments[0] = result;
} }

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -284,7 +284,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </returns> /// </returns>
[HttpDelete] [HttpDelete]
[Route("apps/{app}/")] [Route("apps/{app}/")]
[ApiPermissionOrAnonymous(Permissions.AppDelete)] [ApiPermission(Permissions.AppDelete)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> DeleteApp(string app) public async Task<IActionResult> DeleteApp(string app)
{ {

2
backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs

@ -39,7 +39,7 @@ namespace Squidex.Areas.Api.Controllers.News
[Route("news/features/")] [Route("news/features/")]
[ProducesResponseType(typeof(FeaturesDto), 200)] [ProducesResponseType(typeof(FeaturesDto), 200)]
[ApiPermission] [ApiPermission]
public async Task<IActionResult> GetLanguages([FromQuery] int version = 0) public async Task<IActionResult> GetNews([FromQuery] int version = 0)
{ {
var features = await featuresService.GetFeaturesAsync(version); var features = await featuresService.GetFeaturesAsync(version);

6
backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -37,11 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Users
{ {
var assembly = typeof(UsersController).Assembly; var assembly = typeof(UsersController).Assembly;
using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png")) using (var resourceStream = assembly.GetManifestResourceStream("Squidex.Areas.Api.Controllers.Users.Assets.Avatar.png"))
{ {
AvatarBytes = new byte[avatarStream!.Length]; AvatarBytes = new byte[resourceStream!.Length];
avatarStream.Read(AvatarBytes, 0, AvatarBytes.Length); resourceStream.Read(AvatarBytes, 0, AvatarBytes.Length);
} }
} }

38
backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs

@ -6,6 +6,9 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -18,6 +21,8 @@ namespace Squidex.Areas.Frontend.Middlewares
{ {
public static class IndexExtensions public static class IndexExtensions
{ {
private static readonly ConcurrentDictionary<string, string> Texts = new ConcurrentDictionary<string, string>();
public static bool IsIndex(this HttpContext context) public static bool IsIndex(this HttpContext context)
{ {
return context.Request.Path.Value.EndsWith("/index.html", StringComparison.OrdinalIgnoreCase); return context.Request.Path.Value.EndsWith("/index.html", StringComparison.OrdinalIgnoreCase);
@ -61,9 +66,38 @@ namespace Squidex.Areas.Frontend.Middlewares
} }
var jsonSerializer = httpContext.RequestServices.GetRequiredService<IJsonSerializer>(); var jsonSerializer = httpContext.RequestServices.GetRequiredService<IJsonSerializer>();
var jsonOptions = jsonSerializer.Serialize(uiOptions); var jsonOptions = jsonSerializer.Serialize(uiOptions, true);
var texts = GetText(CultureInfo.CurrentUICulture.Name);
result = result.Replace("<body>", $"<body>\n<script>\nvar options = {jsonOptions};\nvar texts = {texts};</script>");
}
return result;
}
private static string GetText(string culture)
{
if (!Texts.TryGetValue(culture, out var result))
{
var assembly = typeof(IndexExtensions).Assembly;
var resourceName = $"Squidex.Areas.Frontend.Resources.texts.{culture}";
var resourceStream = assembly.GetManifestResourceStream(resourceName);
if (resourceStream != null)
{
using (var reader = new StreamReader(resourceStream))
{
result = reader.ReadToEnd();
result = result.Replace("<body>", $"<body><script>var options = {jsonOptions};</script>"); Texts[culture] = result;
}
}
else
{
return GetText("en");
}
} }
return result; return result;

835
backend/src/Squidex/Areas/Frontend/Resources/texts.en

@ -0,0 +1,835 @@
{
"api.contentApi": "Content API",
"api.generalApi": "General API",
"api.graphql": "GraphQL",
"api.graphqlPageTitle": "GraphQL",
"api.pageTitle": "API",
"api.title": "API",
"apps.allApps": "All Apps",
"apps.appLoadFailed": "Failed to load app. Please reload.",
"apps.appNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.",
"apps.appNameValidationMessage": "Name can contain lower case letters (a-z), numbers and dashes between.",
"apps.appNameWarning": "The app name cannot be changed later.",
"apps.appsButtonCreate": "Apps Overview",
"apps.appsButtonFallbackTitle": "Apps Overview",
"apps.archieve": "Archive App",
"apps.archieveConfirmText": "Remove pattern",
"apps.archieveConfirmTitle": "Do you really want to archive this app?",
"apps.archieveWarning": "Once you archive an app, there is no going back. Please be certain.",
"apps.archiveFailed": "Failed to archive app. Please reload.",
"apps.create": "Create App",
"apps.createBlankApp": "New App.",
"apps.createBlankAppDescription": "Create a new blank app without content and schemas.",
"apps.createBlogApp": "New Blog Sample",
"apps.createBlogAppDescription": "Start with our ready to use blog.",
"apps.createFailed": "Failed to create app. Please reload.",
"apps.createIdentityApp": "New Identity App",
"apps.createIdentityAppDescription": "Create app for Squidex Identity.",
"apps.createIdentityAppV2": "New Identity App V2",
"apps.createIdentityAppV2Description": "Create app for Squidex Identity V2.",
"apps.createProfileApp": "New Profile Sample",
"apps.createProfileAppDescription": "Create your profile page.",
"apps.createWithTemplate": "Create {template} Sample",
"apps.empty": "You are not collaborating to any app yet",
"apps.generalSettings": "General",
"apps.generalSettingsDangerZone": "General",
"apps.image": "Image",
"apps.imageDrop": "Drop to upload",
"apps.listPageTitle": "Apps",
"apps.loadFailed": "Failed to load apps. Please reload.",
"apps.removeImage": "Remove image",
"apps.removeImageFailed": "Failed to remove app image. Please reload.",
"apps.updateFailed": "Failed to update app. Please reload.",
"apps.upgradeHintCurrent": "You are on the {plan} plan.",
"apps.upgradeHintUpgrade": "Upgrade!",
"apps.uploadImage": "Drop an file to replace the app image. Use a square size.",
"apps.uploadImageButton": "Upload File",
"apps.uploadImageFailed": "Failed to upload image. Please reload.",
"apps.uploadImageTooBig": "App image is too big.",
"apps.welcomeSubtitle": "Welcome to Squidex.",
"apps.welcomeTitle": "Hi {user}",
"assets.createFolder": "Create Folder",
"assets.createFolderFailed": "Failed to create asset folder. Please reload.",
"assets.createFolderTooltip": "Create new folder (CTRL + SHIFT + G)",
"assets.deleteConfirmText": "Do you really want to delete the asset?",
"assets.deleteConfirmTitle": "Delete asset",
"assets.deleteFailed": "Failed to delete asset. Please reload.",
"assets.deleteFolderConfirmText": "Do you really want to delete the folder and all assets?",
"assets.deleteFolderConfirmTitle": "Delete folder",
"assets.deleteMetadataConfirmText": "Do you really want to remove this metadata?",
"assets.deleteMetadataConfirmTitle": "Remove metadata",
"assets.downloadVersion": "Download this Version",
"assets.dropToUpdate": "Drop to update",
"assets.duplicateFile": "Asset has already been uploaded.",
"assets.editor.flipHorizontally": "Flip Horizontally",
"assets.editor.flipVertically": "Flip Vertically",
"assets.editor.focusPointLabel": "Select position of focus point",
"assets.editor.focusPointPreview": "Preview for different sizes",
"assets.editor.rotateLeft": "Rotate Left",
"assets.editor.rotateRight": "Rotate Right",
"assets.fileTooBig": "Asset is too big.",
"assets.folderName": "Folder Name",
"assets.folderNameHint": "The folder name is used as display name and must not be unique.",
"assets.insertAssets": "Insert Assets",
"assets.linkSelected": "Link selected assets ({count})",
"assets.listPageTitle": "Assets",
"assets.loadFailed": "Failed to load assets. Please reload.",
"assets.loadFoldersFailed": "Failed to load asset folders. Please reload.",
"assets.metadata": "Metadata",
"assets.metadataAdd": "Add Metadata",
"assets.moveFailed": "Failed to move asset. Please reload.",
"assets.protected": "Protected",
"assets.refreshTooltip": "Refresh Assets (CTRL + SHIFT + R)",
"assets.reloaded": "Assets reloaded.",
"assets.renameFolder": "Rename Folder",
"assets.replaceConfirmText": "Do you really want to replace the asset with a newer version",
"assets.replaceConfirmTitle": "Replace asset?",
"assets.replaceFailed": "Failed to replace asset. Please reload.",
"assets.searchByName": "Search by name",
"assets.searchByTags": "Search by tags",
"assets.selectMany": "Select assets",
"assets.tabFocusPoint": "Focus Point",
"assets.tabHistory": "History",
"assets.tabImage": "Image",
"assets.tabMetadata": "Metadata",
"assets.updated": "Asset has been updated.",
"assets.updateFailed": "Failed to update asset. Please reload.",
"assets.updateFolderFailed": "Failed to update asset folder. Please reload.",
"assets.uploadByDialog": "Select File(s",
"assets.uploadByDrop": "Drop files here to upload",
"assets.uploaderUploadHere": "No upload in progress, drop files here.",
"assets.uploadFailed": "Failed to upload asset. Please reload.",
"assets.uploadHint": "Drop file on existing item to replace the asset with a newer version.",
"backups.backupCountAssets": "Assets",
"backups.backupCountAssetsTooltip": "Archived assets",
"backups.backupCountEvents": "Events",
"backups.backupCountEventsTooltip": "Archived events",
"backups.backupDownload": "Download",
"backups.backupDownloadLink": "Ready",
"backups.backupDuration": "Duration",
"backups.deleteConfirmText": "Do you really want to delete the backup?",
"backups.deleteConfirmTitle": "Delete backup",
"backups.deleted": "Backup is about to be deleted.",
"backups.deleteFailed": "Failed to delete backup.",
"backups.empty": "No backups created yet.",
"backups.loadFailed": "Failed to load backups.",
"backups.maximumReached": "Your have reached the maximum number of backups: 10.",
"backups.refreshTooltip": "Refresh backups (CTRL + SHIFT + R)",
"backups.reloaded": "Backups reloaded.",
"backups.restore": "Restore Backup",
"backups.restoreFailed": "Failed to start restore.",
"backups.restoreLastStatus": "Last Restore Operation",
"backups.restoreLastUrl": "Url to backup",
"backups.restoreNewAppName": "Optional app name",
"backups.restorePageTitle": "Restore Backup",
"backups.restoreStarted": "Restore started, it can take several minutes to complete.",
"backups.restoreStartedLabel": "Started",
"backups.restoreStoppedLabel": "Stopped",
"backups.restoreTitle": "Restore Backup",
"backups.start": "Start Backup",
"backups.started": "Backup started, it can take several minutes to complete.",
"backups.startFailed": "Failed to start backup.",
"clients.add": "Add Client",
"clients.addFailed": "Failed to add client. Please reload.",
"clients.allowAnonymous": "Allow anonymous access.",
"clients.allowAnonymousHint": "Allow access to the API without an access token to all resources that are configured via the role of this client. Do not give more than one client anonymous access.",
"clients.clientIdValidationMessage": "Name can only contain letters, numbers, dashes and spaces.",
"clients.clientNamePlaceholder": "Enter client name",
"clients.connect": "Connect",
"clients.connectWizard.cli": "Connect with Squidex CLI",
"clients.connectWizard.cliHint": "Download the CLI and connect to this app to start backups, sync schemas or export content.",
"clients.connectWizard.cliStep1": "Get the latest Squidex CLI",
"clients.connectWizard.cliStep1Download": "[Download the CLI from Github](https://github.com/Squidex/squidex-samples/releases)",
"clients.connectWizard.cliStep1Hint": "The releases contains binaries for all major operation system and a small download if you have .NET Core installed.",
"clients.connectWizard.cliStep2": "Add `<your Squidex CLI download directory>` to your `$PATH` variable",
"clients.connectWizard.cliStep3": "Add your app name the CLI config",
"clients.connectWizard.cliStep3Hint": "You can manage configuration to multiple apps in the CLI and switch to an app.",
"clients.connectWizard.cliStep4": "Switch to your app in the CLI",
"clients.connectWizard.manually": "Connect manually",
"clients.connectWizard.manuallyHint": "Get instructions how to establish a connection with Postman or curl.",
"clients.connectWizard.manuallyStep1": "Get a token using curl",
"clients.connectWizard.manuallyStep2": "Just use the following token",
"clients.connectWizard.manuallyStep3": "Add the token as HTTP header to all requests",
"clients.connectWizard.manuallyTokenHint": "Tokens usally expire after 30days, but you can request multiple tokens.",
"clients.connectWizard.postManDocs": "Start with the Postman tutorial in the [Documentation](https://docs.squidex.io/02-documentation/developer-guides/api-overview/postman).",
"clients.connectWizard.sdk": "Connect to your App with SDK",
"clients.connectWizard.sdkHelp": "You need another SDK?",
"clients.connectWizard.sdkHelpLink": "Contact us in the Support Forum",
"clients.connectWizard.sdkHint": "Download an SDK and establish a connection to this app.",
"clients.connectWizard.sdkStep1": "Install the .NET SDK",
"clients.connectWizard.sdkStep1Download": "The SDK is available on [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary/)",
"clients.connectWizard.sdkStep2": "Create a client manager",
"clients.connectWizard.step0Title": "Setup client",
"clients.connectWizard.step1Title": "Choose connection method",
"clients.connectWizard.step2Title": "Connect",
"clients.deleteConfirmText": "Do you really want to revoke the client?",
"clients.deleteConfirmTitle": "Revoke client",
"clients.empty": "No client created yet.",
"clients.loadFailed": "Failed to load clients. Please reload.",
"clients.refreshTooltip": "Refresh clients (CTRL + SHIFT + R)",
"clients.reloaded": "Clients reloaded.",
"clients.revokeFailed": "Failed to revoke client. Please reload.",
"clients.tokenFailed": "Failed to create token. Please retry.",
"comments.create": "Create a comment",
"comments.createFailed": "Failed to create comment.",
"comments.deleteConfirmText": "Do you really want to delete the comment?",
"comments.deleteConfirmTitle": "Delete comment",
"comments.deleteFailed": "Failed to delete comment.",
"comments.follow": "Follow",
"comments.loadFailed": "Failed to load comments.",
"comments.title": "Comments",
"comments.updateFailed": "Failed to update comment.",
"common.actions": "Actions",
"common.administration": "Administration",
"common.administrationPageTitle": "Administration",
"common.api": "API",
"common.apps": "Apps",
"common.aspectRatio": "AspectRatio",
"common.assets": "Assets",
"common.back": "Back",
"common.backups": "Backups",
"common.bytes": "bytes",
"common.cancel": "Cancel",
"common.clear": "Clear",
"common.clientId": "Client Id",
"common.clients": "Clients",
"common.clientSecret": "Client Secret",
"common.clipboardAdded": "Value has been added to your clipboard.",
"common.clone": "Clone",
"common.cluster": "Cluster",
"common.clusterPageTitle": "Cluster",
"common.comments": "Comments",
"common.confirm": "Confirm",
"common.consumers": "Consumers",
"common.content": "Content",
"common.contents": "Contents",
"common.continue": "Continue",
"common.contributors": "Contributors",
"common.create": "Create",
"common.created": "Created",
"common.date": "Date",
"common.dateTimeEditor.now": "Now",
"common.dateTimeEditor.nowTooltip": "Use Now (UTC)",
"common.dateTimeEditor.today": "Today",
"common.dateTimeEditor.todayTooltip": "Use Today (UTC)",
"common.delete": "Delete",
"common.description": "Description",
"common.displayName": "Display Name",
"common.edit": "Edit",
"common.email": "Email",
"common.error": "Error",
"common.errorBack": "Back to previous page.",
"common.errorNoPermission": "You do not have the permissions to do this.",
"common.errorNotFound": "Not Found",
"common.event": "Event",
"common.events": "Events",
"common.executed": "Executed",
"common.expertMode": "Expert Mode",
"common.failed": "Failed",
"common.fallback": "Fallback",
"common.field": "Field",
"common.files": "Files",
"common.filters": "Filters",
"common.folders": "Folders",
"common.generalSettings": "common",
"common.generate": "Generate",
"common.github": "Github",
"common.height": "Height",
"common.help": "Help",
"common.helpTour": "Click the help icon to show a context specific help page. Go to",
"common.hide": "Hide",
"common.hints": "Hints",
"common.history": "History",
"common.httpConflict": "Failed to make the update. Another user has made a change. Please reload.",
"common.httpLimit": "You have exceeded the maximum limit of API calls.",
"common.label": "Label",
"common.languages": "Languages",
"common.latitudeShort": "Lat",
"common.loading": "Loading",
"common.logout": "Logout",
"common.logs": "Logs",
"common.longitudeShort": "Lon",
"common.mapHide": "Hide map",
"common.mapShow": "Show map",
"common.message": "Message",
"common.name": "Name",
"common.no": "No",
"common.nothingChanged": "Nothing has been changed.",
"common.noValue": "No value",
"common.or": "or",
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.password": "Password",
"common.passwordConfirm": "Confirm Password",
"common.pattern": "Pattern",
"common.patterns": "Patterns",
"common.permissions": "Permissions",
"common.preview": "Preview",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
"common.refresh": "Refresh",
"common.rename": "Rename",
"common.requiredHint": "required",
"common.reset": "Reset",
"common.restore": "Restore",
"common.role": "Role",
"common.roles": "Roles",
"common.rules": "Rules",
"common.sampleCodeLabel": "Sample Code at",
"common.save": "Save",
"common.saveShortcut": "CTRL + S",
"common.schemas": "Schemas",
"common.searchGoogleMaps": "Search Google Maps",
"common.searchResults": "Search Results",
"common.separateByLine": "Separate by line",
"common.settings": "Settings",
"common.sidebarTour": "The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.",
"common.slug": "Slug",
"common.stars.max": "Must not have more more than 15 stars",
"common.status": "Status",
"common.statusChangeTo": "Change to",
"common.submit": "Submit",
"common.subscription": "Subscription",
"common.succeeded": "Succeeded",
"common.tagAdd": ", to add tag",
"common.tagAddReference": ", to add reference",
"common.tagAddSchema": ", to add schema",
"common.tags": "Tags",
"common.tagsAll": "All tags",
"common.time": "Time",
"common.update": "Update",
"common.users": "Users",
"common.value": "Value",
"common.width": "Width",
"common.workflow": "Workflow",
"common.workflows": "Workflows",
"common.yes": "Yes",
"contents.arrayAddItem": "Add Item",
"contents.arrayCloneItem": "Clone this item",
"contents.arrayCollapseAll": "Collapse all items",
"contents.arrayCollapseItem": "Collapse this item",
"contents.arrayExpandAll": "Expand all items",
"contents.arrayExpandItem": "Expand this item",
"contents.arrayMoveBottom": "Move this item to bottom",
"contents.arrayMoveDown": "Move this item down",
"contents.arrayMoveTop": "Move this item to top",
"contents.arrayMoveUp": "Move this item up",
"contents.arrayNoFields": "Add a nested field first to add items.",
"contents.assetsUpload": "Drop files or click",
"contents.autotranslate": "Autotranslate from master language",
"contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.",
"contents.contentNotValid": "Content element not valid, please check the field with the red bar on the left in all languages (if localizable).",
"contents.create": "New",
"contents.createContentTooltip": "New Content (CTRL + SHIFT + G)",
"contents.created": "Content created successfully.",
"contents.createdByFieldDescription": "The user who created the content item.",
"contents.createFailed": "Failed to create content. Please reload.",
"contents.createFieldDescription": "The date time when the content item was created.",
"contents.createPageTitle": "Create Content",
"contents.createTitle": "New Content",
"contents.currentStatusLabel": "Current Version",
"contents.deleteConfirmText": "Do you really want to delete the content?",
"contents.deleteConfirmTitle": "Delete content",
"contents.deleteFailed": "Failed to delete content. Please reload.",
"contents.deleteManyConfirmText": "Do you really want to delete the selected content items?",
"contents.deleteVersionConfirmText": "Do you really want to delete this version?",
"contents.deleteVersionFailed": "Failed to delete version. Please reload.",
"contents.draftNew": "New Draft",
"contents.draftStatus": "New Version",
"contents.editPageTitle": "Edit Content",
"contents.editTitle": "Edit Content",
"contents.languageModeAll": "All Languages",
"contents.languageModeSingle": "Single Language",
"contents.lastModifiedByFieldDescription": "The user who modified the content item the last time.",
"contents.lastModifiedFieldDescription": "The date time when the content item was modified the last time.",
"contents.lastUpdatedLabel": "Last Updated",
"contents.loadContent": "Load",
"contents.loadContentFailed": "Failed to load content. Please reload.",
"contents.loadDataFailed": "Failed to load data. Please reload.",
"contents.loadFailed": "Failed to load contents. Please reload.",
"contents.loadVersionFailed": "Failed to version a new version. Please reload.",
"contents.noReference": "- No Reference -",
"contents.pendingChangesTextToChange": "You have unsaved changes.\n\nWhen you change the status you will loose them.\n\n**Do you want to continue anyway?**",
"contents.pendingChangesTextToClose": "You have unsaved changes.\n\nWhen you close the current content view you will loose them.\n\n**Do you want to continue anyway?**",
"contents.pendingChangesTitle": "Unsaved changes",
"contents.referencesCreateNew": "Add New",
"contents.referencesCreatePublish": "Create and Publish",
"contents.referencesLink": "Link selected contents ({count})",
"contents.referencesSelectExisting": "Select Existing",
"contents.referencesSelectSchema": "Select {schema}",
"contents.refreshTooltip": "Refresh Contents (CTRL + SHIFT + R)",
"contents.reloaded": "Contents reloaded.",
"contents.saveAndPublish": "Save and Publish",
"contents.scheduledAt": "at",
"contents.scheduledAtLabel": "at",
"contents.scheduledTo": "to",
"contents.schemasPageTitle": "Contents",
"contents.searchPlaceholder": "Fulltext search",
"contents.searchSchemasPlaceholder": "Search for schemas...",
"contents.selectionCount": "{count} items selected",
"contents.statusFieldDescription": "The status of the content item.",
"contents.statusQueries": "Status Queries",
"contents.stockPhotoEmpty": "Nothing selected",
"contents.stockPhotoSearch": "Search for Photos by Unsplash",
"contents.unsavedChangesText": "You have unsaved changes. Do you want to load them now?",
"contents.unsavedChangesTitle": "Unsaved changes",
"contents.updated": "Content updated successfully.",
"contents.updateFailed": "Failed to update content. Please reload.",
"contents.validationHint": "Please remember to check all languages when you see validation errors.",
"contents.versionCompare": "Compare",
"contents.versionDelete": "Delete this Version",
"contents.versionFieldDescription": "The version of the content item",
"contents.versionViewing": "Viewing version **{version}**.",
"contents.viewLatest": "View latest",
"contents.viewReset": "Reset Default View",
"contributors.add": "Add Contributor",
"contributors.addFailed": "Failed to add contributors. Please reload.",
"contributors.contributorAssigned": "A new user with the entered email address has been created and assigned as contributor.",
"contributors.contributorAssignedExisting": "User has been assigned",
"contributors.contributorAssignedInvited": "User has been invited and assigned.",
"contributors.contributorAssignedOld": "User has been added as contributor.",
"contributors.deleteConfirmText": "Do you really want to remove the contributor?",
"contributors.deleteConfirmTitle": "Remove contributor",
"contributors.deleteFailed": "Failed to delete contributors. Please reload.",
"contributors.emailPlaceholder": "Find existing user or invite by email",
"contributors.empty": "No contributors found.",
"contributors.import.emailsDetected": "Emails detected: {count}",
"contributors.import.run": "Add Contributors",
"contributors.import.run2": "Import",
"contributors.importButton": "Add many contributors at once",
"contributors.importHintg": "Big team?",
"contributors.importTitle": "Import contributors",
"contributors.loadFailed": "Failed to load contributors. Please reload.",
"contributors.planHint": "Your plan allows up to {maxContributors} contributors.",
"contributors.refreshTooltip": "Refresh contributors (CTRL + SHIFT + R)",
"contributors.reloaded": "Contributors reloaded.",
"contributors.search": "Search",
"contributors.userNotFound": "The user does not exist.",
"dashboard.apiCallsCard": "API Calls",
"dashboard.apiCallsChart": "API Calls Chart",
"dashboard.apiCallsLimitLabel": "Monthly limit",
"dashboard.apiCallsSummaryCard": "API Calls Summary",
"dashboard.apiDocumentation": "API Documentation",
"dashboard.apiPerformanceCard": "API Performance (ms): {summary}ms avg",
"dashboard.apiPerformanceChart": "API Performance Chart",
"dashboard.assetSizeCard": "Assets Size (MB",
"dashboard.assetSizeLabel": "Total Size",
"dashboard.assetSizeLimitLabel": "Total limit",
"dashboard.assetTotalSize": "Asset Total Storage Size",
"dashboard.assetUpdloadsCountChart": "Asset Uploads Count Chart",
"dashboard.assetUploadsCard": "Assets Uploads",
"dashboard.assetUploadsSizeChart": "Asset Uploads Size Chart",
"dashboard.configSaved": "Configuration saved.",
"dashboard.contentApi": "Content API",
"dashboard.contentApiDescription": "OpenAPI 3.0 compatible documentation for your app content.",
"dashboard.contentsSummaryCard": "Number of items",
"dashboard.currentMonthLabel": "This month",
"dashboard.downloadLog": "Download Log",
"dashboard.editConfig": "Edit Config",
"dashboard.github": "Github",
"dashboard.githubCardDescription": "Get the source code from Github and report bugs or ask for support.",
"dashboard.history": "History",
"dashboard.historyCard": "History",
"dashboard.pageTitle": "Dashboard",
"dashboard.resetConfigConfirmText": "Do you really want to reset the dashboard to the default?",
"dashboard.resetConfigConfirmTitle": "Reset config",
"dashboard.schemaNewCard": "New Schema",
"dashboard.schemaNewCardDescription": "A schema defines the structure of your content element.",
"dashboard.schemasCard": "Schemas",
"dashboard.schemasCardDescription": "Get an insight to the data model of this app.",
"dashboard.stackedChart": "Stacked",
"dashboard.supportCard": "Feedback & Support",
"dashboard.supportCardDescription": "Provide feedback and request features to help us to improve Squidex.",
"dashboard.trafficChart": "API Traffic Chart",
"dashboard.trafficSummaryCard": "Traffic (MB)",
"dashboard.welcomeText": "Welcome to **{app}** dashboard.",
"dashboard.welcomeTitle": "Hi {user}",
"eventConsumers.loadFailed": "Failed to load event consumers. Please reload.",
"eventConsumers.pageTitle": "Event Consumers",
"eventConsumers.position": "Position",
"eventConsumers.refreshTooltip": "Refresh event consumers (CTRL + SHIFT + R)",
"eventConsumers.reloaded": "Event Consumers reloaded.",
"eventConsumers.resetFailed": "Failed to reset event consumer. Please reload.",
"eventConsumers.resetTooltip": "Reset Event Consumer",
"eventConsumers.startFailed": "Failed to start event consumer. Please reload.",
"eventConsumers.startTooltip": "Start Event Consumer",
"eventConsumers.stopFailed": "Failed to stop event consumer. Please reload.",
"eventConsumers.stopTooltip": "Stop Event Consumer",
"features.loadFailed": "Failed to load features. Please reload.",
"history.loadFailed": "Failed to load history. Please reload.",
"history.title": "Activity",
"languages.add": "Add Language",
"languages.addFailed": "Failed to add language. Please reload.",
"languages.deleteConfirmText": "Do you really want to remove the language?",
"languages.deleteConfirmTitle": "Remove language",
"languages.deleteFailed": "Failed to delete language. Please reload.",
"languages.loadFailed": "Failed to load languages. Please reload.",
"languages.master": "Is Master",
"languages.masterHint": "Other languages fall back to the master if no fallback is defined.",
"languages.optional": "Is Optional",
"languages.optionalHint": "Values for optional languages must not be entered, even if field is required.",
"languages.refreshTooltip": "Refresh languages (CTRL + SHIFT + R)",
"languages.reloaded": "Languages reloaded.",
"languages.updateFailed": "Failed to change language. Please reload.",
"news.headline": "What's new?",
"news.title": "New Features",
"notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.",
"patterns.deleteConfirmText": "Do you really want to remove this pattern?",
"patterns.deleteConfirmTitle": "Delete pattern",
"patterns.deleteFailed": "Failed to remove pattern. Please reload.",
"patterns.empty": "No pattern created yet.",
"patterns.loadFailed": "Failed to add pattern. Please reload.",
"patterns.nameValidationMessage": "Name can only contain letters, numbers, dashes and spaces.",
"patterns.refreshTooltip": "Refresh patterns (CTRL + SHIFT + R)",
"patterns.reloaded": "Patterns reloaded.",
"patterns.updateFailed": "Failed to update pattern. Please reload.",
"plans.billingPortal": "Billing Portal",
"plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.",
"plans.change": "Change",
"plans.changeConfirmTitle": "Change subscription",
"plans.changeFailed": "Failed to change plan. Please reload.",
"plans.includedCalls": "API Calls",
"plans.includedContributors": "Contributors",
"plans.includedStorage": "Storage",
"plans.loadFailed": "Failed to load plans. Please reload.",
"plans.noPlanConfigured": "No plan configured, this app has unlimited usage.",
"plans.notPlanOwner": "You have not created the subscription. Therefore you cannot change the plan.",
"plans.perMonth": "Per Month",
"plans.perYear": "Per Year",
"plans.refreshTooltip": "Refresh Plans (CTRL + SHIFT + R)",
"plans.reloaded": "Plans reloaded.",
"plans.selected": "&#10003; Selected",
"profile.title": "Profile",
"profile.userEmail": "Signed in with",
"roles.add": "Add role",
"roles.addFailed": "Failed to add role. Please reload.",
"roles.default.owner": "Can do everything, including deleting the app.",
"roles.default.reader": "Can only read assets and contents.",
"roles.defaults.developer": "Can use the API view, edit assets, contents, schemas, rules, workflows and patterns.",
"roles.defaults.editor": "Can edit assets and contents and view workflows.",
"roles.deleteConfirmText": "Delete role",
"roles.deleteConfirmTitle": "Do you really want to delete the role?",
"roles.loadFailed": "Failed to load roles. Please reload.",
"roles.loadPermissionsFailed": "Failed to load permissions. Please reload.",
"roles.refreshTooltip": "Refresh roles (CTRL + SHIFT + R)",
"roles.reloaded": "Roles reloaded.",
"roles.revokeFailed": "Failed to revoke role. Please reload.",
"roles.roleNamePlaceholder": "Enter role name",
"roles.updateFailed": "Failed to update role. Please reload.",
"rules.actionEdit": "Edit Action",
"rules.cancelFailed": "Failed to cancel rule. Please reload.",
"rules.create": "Create new Rule",
"rules.createFailed": "Failed to create rule. Please reload.",
"rules.createTooltip": "New Rule (CTRL + M)",
"rules.deleteConfirmText": "Do you really want to delete the rule?",
"rules.deleteConfirmTitle": "Delete rule",
"rules.deleteFailed": "Failed to delete rule. Please reload.",
"rules.disableFailed": "Failed to disable rule. Please reload.",
"rules.empty": "No rule created yet.",
"rules.emptyAddRule": "Add Rule",
"rules.enableFailed": "Failed to enable rule. Please reload.",
"rules.enqueued": "Rule has been added to the queue.",
"rules.listPageTitle": "Rules",
"rules.loadFailed": "Failed to load Rules. Please reload.",
"rules.readMore": "Read More",
"rules.refreshEventsTooltip": "Refresh Events (CTRL + SHIFT + R)",
"rules.refreshTooltip": "Refresh Rules (CTRL + SHIFT + R)",
"rules.reloaded": "Rules reloaded.",
"rules.restarted": "Rule will start to run in a few seconds.",
"rules.ruleEvents.cancelFailed": "Failed to cancel rule event. Please reload.",
"rules.ruleEvents.enqueue": "Enqueue",
"rules.ruleEvents.enqueued": "Events enqueued. Will be resend in a few seconds.",
"rules.ruleEvents.enqueueFailed": "Failed to enqueue rule event. Please reload.",
"rules.ruleEvents.lastInvokedLabel": "Last Invocation",
"rules.ruleEvents.listPageTitle": "Rule Events",
"rules.ruleEvents.loadFailed": "Failed to load events. Please reload.",
"rules.ruleEvents.nextAttemptLabel": "Next",
"rules.ruleEvents.numAttemptsLabel": "Attempts",
"rules.ruleEvents.reloaded": "RuleEvents reloaded.",
"rules.ruleSyntax.if": "If",
"rules.ruleSyntax.then": "then",
"rules.run": "Run",
"rules.runFailed": "Failed to run rule. Please reload.",
"rules.runningRule": "Rule '{name}' is currently running.",
"rules.runRuleConfirmText": "Do you really want to run the rule for all events?",
"rules.runRuleConfirmTitle": "Run rule",
"rules.stop": "Rule will stop soon.",
"rules.triggerConfirmText": "Do you really want to trigger the rule?",
"rules.triggerConfirmTitle": "Trigger rule",
"rules.triggerEdit": "Edit Trigger",
"rules.triggerFailed": "Failed to trigger rule. Please reload.",
"rules.unnamed": "Unnamed Rule",
"rules.updateFailed": "Failed to update rule. Please reload.",
"rules.wizard.actionHint": "The selection of the action type cannot be changed later.",
"rules.wizard.selectAction": "Select Action",
"rules.wizard.selectTrigger": "Select Trigger",
"rules.wizard.triggerHint": "The selection of the trigger type cannot be changed later.",
"schemas.addField": "Add Field",
"schemas.addFieldAndClose": "Create and close",
"schemas.addFieldAndCreate": "Create and add field",
"schemas.addFieldAndEdit": "Create and edit field",
"schemas.addFieldFailed": "Failed to add field. Please reload.",
"schemas.addNestedField": "Add Nested Field",
"schemas.changeCategoryFailed": "Failed to change category. Please reload.",
"schemas.clone": "Clone Schema",
"schemas.contextMenuTour": "Open the context menu to delete the schema or to create some scripts for content changes.",
"schemas.create": "Create Schema",
"schemas.createCategory": "Create new category...",
"schemas.createFailed": "Failed to create schema. Please reload.",
"schemas.createSchemaTooltip": "New Schema (CTRL + SHIFT + G)",
"schemas.deleteConfirmText": "Do you really want to delete the schema?",
"schemas.deleteConfirmTitle": "Delete schema",
"schemas.deleteFailed": "Failed to delete schema. Please reload.",
"schemas.deleteFieldFailed": "Failed to delete field. Please reload.",
"schemas.deleteRuleConfirmText": "Do you really want to remove this rule?",
"schemas.deleteRuleConfirmTitle": "Remove rule",
"schemas.deleteUrlConfirmText": "Do you really want to remove this URL?",
"schemas.deleteUrlConfirmTitle": "Remove URL",
"schemas.disableFieldFailed": "Failed to disable field. Please reload.",
"schemas.enableFieldFailed": "Failed to enable field. Please reload.",
"schemas.export.deleteFields": "Delete fields",
"schemas.export.recreateFields": "Recreate fields",
"schemas.export.synchronize": "Synchronize",
"schemas.field.allowedValues": "Allowed Values",
"schemas.field.deleteConfirmText": "Do you really want to delete the field?",
"schemas.field.deleteConfirmTitle": "Delete field",
"schemas.field.disable": "Disable in UI",
"schemas.field.disabledMarker": "Disabled",
"schemas.field.editorUrl": "Editor Url",
"schemas.field.editorUrlHint": "Url to your plugin if you use a custom editor.",
"schemas.field.enable": "Enable in UI",
"schemas.field.enabledMarker": "Enabled",
"schemas.field.hide": "Hide in API",
"schemas.field.labelHint": "Describe this field for documentation and user interfaces.",
"schemas.field.localizable": "Localizable",
"schemas.field.localizableHint": "You can mark the field as localizable. It means that is dependent on the language, for example a city name.",
"schemas.field.localizableMarker": "localizable",
"schemas.field.lock": "Lock and prevent changes",
"schemas.field.lockConfirmText": "WARNING: Locking a field cannot be undone! Locked field definitions cannot be unlocked, deleted, or changed anymore. Do you really want to lock this field?",
"schemas.field.lockedMarker": "Locked",
"schemas.field.nameHint": "The name of the field in the API response.",
"schemas.field.namePlaceholder": "Enter field name",
"schemas.field.nameValidationMessage": "Name must be a valid javascript name in camel case.",
"schemas.field.required": "Required",
"schemas.field.show": "Show in API",
"schemas.field.tabCommon": "Common",
"schemas.field.tabUI": "UI",
"schemas.field.tabValidation": "Validation",
"schemas.field.tagsHint": "Tags to annotate your field for automation processes.",
"schemas.field.unique": "Unique",
"schemas.fields.defaultValue": "Default Value",
"schemas.fields.editor": "Editor",
"schemas.fields.empty": "No field created yet.",
"schemas.fields.inlineEditable": "Inline Editable",
"schemas.fields.placeholder": "Placeholder",
"schemas.fields.placeholderHint": "Define the placeholder for the input control.",
"schemas.fieldTypes.array.count": "Items",
"schemas.fieldTypes.array.countMax": "Max Items",
"schemas.fieldTypes.array.countMin": "Min Items",
"schemas.fieldTypes.array.description": "List of embedded objects.",
"schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values",
"schemas.fieldTypes.assets.count": "Count",
"schemas.fieldTypes.assets.countMax": "Max Assets",
"schemas.fieldTypes.assets.countMin": "Min Assets",
"schemas.fieldTypes.assets.description": "Images, videos, documents.",
"schemas.fieldTypes.assets.fileExtensions": "File Extensions",
"schemas.fieldTypes.assets.mustBeImage": "Must be Image",
"schemas.fieldTypes.assets.previewMode": "PreviewMode",
"schemas.fieldTypes.assets.previewModeHint": "The preview mode for assets in content lists.",
"schemas.fieldTypes.assets.previewModeName": "Only file name",
"schemas.fieldTypes.assets.previewModeTumbnailName": "Thumbnail and file name",
"schemas.fieldTypes.assets.previewModeTumbnailOrName": "Only thumbnail or file name if not an image",
"schemas.fieldTypes.assets.resolve": "Resolve first asset",
"schemas.fieldTypes.assets.resolveHint": "Show the first referenced asset in the content list.",
"schemas.fieldTypes.assets.size": "Size",
"schemas.fieldTypes.assets.sizeMax": "Min Size",
"schemas.fieldTypes.assets.sizeMin": "Max Size",
"schemas.fieldTypes.boolean.description": "Yes or no, true or false.",
"schemas.fieldTypes.dateTime.defaultMode": "Default Mode",
"schemas.fieldTypes.dateTime.description": "Events date, opening hours.",
"schemas.fieldTypes.dateTime.rangeMax": "Max Value",
"schemas.fieldTypes.dateTime.rangeMin": "Min Value",
"schemas.fieldTypes.geolocation.description": "Coordinates: latitude and longitude.",
"schemas.fieldTypes.json.description": "Data in JSON format, for developers.",
"schemas.fieldTypes.number.description": "ID, order number, rating, quantity.",
"schemas.fieldTypes.number.range": "Range",
"schemas.fieldTypes.number.rangeMax": "Max Value",
"schemas.fieldTypes.number.rangeMin": "Min Value",
"schemas.fieldTypes.references.count": "Items",
"schemas.fieldTypes.references.countMax": "Max Items",
"schemas.fieldTypes.references.countMin": "Min Items",
"schemas.fieldTypes.references.description": "Links to other content items.",
"schemas.fieldTypes.references.resolveHint": "Show the name of the referenced item in content list when MaxItems is set to 1.",
"schemas.fieldTypes.string.description": "Titles, names, paragraphs.",
"schemas.fieldTypes.string.length": "Length",
"schemas.fieldTypes.string.lengthMax": "Max Length",
"schemas.fieldTypes.string.lengthMin": "Min Length",
"schemas.fieldTypes.string.pattern": "Regex Pattern",
"schemas.fieldTypes.string.patternMessage": "Pattern Message",
"schemas.fieldTypes.string.suggestions": "Suggestions",
"schemas.fieldTypes.tags.count": "Items",
"schemas.fieldTypes.tags.countMax": "Max Items",
"schemas.fieldTypes.tags.countMin": "Min Items",
"schemas.fieldTypes.tags.description": "Special format for tags.",
"schemas.fieldTypes.ui.description": "Separator for editing UI.",
"schemas.hideFieldFailed": "Failed to hide field. Please reload.",
"schemas.import": "Import schema",
"schemas.labelHint": "Display name for documentation and user interfaces.",
"schemas.listFields": "List Fields",
"schemas.listFieldsEmpty": "Drop field here or reorder them to show the fields in the content list. When no list field is defined, the first field is used.",
"schemas.loadFailed": "Failed to load schemas. Please reload.",
"schemas.loadSchemaFailed": "Failed to load schema. Please reload.",
"schemas.lockFieldFailed": "Failed to lock field. Please reload.",
"schemas.modeMultiple": "Multiple contents",
"schemas.modeMultipleDescription": "Best for multiple instances like blog posts, pages, authors, products...",
"schemas.modeSingle": "Single content",
"schemas.modeSingleDescription": "Best for single instances like the home page, privacy policies, settings...",
"schemas.nameWarning": "These values cannot be changed later.",
"schemas.previewUrls.empty": "No preview urls configured.",
"schemas.previewUrls.help": "Checkout the integrated help page to learn more about preview URL's.",
"schemas.previewUrls.namePlaceholder": "Web or Mobile",
"schemas.previewUrls.title": "Preview URLs",
"schemas.previewUrls.urlPlaceholder": "URL with variables",
"schemas.published": "Published",
"schemas.publishedTour": "Note, that you have to publish the schema before you can add content to it.",
"schemas.publishFailed": "Failed to publish schema. Please reload.",
"schemas.referenceFields": "Reference Fields",
"schemas.referenceFieldsEmpty": "Drop field here or reorder them to show the fields when referenced by another content. When no reference field is defined, the list fields are used instead.",
"schemas.reloaded": "Schemas reloaded.",
"schemas.reorderFieldsFailed": "Failed to reorder fields. Please reload.",
"schemas.rules.condition": "Condition in Javascript",
"schemas.rules.disable": "Disable",
"schemas.rules.empty": "No field rules configured.",
"schemas.rules.require": "Require",
"schemas.rules.title": "Field rules",
"schemas.rules.when": "when",
"schemas.saved": "Schema saved successfully.",
"schemas.saveFieldAndClose": "Save and close",
"schemas.saveFieldAndNew": "Save and add field",
"schemas.schemaNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.",
"schemas.schemaNameValidationMessage": "Name can only contain letters, numbers, dashes and spaces.",
"schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.",
"schemas.searchPlaceholder": "Search for schemas...",
"schemas.showFieldFailed": "Failed to show field. Please reload.",
"schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.",
"schemas.ui": "Assigned fields",
"schemas.ui.unassignedFields": "Unassigned fields",
"schemas.unpublished": "Unpublished",
"schemas.unpublishFailed": "Failed to unpublish schema. Please reload.",
"schemas.updateFailed": "Failed to update schema. Please reload.",
"schemas.updateFieldFailed": "Failed to update field. Please reload.",
"schemas.updatePreviewUrlsFailed": "Failed to configure preview urls. Please reload.",
"schemas.updateRulesFailed": "Failed to update schema rules. Please reload.",
"schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.",
"schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.",
"search.addFilter": "Add Filter",
"search.addGroup": "Add Group",
"search.addSorting": "Add Sorting",
"search.advancedTour": "Click this icon to show the advanced search menu!",
"search.customQuery": "Custom Query",
"search.fullTextTour": "Search for content using full text search over all fields and languages!",
"search.help": "Read more about filtering in the [Documentation](https://https://docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "My queries",
"search.nameQuery": "Name your query",
"search.queriesEmpty": "Search for {types} and use <i class=\"icon-star-empty\"></i> icon in search form to save query for all contributors.",
"search.queryAllNewestFirst": "All (newest first)",
"search.queryAllOldestFirst": "All (oldest first)",
"search.quickNavPlaceholder": "Quick Nav (Press 'q')",
"search.saveQueryMyself": "Save the query only for myself.",
"search.searchFailed": "Failed to make search. Please reload.",
"search.sharedQueries": "Shared queries",
"search.sorting": "Sorting",
"start.login": "Login to Squidex",
"start.loginHint": "The login button will open a new popup. Once you are logged in successful we will redirect you to the Squidex management portal.",
"start.madeBy": "Proudly made by",
"start.madeByCopyright": "Qaisar Ahmad &amp; Sebastian Stehle, 2016-2020",
"tour.joinForum": "Join our Forum",
"tour.joinGithub": "Join us on Github",
"tour.skip": "Skip Tour",
"tour.step0Next": "Let's take a look around",
"tour.step0Text": "You can start managing and distributing your content right away, but we we'd like to walk you through some basics first...\n\nHow'that",
"tour.step1Next": "Continue",
"tour.step1Text": "An App is the repository for your project, e.g. (blog, web shop or mobile app). You can assign contributors to your app to work together.\n\nYou can create an unlimited number of Apps in Squidex to manage multiple projects at the same time.",
"tour.step2Next": "Keep going!",
"tour.step2Text": "Schemas define the structure of your content, the fields and the data types of a content item.\n\nBefore you can add content to your schema, make sure to hit the 'Publish' button at the top to make the schema available to your content editors.",
"tour.step3Next": "Almost there!",
"tour.step3Text": "Content is the actual data in your app which is grouped by the schema.\n\nSelect a published schema first, then add content for this schema.",
"tour.step4Next": "Got It!",
"tour.step4Text": "The assets contains all files that can also be linked to your content. For example images, videos or documents.\n\nYou can upload the assets here and use them later or also upload them directly when you create a new content item with an asset field.",
"tour.step5Text": "But that's not all of the support we can provide.\n\nYou can got to https://docs.squidex.io/> to read more.\n\nDo you want to join our community?",
"tour.step5Title": "Awesome, now you know the basics!",
"tour.tooltipConfirm": "Got It",
"tour.tooltipStop": "Stop Tour",
"tour.welcome": "Welcome to",
"tour.welcomeProduct": "Squidex CMS",
"translate.translateFailed": "Failed to translate text. Please reload.",
"usages.loadCallsFailed": "Failed to load calls usage. Please reload.",
"usages.loadMonthlyCallsFailed": "Failed to load monthly API calls. Please reload.",
"usages.loadStorageFailed": "Failed to load storage usage. Please reload.",
"usages.loadTodayStorageFailed": "Failed to load todays storage size. Please reload.",
"users.create": "New",
"users.createFailed": "Failed to create user. Please reload.",
"users.createPageTitle": "Create User",
"users.createTitle": "New User",
"users.createTooltip": "New User (CTRL + N)",
"users.editPageTitle": "Edit User",
"users.editTitle": "Edit User",
"users.listPageTitle": "User Management",
"users.listTitle": "Users",
"users.loadFailed": "Failed to load users. Please reload.",
"users.loadUserFailed": "Failed to load user. Please reload.",
"users.lockTooltip": "Lock User",
"users.passwordConfirmValidationMessage": "Passwords must be the same.",
"users.refreshTooltip": "Refresh Users (CTRL + SHIFT + R)",
"users.reloaded": "Users reloaded.",
"users.search": "Search for user",
"users.unlockTooltip": "Unlock User",
"users.updateFailed": "Failed to update user. Please reload.",
"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.",
"workflows.add": "Add Workflow",
"workflows.addStep": "Add Step",
"workflows.createFailed": "Failed to create workflow. Please reload.",
"workflows.deleteConfirmText": "Do you really want to remove the workflow?",
"workflows.deleteConfirmTitle": "Delete workflow",
"workflows.deleteFailed": "Failed to delete Workflow. Please reload.",
"workflows.empty": "No workflows created yet.",
"workflows.loadFailed": "Failed to load workflows. Please reload.",
"workflows.notNamed": "Unnamed Workflow",
"workflows.preventUpdates": "Prevent updates",
"workflows.publishedNotRemovable": "Cannot be removed",
"workflows.refreshTooltip": "Refresh workflows (CTRL + SHIFT + R)",
"workflows.reloaded": "Workflows reloaded.",
"workflows.saved": "Workflow has been saved.",
"workflows.schemasHint": "Restrict this workflow to specific schemas or keep it empty for all schemas.",
"workflows.syntax.expression": "Expression",
"workflows.syntax.for": "for",
"workflows.syntax.when": "when",
"workflows.tabEdit": "Editing",
"workflows.tabVisualize": "Visualize",
"workflows.updateFailed": "Failed to update Workflow. Please reload.",
"workflows.workflowNameHint": "Optional name for the workflow.",
"workflows.workflowNamePlaceholder": "Enter workflow name"
}

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

@ -35,7 +35,7 @@ namespace Squidex.Config.Web
public static IApplicationBuilder UseSquidexLocalization(this IApplicationBuilder app) public static IApplicationBuilder UseSquidexLocalization(this IApplicationBuilder app)
{ {
var supportedCultures = new[] { "en-US" }; var supportedCultures = new[] { "en" };
var localizationOptions = new RequestLocalizationOptions() var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0]) .SetDefaultCulture(supportedCultures[0])

13
backend/src/Squidex/Squidex.csproj

@ -86,6 +86,7 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Areas\Api\Controllers\Users\Assets\Avatar.png" /> <EmbeddedResource Include="Areas\Api\Controllers\Users\Assets\Avatar.png" />
<EmbeddedResource Include="Areas\Frontend\Resources\texts.en" />
<EmbeddedResource Include="Docs\schemabody.md" /> <EmbeddedResource Include="Docs\schemabody.md" />
<EmbeddedResource Include="Docs\schemaquery.md" /> <EmbeddedResource Include="Docs\schemaquery.md" />
<EmbeddedResource Include="Docs\security.md" /> <EmbeddedResource Include="Docs\security.md" />
@ -94,19 +95,15 @@
<EmbeddedResource Include="Pipeline\Squid\icon-sad-sm.svg" /> <EmbeddedResource Include="Pipeline\Squid\icon-sad-sm.svg" />
<EmbeddedResource Include="Pipeline\Squid\icon-sad.svg" /> <EmbeddedResource Include="Pipeline\Squid\icon-sad.svg" />
<EmbeddedResource Remove="Assets\**" /> <EmbeddedResource Remove="Assets\**" />
<EmbeddedResource Remove="_test-output\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="Assets\**" /> <Compile Remove="Assets\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Remove="Assets\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Areas\Api\Controllers\Users\Assets\Avatar.png" /> <None Remove="Areas\Api\Controllers\Users\Assets\Avatar.png" />
<None Remove="Areas\Frontend\Resources\texts.en" />
<None Remove="Docs\schemabody.md" /> <None Remove="Docs\schemabody.md" />
<None Remove="Docs\schemaquery.md" /> <None Remove="Docs\schemaquery.md" />
<None Remove="Docs\security.md" /> <None Remove="Docs\security.md" />
@ -120,9 +117,11 @@
<None Remove="Assets\**" /> <None Remove="Assets\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Remove="Assets\**" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060</NoWarn> <NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060</NoWarn>
</PropertyGroup> </PropertyGroup>
<ProjectExtensions><VisualStudio><UserProperties appsettings_1development_1json__JsonSchema="http://json.schemastore.org/BizTalkServerApplicationSchema" /></VisualStudio></ProjectExtensions>
</Project> </Project>

25
backend/tests/Squidex.Infrastructure.Tests/Translations/TTests.cs

@ -22,42 +22,41 @@ namespace Squidex.Infrastructure.Translations
[Fact] [Fact]
public void Should_return_key_if_not_found() public void Should_return_key_if_not_found()
{ {
var (result, notFound) = sut.Get(CultureInfo.CurrentUICulture, "key", "fallback"); var result = sut.Get(CultureInfo.CurrentUICulture, "key", "fallback");
Assert.Equal("fallback", result); Assert.Equal(("fallback", false), result);
Assert.True(notFound);
} }
[Fact] [Fact]
public void Should_return_simple_key() public void Should_return_simple_key()
{ {
var (result, _) = sut.Get(CultureInfo.CurrentUICulture, "simple", "fallback"); var result = sut.Get(CultureInfo.CurrentUICulture, "simple", "fallback");
Assert.Equal("Simple Result", result); Assert.Equal(("Simple Result", true), result);
} }
[Fact] [Fact]
public void Should_return_text_with_variable() public void Should_return_text_with_variable()
{ {
var (result, _) = sut.Get(CultureInfo.CurrentUICulture, "withVar", "fallback", new { var = 5 }); var result = sut.Get(CultureInfo.CurrentUICulture, "withVar", "fallback", new { var = 5 });
Assert.Equal("Var: 5.", result); Assert.Equal(("Var: 5.", true), result);
} }
[Fact] [Fact]
public void Should_return_text_with_lower_var() public void Should_return_text_with_lower_variable()
{ {
var (result, _) = sut.Get(CultureInfo.CurrentUICulture, "withLowerVar", "fallback", new { var = "Lower" }); var result = sut.Get(CultureInfo.CurrentUICulture, "withLowerVar", "fallback", new { var = "Lower" });
Assert.Equal("Var: lower.", result); Assert.Equal(("Var: lower.", true), result);
} }
[Fact] [Fact]
public void Should_return_text_with_upper_var() public void Should_return_text_with_upper_variable()
{ {
var (result, _) = sut.Get(CultureInfo.CurrentUICulture, "withUpperVar", "fallback", new { var = "upper" }); var result = sut.Get(CultureInfo.CurrentUICulture, "withUpperVar", "fallback", new { var = "upper" });
Assert.Equal("Var: Upper.", result); Assert.Equal(("Var: Upper.", true), result);
} }
} }
} }

6
frontend/app/app.component.html

@ -1,11 +1,11 @@
<main> <main>
<sqx-root-view> <sqx-root-view>
<sqx-dialog-renderer> <sqx-dialog-renderer>
<router-outlet (activate)="isLoaded = true"> <router-outlet (activate)="isLoaded = true">
<div class="loading" *ngIf="!isLoaded"> <div class="loading" *ngIf="!isLoaded">
<img alt="Loading" src="./images/loader.gif" /> <img alt="Loading" src="./images/loader.gif">
<div>Loading Squidex</div> <div>{{ 'common.loading' | sqxTranslate }}</div>
</div> </div>
</router-outlet> </router-outlet>
</sqx-dialog-renderer> </sqx-dialog-renderer>

13
frontend/app/app.module.ts

@ -14,7 +14,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { routing } from './app.routes'; import { routing } from './app.routes';
import { ApiUrlConfig, CurrencyConfig, DecimalSeparatorConfig, SqxFrameworkModule, SqxSharedModule, TitlesConfig, UIOptions } from './shared'; import { ApiUrlConfig, CurrencyConfig, DecimalSeparatorConfig, LocalizerService, SqxFrameworkModule, SqxSharedModule, TitlesConfig, UIOptions } from './shared';
import { SqxShellModule } from './shell'; import { SqxShellModule } from './shell';
export function configApiUrl() { export function configApiUrl() {
@ -42,7 +42,7 @@ export function configUIOptions() {
} }
export function configTitles() { export function configTitles() {
return new TitlesConfig(undefined, 'Squidex Headless CMS'); return new TitlesConfig(undefined, 'i18n:common.product');
} }
export function configDecimalSeparator() { export function configDecimalSeparator() {
@ -53,6 +53,14 @@ export function configCurrency() {
return new CurrencyConfig('EUR', '€', true); return new CurrencyConfig('EUR', '€', true);
} }
export function configTranslations() {
if (process.env.NODE_ENV === 'production') {
return new LocalizerService(window['texts']);
} else {
return new LocalizerService(require('./../../i18n/translations-frontend.en.json')).logMissingKeys();
}
}
@NgModule({ @NgModule({
imports: [ imports: [
BrowserAnimationsModule, BrowserAnimationsModule,
@ -75,6 +83,7 @@ export function configCurrency() {
{ provide: CurrencyConfig, useFactory: configCurrency }, { provide: CurrencyConfig, useFactory: configCurrency },
{ provide: DecimalSeparatorConfig, useFactory: configDecimalSeparator }, { provide: DecimalSeparatorConfig, useFactory: configDecimalSeparator },
{ provide: TitlesConfig, useFactory: configTitles }, { provide: TitlesConfig, useFactory: configTitles },
{ provide: LocalizerService, useFactory: configTranslations },
{ provide: UIOptions, useFactory: configUIOptions } { provide: UIOptions, useFactory: configUIOptions }
], ],
entryComponents: [AppComponent] entryComponents: [AppComponent]

10
frontend/app/features/administration/administration-area.component.html

@ -1,25 +1,25 @@
<sqx-title message="Administration"></sqx-title> <sqx-title message="i18n:common.administrationPageTitle"></sqx-title>
<div class="sidebar"> <div class="sidebar">
<ul class="nav nav-panel flex-column"> <ul class="nav nav-panel flex-column">
<li class="nav-item" *ngIf="uiState.canReadEvents | async"> <li class="nav-item" *ngIf="uiState.canReadEvents | async">
<a class="nav-link" routerLink="event-consumers" routerLinkActive="active"> <a class="nav-link" routerLink="event-consumers" routerLinkActive="active">
<i class="nav-icon icon-time"></i> <div class="nav-text">Consumers</div> <i class="nav-icon icon-time"></i> <div class="nav-text">{{ 'common.consumers' | sqxTranslate }}</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="uiState.canReadUsers | async"> <li class="nav-item" *ngIf="uiState.canReadUsers | async">
<a class="nav-link" routerLink="users" routerLinkActive="active"> <a class="nav-link" routerLink="users" routerLinkActive="active">
<i class="nav-icon icon-user-o"></i> <div class="nav-text">Users</div> <i class="nav-icon icon-user-o"></i> <div class="nav-text">{{ 'common.users' | sqxTranslate }}</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="uiState.canRestore | async"> <li class="nav-item" *ngIf="uiState.canRestore | async">
<a class="nav-link" routerLink="restore" routerLinkActive="active"> <a class="nav-link" routerLink="restore" routerLinkActive="active">
<i class="nav-icon icon-backup"></i> <div class="nav-text">Restore</div> <i class="nav-icon icon-backup"></i> <div class="nav-text">{{ 'common.restore' | sqxTranslate }}</div>
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="uiState.canUseOrleans | async"> <li class="nav-item" *ngIf="uiState.canUseOrleans | async">
<a class="nav-link" routerLink="cluster" routerLinkActive="active"> <a class="nav-link" routerLink="cluster" routerLinkActive="active">
<i class="nav-icon icon-orleans"></i> <div class="nav-text">Cluster</div> <i class="nav-icon icon-orleans"></i> <div class="nav-text">{{ 'common.cluster' | sqxTranslate }}</div>
</a> </a>
</li> </li>
</ul> </ul>

2
frontend/app/features/administration/pages/cluster/cluster-page.component.html

@ -1,4 +1,4 @@
<sqx-title message="Cluster"></sqx-title> <sqx-title message="i18n:common.clusterPageTitle"></sqx-title>
<sqx-panel desiredWidth="*" minWidth="50rem" isFullSize="true"> <sqx-panel desiredWidth="*" minWidth="50rem" isFullSize="true">
<div inner> <div inner>

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

@ -10,13 +10,13 @@
<span>{{eventConsumer.position}}</span> <span>{{eventConsumer.position}}</span>
</td> </td>
<td class="cell-actions-lg"> <td class="cell-actions-lg">
<button type="button" class="btn btn-text" (click)="reset()" *ngIf="eventConsumer.canReset" title="Reset Event Consumer"> <button type="button" class="btn btn-text" (click)="reset()" *ngIf="eventConsumer.canReset" title="i18n:eventConsumers.resetTooltip">
<i class="icon icon-reset"></i> <i class="icon icon-reset"></i>
</button> </button>
<button type="button" class="btn btn-text" (click)="start()" *ngIf="eventConsumer.canStart" title="Start Event Consumer"> <button type="button" class="btn btn-text" (click)="start()" *ngIf="eventConsumer.canStart" title="i18n:eventConsumers.startTooltip">
<i class="icon icon-play"></i> <i class="icon icon-play"></i>
</button> </button>
<button type="button" class="btn btn-text" (click)="stop()" *ngIf="eventConsumer.canStop" title="Stop Event Consumer"> <button type="button" class="btn btn-text" (click)="stop()" *ngIf="eventConsumer.canStop" title="i18n:eventConsumers.stopTooltip">
<i class="icon icon-pause"></i> <i class="icon icon-pause"></i>
</button> </button>
</td> </td>

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

@ -1,13 +1,13 @@
<sqx-title message="Event Consumers"></sqx-title> <sqx-title message="i18n:eventConsumers.pageTitle"></sqx-title>
<sqx-panel theme="light" desiredWidth="50rem" grid="true"> <sqx-panel theme="light" desiredWidth="50rem" grid="true">
<ng-container title> <ng-container title>
Consumers {{ 'common.consumers' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh event consumers (CTRL + SHIFT + R)"> <button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:eventConsumers.refreshTooltip">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
@ -20,13 +20,13 @@
<thead> <thead>
<tr> <tr>
<th class="cell-auto"> <th class="cell-auto">
Name {{ 'common.name' | sqxTranslate }}
</th> </th>
<th class="cell-auto-right"> <th class="cell-auto-right">
Position {{ 'eventConsumers.position' | sqxTranslate }}
</th> </th>
<th class="cell-actions-lg"> <th class="cell-actions-lg">
Actions {{ 'common.actions' | sqxTranslate }}
</th> </th>
</tr> </tr>
</thead> </thead>
@ -47,7 +47,7 @@
<ng-container *sqxModal="eventConsumerErrorDialog"> <ng-container *sqxModal="eventConsumerErrorDialog">
<sqx-modal-dialog (close)="eventConsumerErrorDialog.hide()"> <sqx-modal-dialog (close)="eventConsumerErrorDialog.hide()">
<ng-container title> <ng-container title>
Error {{ 'common.error' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>

16
frontend/app/features/administration/pages/restore/restore-page.component.html

@ -1,8 +1,8 @@
<sqx-title message="Restore Backup"></sqx-title> <sqx-title message="i18n:backups.restorePageTitle"></sqx-title>
<sqx-panel theme="light" desiredWidth="70rem"> <sqx-panel theme="light" desiredWidth="70rem">
<ng-container title> <ng-container title>
Restore Backup {{ 'backups.restoreTitle' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
@ -22,7 +22,7 @@
</div> </div>
<div class="col"> <div class="col">
<h3>Last Restore Operation</h3> <h3>{{ 'backups.restoreLastStatus' | sqxTranslate }}</h3>
</div> </div>
<div class="col text-right restore-url"> <div class="col text-right restore-url">
@ -38,10 +38,10 @@
<div class="card-footer text-muted"> <div class="card-footer text-muted">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
Started: {{job.started | sqxISODate}} {{ 'backups.restoreStartedLabel' | sqxTranslate }}: {{job.started | sqxISODate}}
</div> </div>
<div class="col text-right" *ngIf="job.stopped"> <div class="col text-right" *ngIf="job.stopped">
Stopped: {{job.stopped | sqxISODate}} {{ 'backups.restoreStoppedLabel' | sqxTranslate }}: {{job.stopped | sqxISODate}}
</div> </div>
</div> </div>
</div> </div>
@ -51,13 +51,13 @@
<form [formGroup]="restoreForm.form" (ngSubmit)="restore()"> <form [formGroup]="restoreForm.form" (ngSubmit)="restore()">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<input class="form-control" formControlName="url" placeholder="Url to backup" /> <input class="form-control" formControlName="url" placeholder="{{ 'backups.restoreLastUrl' | sqxTranslate }}">
</div> </div>
<div class="col pl-1"> <div class="col pl-1">
<input class="form-control" formControlName="name" placeholder="Optional app name" /> <input class="form-control" formControlName="name" placeholder="{{ 'backups.restoreNewAppName' | sqxTranslate }}">
</div> </div>
<div class="col-auto pl-1"> <div class="col-auto pl-1">
<button type="submit" class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async">Restore Backup</button> <button type="submit" class="btn btn-success" [disabled]="restoreForm.hasNoUrl | async">{{ 'backups.restore' | sqxTranslate }}</button>
</div> </div>
</div> </div>
</form> </form>

2
frontend/app/features/administration/pages/restore/restore-page.component.ts

@ -37,7 +37,7 @@ export class RestorePageComponent {
this.backupsService.postRestore(value) this.backupsService.postRestore(value)
.subscribe(() => { .subscribe(() => {
this.dialogs.notifyInfo('Restore started, it can take several minutes to complete.'); this.dialogs.notifyInfo('i18n:backups.restoreStarted');
}, error => { }, error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);
}); });

40
frontend/app/features/administration/pages/users/user-page.component.html

@ -1,27 +1,27 @@
<form [formGroup]="userForm.form" (ngSubmit)="save()"> <form [formGroup]="userForm.form" (ngSubmit)="save()">
<input style="display:none" type="password" name="foilautofill"/> <input style="display:none" type="password" name="foilautofill">
<sqx-panel desiredWidth="26rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="26rem" isBlank="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
<ng-container *ngIf="usersState.selectedUser | async; else noUserTitle"> <ng-container *ngIf="usersState.selectedUser | async; else noUserTitle">
<sqx-title message="Edit User"></sqx-title> <sqx-title message="i18n:users.editPageTitle"></sqx-title>
Edit User {{ 'users.editTitle' | sqxTranslate }}
</ng-container> </ng-container>
<ng-template #noUserTitle> <ng-template #noUserTitle>
<sqx-title message="New User"></sqx-title> <sqx-title message="i18n:users.createPageTitle"></sqx-title>
New User {{ 'users.createTitle' | sqxTranslate }}
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<ng-container *ngIf="usersState.selectedUser | async; let user; else noUserMenu"> <ng-container *ngIf="usersState.selectedUser | async; let user; else noUserMenu">
<ng-container *ngIf="isEditable"> <ng-container *ngIf="isEditable">
<button type="submit" class="btn btn-primary" title="CTRL + S"> <button type="submit" class="btn btn-primary" title="i18n:common.saveShortcut">
Save {{ 'common.save' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> <sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut>
@ -29,8 +29,8 @@
</ng-container> </ng-container>
<ng-template #noUserMenu> <ng-template #noUserMenu>
<button type="submit" class="btn btn-primary" title="CTRL + S"> <button type="submit" class="btn btn-primary" title="i18n:common.saveShortcut">
Save {{ 'common.save' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> <sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut>
@ -41,44 +41,44 @@
<sqx-form-error [error]="userForm.error | async"></sqx-form-error> <sqx-form-error [error]="userForm.error | async"></sqx-form-error>
<div class="form-group"> <div class="form-group">
<label for="email">Email <small class="hint">(required)</small></label> <label for="email">{{ 'common.email' | sqxTranslate }} <small class="hint">({{ 'common.requiredHint' | sqxTranslate }})</small></label>
<sqx-control-errors for="email" ></sqx-control-errors> <sqx-control-errors for="email"></sqx-control-errors>
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" autocomplete="off" /> <input type="email" class="form-control" id="email" maxlength="100" formControlName="email" autocomplete="off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="displayName">Display Name <small class="hint">(required)</small></label> <label for="displayName">{{ 'common.displayName' | sqxTranslate }} <small class="hint">({{ 'common.requiredHint' | sqxTranslate }})</small></label>
<sqx-control-errors for="displayName"></sqx-control-errors> <sqx-control-errors for="displayName"></sqx-control-errors>
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="off" spellcheck="false" /> <input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="off" spellcheck="false">
</div> </div>
<div class="form-group form-group-section"> <div class="form-group form-group-section">
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">{{ 'common.password' | sqxTranslate }}</label>
<sqx-control-errors for="password"></sqx-control-errors> <sqx-control-errors for="password"></sqx-control-errors>
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="off" /> <input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Confirm Password</label> <label for="password">{{ 'common.passwordConfirm' | sqxTranslate }}</label>
<sqx-control-errors for="passwordConfirm"></sqx-control-errors> <sqx-control-errors for="passwordConfirm"></sqx-control-errors>
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="off" /> <input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="off">
</div> </div>
</div> </div>
<div class="form-group form-group-section"> <div class="form-group form-group-section">
<label for="permissions">Permissions</label> <label for="permissions">{{ 'common.permissions' | sqxTranslate }}</label>
<sqx-control-errors for="permissions"></sqx-control-errors> <sqx-control-errors for="permissions"></sqx-control-errors>
<textarea class="form-control" id="permissions" formControlName="permissions" placeholder="Separate by line" autocomplete="off" spellcheck="false"></textarea> <textarea class="form-control" id="permissions" formControlName="permissions" placeholder="{{ 'common.separateByLine' | sqxTranslate }}" autocomplete="off" spellcheck="false"></textarea>
</div> </div>
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>

6
frontend/app/features/administration/pages/users/user.component.html

@ -1,6 +1,6 @@
<tr [routerLink]="user.id" queryParamsHandling="merge" routerLinkActive="active"> <tr [routerLink]="user.id" queryParamsHandling="merge" routerLinkActive="active">
<td class="cell-user"> <td class="cell-user">
<img class="user-picture" title="{{user.displayName}}" [src]="user | sqxUserDtoPicture" /> <img class="user-picture" title="{{user.displayName}}" [src]="user | sqxUserDtoPicture">
</td> </td>
<td class="cell-auto"> <td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span> <span class="user-name table-cell">{{user.displayName}}</span>
@ -9,10 +9,10 @@
<span class="user-email table-cell">{{user.email}}</span> <span class="user-email table-cell">{{user.email}}</span>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<button type="button" class="btn btn-text" (click)="lock()" sqxStopClick *ngIf="user.canLock" title="Lock User"> <button type="button" class="btn btn-text" (click)="lock()" sqxStopClick *ngIf="user.canLock" title="i18n:users.lockTooltip">
<i class="icon icon-unlocked"></i> <i class="icon icon-unlocked"></i>
</button> </button>
<button type="button" class="btn btn-text" (click)="unlock()" sqxStopClick *ngIf="user.canUnlock" title="Unlock User"> <button type="button" class="btn btn-text" (click)="unlock()" sqxStopClick *ngIf="user.canUnlock" title="i18n:users.unlockTooltip">
<i class="icon icon-lock"></i> <i class="icon icon-lock"></i>
</button> </button>
</td> </td>

20
frontend/app/features/administration/pages/users/users-page.component.html

@ -1,27 +1,27 @@
<sqx-title message="User Management"></sqx-title> <sqx-title message="i18n:users.listPageTitle"></sqx-title>
<sqx-panel desiredWidth="50rem" grid="true"> <sqx-panel desiredWidth="50rem" grid="true">
<ng-container title> <ng-container title>
Users {{ 'users.listTitle' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<button type="button" class="btn btn-text-secondary mr-1" (click)="reload()" title="Refresh Users (CTRL + SHIFT + R)"> <button type="button" class="btn btn-text-secondary mr-1" (click)="reload()" title="i18n:users.refreshTooltip">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<form class="form-inline mr-1" (ngSubmit)="search()"> <form class="form-inline mr-1" (ngSubmit)="search()">
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> <input class="form-control" #inputFind [formControl]="usersFilter" placeholder="{{ 'users.search' | sqxTranslate }}">
</form> </form>
<ng-container *ngIf="usersState.canCreate | async"> <ng-container *ngIf="usersState.canCreate | async">
<sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut>
<button type="button" class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)"> <button type="button" class="btn btn-success" #buttonNew routerLink="new" title="i18n:users.createTooltip">
<i class="icon-plus"></i> New <i class="icon-plus"></i> {{ 'users.create' | sqxTranslate }}
</button> </button>
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -36,13 +36,13 @@
&nbsp; &nbsp;
</th> </th>
<th class="cell-auto"> <th class="cell-auto">
Name {{ 'common.name' | sqxTranslate }}
</th> </th>
<th class="cell-auto"> <th class="cell-auto">
Email {{ 'common.email' | sqxTranslate }}
</th> </th>
<th class="cell-actions"> <th class="cell-actions">
Actions {{ 'common.actions' | sqxTranslate }}
</th> </th>
</tr> </tr>
</thead> </thead>

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

@ -60,7 +60,7 @@ export class EventConsumersService {
return new EventConsumersDto(eventConsumers, _links); return new EventConsumersDto(eventConsumers, _links);
}), }),
pretifyError('Failed to load event consumers. Please reload.')); pretifyError('i18n:eventConsumers.loadFailed'));
} }
public putStart(eventConsumer: Resource): Observable<EventConsumerDto> { public putStart(eventConsumer: Resource): Observable<EventConsumerDto> {
@ -72,7 +72,7 @@ export class EventConsumersService {
map(body => { map(body => {
return parseEventConsumer(body); return parseEventConsumer(body);
}), }),
pretifyError('Failed to start event consumer. Please reload.')); pretifyError('i18n:eventConsumers.startFailed'));
} }
public putStop(eventConsumer: Resource): Observable<EventConsumerDto> { public putStop(eventConsumer: Resource): Observable<EventConsumerDto> {
@ -84,7 +84,7 @@ export class EventConsumersService {
map(body => { map(body => {
return parseEventConsumer(body); return parseEventConsumer(body);
}), }),
pretifyError('Failed to stop event consumer. Please reload.')); pretifyError('i18n:eventConsumers.stopFailed'));
} }
public putReset(eventConsumer: Resource): Observable<EventConsumerDto> { public putReset(eventConsumer: Resource): Observable<EventConsumerDto> {
@ -96,7 +96,7 @@ export class EventConsumersService {
map(body => { map(body => {
return parseEventConsumer(body); return parseEventConsumer(body);
}), }),
pretifyError('Failed to reset event consumer. Please reload.')); pretifyError('i18n:eventConsumers.resetFailed'));
} }
} }

12
frontend/app/features/administration/services/users.service.ts

@ -70,7 +70,7 @@ export class UsersService {
return new UsersDto(total, users, _links); return new UsersDto(total, users, _links);
}), }),
pretifyError('Failed to load users. Please reload.')); pretifyError('i18n:users.loadFailed'));
} }
public getUser(id: string): Observable<UserDto> { public getUser(id: string): Observable<UserDto> {
@ -80,7 +80,7 @@ export class UsersService {
map(body => { map(body => {
return parseUser(body); return parseUser(body);
}), }),
pretifyError('Failed to load user. Please reload.')); pretifyError('i18n:users.loadUserFailed'));
} }
public postUser(dto: CreateUserDto): Observable<UserDto> { public postUser(dto: CreateUserDto): Observable<UserDto> {
@ -90,7 +90,7 @@ export class UsersService {
map(body => { map(body => {
return parseUser(body); return parseUser(body);
}), }),
pretifyError('Failed to create user. Please reload.')); pretifyError('i18n:users.createFailed'));
} }
public putUser(user: Resource, dto: UpdateUserDto): Observable<UserDto> { public putUser(user: Resource, dto: UpdateUserDto): Observable<UserDto> {
@ -102,7 +102,7 @@ export class UsersService {
map(body => { map(body => {
return parseUser(body); return parseUser(body);
}), }),
pretifyError('Failed to update user. Please reload.')); pretifyError('i18n:users.updateFailed'));
} }
public lockUser(user: Resource): Observable<UserDto> { public lockUser(user: Resource): Observable<UserDto> {
@ -114,7 +114,7 @@ export class UsersService {
map(body => { map(body => {
return parseUser(body); return parseUser(body);
}), }),
pretifyError('Failed to load users. Please retry.')); pretifyError('i18n:users.loadFailed'));
} }
public unlockUser(user: Resource): Observable<UserDto> { public unlockUser(user: Resource): Observable<UserDto> {
@ -126,7 +126,7 @@ export class UsersService {
map(body => { map(body => {
return parseUser(body); return parseUser(body);
}), }),
pretifyError('Failed to load users. Please retry.')); pretifyError('i18n:users.loadFailed'));
} }
} }

2
frontend/app/features/administration/state/event-consumers.state.ts

@ -58,7 +58,7 @@ export class EventConsumersState extends State<Snapshot> {
return this.eventConsumersService.getEventConsumers().pipe( return this.eventConsumersService.getEventConsumers().pipe(
tap(({ items: eventConsumers }) => { tap(({ items: eventConsumers }) => {
if (isReload && !silent) { if (isReload && !silent) {
this.dialogs.notifyInfo('Event Consumers reloaded.'); this.dialogs.notifyInfo('i18n:eventConsumers.reloaded');
} }
this.next({ this.next({

2
frontend/app/features/administration/state/users.forms.ts

@ -34,7 +34,7 @@ export class UserForm extends Form<FormGroup, UpdateUserDto, UserDto> {
], ],
passwordConfirm: ['', passwordConfirm: ['',
[ [
ValidatorsEx.match('password', 'Passwords must be the same.') ValidatorsEx.match('password', 'i18n:users.passwordConfirmValidationMessage')
] ]
], ],
permissions: [''] permissions: ['']

2
frontend/app/features/administration/state/users.state.ts

@ -119,7 +119,7 @@ export class UsersState extends State<Snapshot> {
this.snapshot.usersQuery).pipe( this.snapshot.usersQuery).pipe(
tap(({ total, items: users, canCreate }) => { tap(({ total, items: users, canCreate }) => {
if (isReload) { if (isReload) {
this.dialogs.notifyInfo('Users reloaded.'); this.dialogs.notifyInfo('i18n:users.reloaded');
} }
this.next(s => { this.next(s => {

10
frontend/app/features/api/api-area.component.html

@ -1,26 +1,26 @@
<sqx-title message="API"></sqx-title> <sqx-title message="i18n:api.pageTitle"></sqx-title>
<sqx-panel theme="dark" desiredWidth="12rem"> <sqx-panel theme="dark" desiredWidth="12rem">
<ng-container title> <ng-container title>
API {{ 'api.title' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<ul class="nav nav-panel nav-dark flex-column"> <ul class="nav nav-panel nav-dark flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> <a class="nav-link" routerLink="graphql" routerLinkActive="active">
GraphQL {{ 'api.graphql' | sqxTranslate }}
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/api/content/{{appsState.appName}}/docs" sqxExternalLink> <a class="nav-link" href="/api/content/{{appsState.appName}}/docs" sqxExternalLink>
Content API {{ 'api.contentApi' | sqxTranslate }}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/api/docs" sqxExternalLink> <a class="nav-link" href="/api/docs" sqxExternalLink>
General API {{ 'api.generalApi' | sqxTranslate }}
</a> </a>
</li> </li>
</ul> </ul>

2
frontend/app/features/api/pages/graphql/graphql-page.component.html

@ -1,4 +1,4 @@
<sqx-title message="GraphQL"></sqx-title> <sqx-title message="i18n:api.graphqlPageTitle"></sqx-title>
<sqx-panel desiredWidth="*" minWidth="50rem" isFullSize="true"> <sqx-panel desiredWidth="*" minWidth="50rem" isFullSize="true">
<div inner #graphiQLContainer></div> <div inner #graphiQLContainer></div>

66
frontend/app/features/apps/pages/apps-page.component.html

@ -1,17 +1,17 @@
<sqx-title message="Apps"></sqx-title> <sqx-title message="i18n:apps.listPageTitle"></sqx-title>
<div class="apps-section"> <div class="apps-section">
<h1 class="apps-title">Hi {{authState.user?.displayName}}</h1> <h1 class="apps-title">{{ 'apps.welcomeTitle' | sqxTranslate: { user: authState.user?.displayName } }}</h1>
<div class="subtext"> <div class="subtext">
Welcome to Squidex. {{ 'apps.welcomeSubtitle' | sqxTranslate }}
</div> </div>
</div> </div>
<ng-container *ngIf="appsState.apps | async; let apps"> <ng-container *ngIf="appsState.apps | async; let apps">
<div class="apps-section"> <div class="apps-section">
<div class="empty" *ngIf="apps.length === 0"> <div class="empty" *ngIf="apps.length === 0">
<h3 class="empty-headline">You are not collaborating to any app yet</h3> <h3 class="empty-headline">{{ 'apps.empty' | sqxTranslate }}</h3>
</div> </div>
<div class="card card-href card-app" *ngFor="let app of apps; trackBy: trackByApp" [routerLink]="['/app', app.name]"> <div class="card card-href card-app" *ngFor="let app of apps; trackBy: trackByApp" [routerLink]="['/app', app.name]">
@ -24,13 +24,13 @@
<h3 class="card-title">{{app.displayName}}</h3> <h3 class="card-title">{{app.displayName}}</h3>
<div class="card-text card-links"> <div class="card-text card-links">
<a [routerLink]="['/app', app.name]" sqxStopClick>Edit</a> <a [routerLink]="['/app', app.name]" sqxStopClick>{{ 'common.edit' | sqxTranslate }}</a>
<span class="deeplinks"> <span class="deeplinks">
&nbsp;| &nbsp;|
<a [routerLink]="['/app', app.name, 'content']" sqxStopClick>Content</a> &middot; <a [routerLink]="['/app', app.name, 'content']" sqxStopClick>{{ 'common.content' | sqxTranslate }}</a> &middot;
<a [routerLink]="['/app', app.name, 'assets']" sqxStopClick>Assets</a> &middot; <a [routerLink]="['/app', app.name, 'assets']" sqxStopClick>{{ 'common.assets' | sqxTranslate }}</a> &middot;
<a [routerLink]="['/app', app.name, 'settings']" sqxStopClick>Settings</a> <a [routerLink]="['/app', app.name, 'settings']" sqxStopClick>{{ 'common.settings' | sqxTranslate }}</a>
</span> </span>
</div> </div>
@ -48,13 +48,13 @@
<div class="card card-template card-href" (click)="createNewApp('')"> <div class="card card-template card-href" (click)="createNewApp('')">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/add-app.svg" /> <img src="./images/add-app.svg">
</div> </div>
<h3 class="card-title">New App</h3> <h3 class="card-title">{{ 'apps.createBlankApp' | sqxTranslate }}</h3>
<div class="card-text"> <div class="card-text">
Create a new blank app without content and schemas. {{ 'apps.createBlankAppDescription' | sqxTranslate }}
</div> </div>
</div> </div>
</div> </div>
@ -62,15 +62,15 @@
<div class="card card-template card-href" (click)="createNewApp('Blog')"> <div class="card card-template card-href" (click)="createNewApp('Blog')">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/add-blog.svg" /> <img src="./images/add-blog.svg">
</div> </div>
<h3 class="card-title">New Blog Sample</h3> <h3 class="card-title">{{ 'apps.createBlogApp' | sqxTranslate }}</h3>
<div class="card-text"> <div class="card-text">
<div>Start with our ready to use blog.</div> <div>{{ 'apps.createBlogAppDescription' | sqxTranslate }}</div>
<div> <div>
Sample Code at <a href="https://github.com/Squidex/squidex-samples" sqxStopClick sqxExternalLink>Github</a> {{ 'common.sampleCodeLabel' | sqxTranslate }} <a href="https://github.com/Squidex/squidex-samples" sqxStopClick sqxExternalLink>{{ 'common.github' | sqxTranslate }}</a>
</div> </div>
</div> </div>
</div> </div>
@ -79,15 +79,15 @@
<div class="card card-template card-href" (click)="createNewApp('Identity')"> <div class="card card-template card-href" (click)="createNewApp('Identity')">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/add-identity.svg" /> <img src="./images/add-identity.svg">
</div> </div>
<h3 class="card-title">New Identity App</h3> <h3 class="card-title">{{ 'apps.createIdentityApp' | sqxTranslate }}</h3>
<div class="card-text"> <div class="card-text">
<div>Create app for Squidex Identity.</div> <div>{{ 'apps.createIdentityAppDescription' | sqxTranslate }}</div>
<div> <div>
<a href="https://github.com/Squidex/squidex-identity" sqxStopClick sqxExternalLink>Project</a> <a href="https://github.com/Squidex/squidex-identity" sqxStopClick sqxExternalLink>{{ 'common.project' | sqxTranslate }}</a>
</div> </div>
</div> </div>
</div> </div>
@ -96,15 +96,15 @@
<div class="card card-template card-href" (click)="createNewApp('IdentityV2')"> <div class="card card-template card-href" (click)="createNewApp('IdentityV2')">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/add-identity.svg" /> <img src="./images/add-identity.svg">
</div> </div>
<h3 class="card-title">New Identity App V2</h3> <h3 class="card-title">{{ 'apps.createIdentityAppV2' | sqxTranslate }}</h3>
<div class="card-text"> <div class="card-text">
<div>Create app for Squidex Identity V2.</div> <div>{{ 'apps.createIdentityAppV2Description' | sqxTranslate }}</div>
<div> <div>
<a href="https://github.com/Squidex/squidex-identity" sqxStopClick sqxExternalLink>Project</a> <a href="https://github.com/Squidex/squidex-identity" sqxStopClick sqxExternalLink>{{ 'common.project' | sqxTranslate }}</a>
</div> </div>
</div> </div>
</div> </div>
@ -113,15 +113,15 @@
<div class="card card-template card-href" (click)="createNewApp('Profile')"> <div class="card card-template card-href" (click)="createNewApp('Profile')">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/add-profile.svg" /> <img src="./images/add-profile.svg">
</div> </div>
<h3 class="card-title">New Profile Sample</h3> <h3 class="card-title">{{ 'apps.createProfileApp' | sqxTranslate }}</h3>
<div class="card-text"> <div class="card-text">
<div>Create your profile page.</div> <div>{{ 'apps.createProfileAppDescription' | sqxTranslate }}</div>
<div> <div>
Sample Code at <a href="https://github.com/Squidex/squidex-samples" sqxStopClick sqxExternalLink>Github</a> {{ 'common.sampleCodeLabel' | sqxTranslate }} <a href="https://github.com/Squidex/squidex-samples" sqxStopClick sqxExternalLink>{{ 'common.github' | sqxTranslate }}</a>
</div> </div>
</div> </div>
</div> </div>
@ -133,19 +133,13 @@
</div> </div>
<ng-container *sqxModal="addAppDialog"> <ng-container *sqxModal="addAppDialog">
<sqx-app-form [template]="addAppTemplate" <sqx-app-form [template]="addAppTemplate" (complete)="addAppDialog.hide()"></sqx-app-form>
(complete)="addAppDialog.hide()">
</sqx-app-form>
</ng-container> </ng-container>
<ng-container *sqxModal="onboardingDialog"> <ng-container *sqxModal="onboardingDialog">
<sqx-onboarding-dialog <sqx-onboarding-dialog (close)="onboardingDialog.hide()"></sqx-onboarding-dialog>
(close)="onboardingDialog.hide()">
</sqx-onboarding-dialog>
</ng-container> </ng-container>
<ng-container *sqxModal="newsDialog"> <ng-container *sqxModal="newsDialog">
<sqx-news-dialog [features]="newsFeatures" <sqx-news-dialog [features]="newsFeatures" (close)="newsDialog.hide()"></sqx-news-dialog>
(close)="newsDialog.hide()">
</sqx-news-dialog>
</ng-container> </ng-container>

4
frontend/app/features/apps/pages/news-dialog.component.html

@ -1,11 +1,11 @@
<sqx-modal-dialog size="lg" (close)="close.emit()"> <sqx-modal-dialog size="lg" (close)="close.emit()">
<ng-container title> <ng-container title>
New Features {{ 'news.title' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="help"> <div class="help">
<h1>What's new?</h1> <h1>{{ 'news.headline' | sqxTranslate }}</h1>
<div *ngFor="let feature of features; trackBy: trackByFeature"> <div *ngFor="let feature of features; trackBy: trackByFeature">
<h4>{{feature.name}}</h4> <h4>{{feature.name}}</h4>

92
frontend/app/features/apps/pages/onboarding-dialog.component.html

@ -1,143 +1,105 @@
<sqx-modal-dialog [showHeader]="false"> <sqx-modal-dialog [showHeader]="false">
<ng-container content> <ng-container content>
<a class="header-right modal-close" (click)="close.emit()">Skip Tour</a> <a class="header-right modal-close" (click)="close.emit()">{{ 'tour.skip' | sqxTranslate }}</a>
<div class="onboarding-step" *ngIf="step === 0"> <div class="onboarding-step" *ngIf="step === 0">
<img @fade class="header-left" src="./images/logo-white-small.png" /> <img @fade class="header-left" src="./images/logo-white-small.png">
<div @slide class="onboarding-enter-leave"> <div @slide class="onboarding-enter-leave">
<h1>Welcome to <span class="header-focus">Squidex CMS</span></h1> <h1>{{ 'tour.welcome' | sqxTranslate }} <span class="header-focus">{{ 'tour.welcomeProduct' | sqxTranslate }}</span></h1>
<p> <div [innerHTML]="'tour.step0Text' | sqxTranslate | sqxMarkdown"></div>
You can start managing and distributing your content right away, but we we'd like to walk you through some basics first...
</p>
<p>
How's that?
</p>
<button (click)="next()" class="btn btn-success">Let's take a look around</button> <button (click)="next()" class="btn btn-success">{{ 'tour.step0Next' | sqxTranslate }}</button>
</div> </div>
</div> </div>
<div class="onboarding-step" *ngIf="step === 1"> <div class="onboarding-step" *ngIf="step === 1">
<h1 @fade class="header-left header-focus">Apps</h1> <h1 @fade class="header-left header-focus">{{ 'common.apps' | sqxTranslate }}</h1>
<div @slide> <div @slide>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="onboarding-text"> <div class="onboarding-text" [innerHTML]="'tour.step1Text' | sqxTranslate | sqxMarkdown"></div>
<p>
An App is the repository for your project, e.g. (blog, web shop or mobile app). You can assign contributors to your app to work together.
</p>
<p>
You can create an unlimited number of Apps in Squidex to manage multiple projects at the same time.
</p>
</div>
</div> </div>
<div class="col col-image"> <div class="col col-image">
<img src="./images/onboarding-step1.png" /> <img src="./images/onboarding-step1.png">
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<button (click)="next()" class="btn btn-success">Continue</button> <button (click)="next()" class="btn btn-success">{{ 'tour.step1Next' | sqxTranslate }}</button>
</div> </div>
</div> </div>
</div> </div>
<div class="onboarding-step" *ngIf="step === 2"> <div class="onboarding-step" *ngIf="step === 2">
<h1 @fade class="header-left header-focus">Schemas</h1> <h1 @fade class="header-left header-focus">{{ 'common.schemas' | sqxTranslate }}</h1>
<div @slide> <div @slide>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="onboarding-text"> <div class="onboarding-text" [innerHTML]="'tour.step2Text' | sqxTranslate | sqxMarkdown"></div>
<p>
Schemas define the structure of your content, the fields and the data types of a content item.
</p>
<p>
Before you can add content to your schema, make sure to hit the 'Publish' button at the top to make the schema available to your content editors.
</p>
</div>
</div> </div>
<div class="col col-image"> <div class="col col-image">
<img src="./images/onboarding-step2.png" /> <img src="./images/onboarding-step2.png">
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<button (click)="next()" class="btn btn-success">Keep going!</button> <button (click)="next()" class="btn btn-success">{{ 'tour.step2Next' | sqxTranslate }}</button>
</div> </div>
</div> </div>
</div> </div>
<div class="onboarding-step" *ngIf="step === 3"> <div class="onboarding-step" *ngIf="step === 3">
<h1 @fade class="header-left header-focus">Contents</h1> <h1 @fade class="header-left header-focus">{{ 'common.contents' | sqxTranslate }}</h1>
<div @slide> <div @slide>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="onboarding-text"> <div class="onboarding-text" [innerHTML]="'tour.step3Text' | sqxTranslate | sqxMarkdown"></div>
<p>
Content is the actual data in your app which is grouped by the schema.
</p>
<p>
Select a published schema first, then add content for this schema.
</p>
</div>
</div> </div>
<div class="col col-image"> <div class="col col-image">
<img src="./images/onboarding-step3.png" /> <img src="./images/onboarding-step3.png">
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<button (click)="next()" class="btn btn-success">Almost there!</button> <button (click)="next()" class="btn btn-success">{{ 'tour.step3Next' | sqxTranslate }}</button>
</div> </div>
</div> </div>
</div> </div>
<div class="onboarding-step" *ngIf="step === 4"> <div class="onboarding-step" *ngIf="step === 4">
<h1 @fade class="header-left header-focus">Assets</h1> <h1 @fade class="header-left header-focus">{{ 'common.assets' | sqxTranslate }}</h1>
<div @slide> <div @slide>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="onboarding-text"> <div class="onboarding-text" [innerHTML]="'tour.step4Text' | sqxTranslate | sqxMarkdown"></div>
<p>
The assets contains all files that can also be linked to your content. For example images, videos or documents.
</p>
<p>
You can upload the assets here and use them later or also upload them directly when you create a new content item with an asset field.
</p>
</div>
</div> </div>
<div class="col col-image"> <div class="col col-image">
<img src="./images/onboarding-step4.png" /> <img src="./images/onboarding-step4.png">
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<button (click)="next()" class="btn btn-success">Got It!</button> <button (click)="next()" class="btn btn-success">{{ 'tour.step4Next' | sqxTranslate }}</button>
</div> </div>
</div> </div>
</div> </div>
<div class="onboarding-step" *ngIf="step === 5"> <div class="onboarding-step" *ngIf="step === 5">
<img @fade class="header-left" src="./images/logo-white-small.png" /> <img @fade class="header-left" src="./images/logo-white-small.png">
<div @slide class="onboarding-enter-leave"> <div @slide class="onboarding-enter-leave">
<h1>Awesome, now you know the basics!</h1> <h1>{{ 'tour.step5Title' | sqxTranslate }}</h1>
<p> <div [innerHTML]="'tour.step5Text' | sqxTranslate | sqxMarkdown"></div>
But that's not all of the support we can provide. <br />You can go to <a href="https://docs.squidex.io/" sqxExternalLink>https://docs.squidex.io/></a> to read more.
</p>
<p>
Do you want to join our community?
</p>
<div> <div>
<a class="btn btn-success" href="https://support.squidex.io" sqxExternalLink> <a class="btn btn-success" href="https://support.squidex.io" sqxExternalLink>
Join our Forum {{ 'tour.joinForum' | sqxTranslate }}
</a> &nbsp; </a> &nbsp;
<a class="btn btn-success" href="https://github.com/squidex/squidex" sqxExternalLink> <a class="btn btn-success" href="https://github.com/squidex/squidex" sqxExternalLink>
Join us on Github {{ 'tour.joinGithub' | sqxTranslate }}
</a> </a>
</div> </div>
</div> </div>

2
frontend/app/features/assets/pages/asset-tags.component.html

@ -1,7 +1,7 @@
<a class="sidebar-item" (click)="reset.emit()" [class.active]="isEmpty()"> <a class="sidebar-item" (click)="reset.emit()" [class.active]="isEmpty()">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
All tags {{ 'common.tagsAll' | sqxTranslate }}
</div> </div>
</div> </div>
</a> </a>

12
frontend/app/features/assets/pages/assets-filters-page.component.html

@ -1,21 +1,21 @@
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
Filters {{ 'common.filters' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<h3>Tags</h3> <h3>{{ 'common.tags' | sqxTranslate }}</h3>
<sqx-asset-tags <sqx-asset-tags (reset)="resetTags()"
(reset)="resetTags()"
[tags]="assetsState.tags | async" [tags]="assetsState.tags | async"
[tagsSelected]="assetsState.tagsSelected | async" [tagsSelected]="assetsState.tagsSelected | async"
(toggle)="toggleTag($event)"> (toggle)="toggleTag($event)">
</sqx-asset-tags> </sqx-asset-tags>
<hr /> <hr>
<sqx-shared-queries types="contents" <sqx-shared-queries
[types]="'common.assets' | sqxTranslate"
[queryUsed]="assetsState.assetsQuery | async" [queryUsed]="assetsState.assetsQuery | async"
[queries]="assetsQueries" [queries]="assetsQueries"
(search)="search($event)"> (search)="search($event)">

24
frontend/app/features/assets/pages/assets-page.component.html

@ -1,8 +1,8 @@
<sqx-title message="Assets"></sqx-title> <sqx-title message="i18n:assets.listPageTitle"></sqx-title>
<sqx-panel desiredWidth="*" minWidth="50rem" showSidebar="true" grid="true"> <sqx-panel desiredWidth="*" minWidth="50rem" showSidebar="true" grid="true">
<ng-container title> <ng-container title>
Assets {{ 'common.assets' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
@ -10,14 +10,14 @@
<div class="col-auto offset-xl-2"> <div class="col-auto offset-xl-2">
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)"> <button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:assets.refreshTooltip">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
</button> </button>
</div> </div>
<div class="col pl-1" style="width: 300px"> <div class="col pl-1" style="width: 300px">
<div class="row no-gutters search"> <div class="row no-gutters search">
<div class="col-6"> <div class="col-6">
<sqx-tag-editor class="tags" singleLine="true" placeholder="Search by tags" <sqx-tag-editor class="tags" singleLine="true" placeholder="{{ 'assets.searchByTags' | sqxTranslate }}"
[suggestions]="assetsState.tagsNames | async" [suggestions]="assetsState.tagsNames | async"
[ngModel]="assetsState.selectedTagNames | async" [ngModel]="assetsState.selectedTagNames | async"
(ngModelChange)="selectTags($event)" (ngModelChange)="selectTags($event)"
@ -25,7 +25,7 @@
</sqx-tag-editor> </sqx-tag-editor>
</div> </div>
<div class="col-6"> <div class="col-6">
<sqx-search-form formClass="form" placeholder="Search by name" fieldExample="fileSize" <sqx-search-form formClass="form" placeholder="{{ 'assets.searchByName' | sqxTranslate }}" fieldExample="fileSize"
[query]="assetsState.assetsQuery | async" [query]="assetsState.assetsQuery | async"
[queries]="queries" [queries]="queries"
(queryChange)="search($event)" (queryChange)="search($event)"
@ -47,7 +47,7 @@
<div class="col-auto pl-1"> <div class="col-auto pl-1">
<sqx-shortcut keys="ctrl+shift+g" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+g" (trigger)="reload()"></sqx-shortcut>
<button type="button" class="btn btn-success" (click)="addAssetFolderDialog.show()" title="Create new folder (CTRL + SHIFT + G)"> <button type="button" class="btn btn-success" (click)="addAssetFolderDialog.show()" title="i18n:assets.createFolderTooltip">
<i class="icon-create_new_folder"></i> <i class="icon-create_new_folder"></i>
</button> </button>
</div> </div>
@ -57,9 +57,7 @@
<ng-container content> <ng-container content>
<sqx-list-view [isLoading]="assetsState.isLoading | async"> <sqx-list-view [isLoading]="assetsState.isLoading | async">
<ng-container header> <ng-container header>
<sqx-asset-path [path]="assetsState.path | async" <sqx-asset-path [path]="assetsState.path | async" (navigate)="assetsState.navigate($event)"></sqx-asset-path>
(navigate)="assetsState.navigate($event)">
</sqx-asset-path>
</ng-container> </ng-container>
<div content> <div content>
@ -74,7 +72,7 @@
<ng-container sidebar> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" routerLink="filters" routerLinkActive="active" title="Filters" titlePosition="left"> <a class="panel-link" routerLink="filters" routerLinkActive="active" title="i18n:common.filters" titlePosition="left">
<i class="icon-filter"></i> <i class="icon-filter"></i>
</a> </a>
</div> </div>
@ -82,9 +80,7 @@
</sqx-panel> </sqx-panel>
<ng-container *sqxModal="addAssetFolderDialog"> <ng-container *sqxModal="addAssetFolderDialog">
<sqx-asset-folder-dialog <sqx-asset-folder-dialog (complete)="addAssetFolderDialog.hide()"></sqx-asset-folder-dialog>
(complete)="addAssetFolderDialog.hide()">
</sqx-asset-folder-dialog>
</ng-container> </ng-container>
<router-outlet></router-outlet> <router-outlet></router-outlet>

6
frontend/app/features/content/pages/content/content-event.component.html

@ -1,6 +1,6 @@
<div class="event row no-gutters"> <div class="event row no-gutters">
<div class="col-auto"> <div class="col-auto">
<img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [src]="event.actor | sqxUserPictureRef" /> <img class="user-picture" title="{{event.actor | sqxUserNameRef}}" [src]="event.actor | sqxUserPictureRef">
</div> </div>
<div class="col pl-2 event-right"> <div class="col pl-2 event-right">
<div class="event-message"> <div class="event-message">
@ -11,11 +11,11 @@
<div class="event-created">{{event.created | sqxFromNow}}</div> <div class="event-created">{{event.created | sqxFromNow}}</div>
<ng-container *ngIf="canLoadOrCompare"> <ng-container *ngIf="canLoadOrCompare">
<a class="event-load force" (click)="dataLoad.emit()">Load</a> <a class="event-load force" (click)="dataLoad.emit()">{{ 'contents.loadContent' | sqxTranslate }}</a>
&middot; &middot;
<a class="event-load force" (click)="dataCompare.emit()">Compare</a> <a class="event-load force" (click)="dataCompare.emit()">{{ 'contents.versionCompare' | sqxTranslate }}</a>
</ng-container> </ng-container>
</div> </div>
</div> </div>

4
frontend/app/features/content/pages/content/content-field.component.html

@ -3,7 +3,7 @@
<div class="table-items-row" [class.field-invalid]="isInvalid | async" *ngIf="!(formModel.hiddenChanges | async)"> <div class="table-items-row" [class.field-invalid]="isInvalid | async" *ngIf="!(formModel.hiddenChanges | async)">
<div class="languages-container"> <div class="languages-container">
<div class="languages-buttons"> <div class="languages-buttons">
<button *ngIf="canTranslate" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="translate()" title="Autotranslate from master language"> <button *ngIf="canTranslate" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="translate()" title="i18n:contents.autotranslate">
<i class="icon-translate"></i> <i class="icon-translate"></i>
</button> </button>
@ -53,7 +53,7 @@
<div class="table-items-row" *ngIf="!(formModelCompare!.hiddenChanges | async)"> <div class="table-items-row" *ngIf="!(formModelCompare!.hiddenChanges | async)">
<div class="languages-container"> <div class="languages-container">
<div class="languages-buttons-compare"> <div class="languages-buttons-compare">
<sqx-field-languages <sqx-field-languages
[field]="formModelCompare!.field" [field]="formModelCompare!.field"
(languageChange)="languageChange.emit($event)" (languageChange)="languageChange.emit($event)"
[language]="language" [language]="language"

54
frontend/app/features/content/pages/content/content-history-page.component.html

@ -1,21 +1,21 @@
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
Workflow {{ 'common.workflow' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="section mb-4" *ngIf="content.canDraftCreate || content.canDraftDelete"> <div class="section mb-4" *ngIf="content.canDraftCreate || content.canDraftDelete">
<ng-container *ngIf="!content.newStatus; else newVersion"> <ng-container *ngIf="!content.newStatus; else newVersion">
<button class="btn btn-success btn-block" (click)="createDraft() "> <button class="btn btn-success btn-block" (click)="createDraft() ">
New Draft {{ 'contents.draftNew' | sqxTranslate }}
</button> </button>
</ng-container> </ng-container>
<ng-template #newVersion> <ng-template #newVersion>
<label>New Version</label> <label>{{ 'contents.draftStatus' | sqxTranslate }}</label>
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" [class.active]="dropdownNew.isOpen | async" #buttonOptions> <button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" [class.active]="dropdownNew.isOpen | async" #buttonOptions>
<sqx-content-status <sqx-content-status
layout="multiline" layout="multiline"
[status]="content.newStatus" [status]="content.newStatus"
[statusColor]="content.newStatusColor" [statusColor]="content.newStatusColor"
@ -27,28 +27,26 @@
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade> <div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<ng-container *ngIf="content.statusUpdates.length > 0"> <ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)"> <a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
Change to <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}} {{ 'common.statusChangeTo' | sqxTranslate }} <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
</ng-container> </ng-container>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDraftDelete"
[class.disabled]="!content.canDraftDelete" (sqxConfirmClick)="deleteDraft()"
(sqxConfirmClick)="deleteDraft()" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmTitle="Delete content" confirmText="i18n:contents.deleteVersionConfirmText">
confirmText="Do you really want to delete this version?"> {{ 'contents.versionDelete' | sqxTranslate }}
Delete this Version
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
[class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="Delete content" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="Do you really want to delete the content?"> confirmText="i18n:contents.deleteConfirmText">
Delete {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>
</ng-container> </ng-container>
@ -56,7 +54,7 @@
</div> </div>
<div class="section"> <div class="section">
<label>Current Version</label> <label>{{ 'contents.currentStatusLabel' | sqxTranslate }}</label>
<div *ngIf="!content.newStatus; else newStatusOld"> <div *ngIf="!content.newStatus; else newStatusOld">
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions> <button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
@ -72,7 +70,7 @@
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade> <div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<ng-container *ngIf="content.statusUpdates.length > 0"> <ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)"> <a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
Change to {{ 'common.statusChangeTo' | sqxTranslate }}
<sqx-content-status small="true" <sqx-content-status small="true"
layout="text" layout="text"
@ -84,12 +82,11 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
</ng-container> </ng-container>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
[class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="Delete content" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="Do you really want to delete the content?"> confirmText="i18n:contents.deleteConfirmText">
Delete {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>
</ng-container> </ng-container>
@ -97,24 +94,21 @@
<ng-template #newStatusOld> <ng-template #newStatusOld>
<button type="button" class="btn btn-outline-secondary btn-block btn-status"> <button type="button" class="btn btn-outline-secondary btn-block btn-status">
<sqx-content-status <sqx-content-status [status]="content.status" [statusColor]="content.statusColor" layout="multiline">
[status]="content.status"
[statusColor]="content.statusColor"
layout="multiline">
</sqx-content-status> </sqx-content-status>
</button> </button>
</ng-template> </ng-template>
<sqx-form-hint marginTop="1"> <sqx-form-hint marginTop="1">
Last Updated: {{content.lastModified | sqxFromNow}} {{ 'contents.lastUpdatedLabel' | sqxTranslate }}: {{content.lastModified | sqxFromNow}}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
<div class="section"> <div class="section">
<h3 class="bordered">History</h3> <h3 class="bordered">{{ 'common.history' | sqxTranslate }}</h3>
<sqx-content-event *ngFor="let event of contentEvents | async; trackBy: trackByEvent" <sqx-content-event *ngFor="let event of contentEvents | async; trackBy: trackByEvent"
[content]="content" [content]="content"
[event]="event" [event]="event"
(dataLoad)="loadVersion(event)" (dataLoad)="loadVersion(event)"
(dataCompare)="compareVersion(event)"> (dataCompare)="compareVersion(event)">

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

@ -70,7 +70,7 @@ export class ContentHistoryPageComponent extends ResourceOwner implements OnInit
} }
public changeStatus(status: string) { public changeStatus(status: string) {
this.contentPage.checkPendingChanges('change the status').pipe( this.contentPage.checkPendingChangesBeforeChangingStatus().pipe(
filter(x => !!x), filter(x => !!x),
switchMap(_ => this.dueTimeSelector.selectDueTime(status)), switchMap(_ => this.dueTimeSelector.selectDueTime(status)),
switchMap(d => this.contentsState.changeStatus(this.content, status, d)), switchMap(d => this.contentsState.changeStatus(this.content, status, d)),

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

@ -8,14 +8,14 @@
</a> </a>
<ng-container *ngIf="content else noContentTitle"> <ng-container *ngIf="content else noContentTitle">
<sqx-title message="Edit Content"></sqx-title> <sqx-title message="i18n:contents.editPageTitle"></sqx-title>
Edit Content {{ 'contents.editTitle' | sqxTranslate }}
</ng-container> </ng-container>
<ng-template #noContentTitle> <ng-template #noContentTitle>
<sqx-title message="New Content"></sqx-title> <sqx-title message="i18n:contents.createPageTitle"></sqx-title>
New Content {{ 'contents.createTitle' | sqxTranslate }}
</ng-template> </ng-template>
</ng-container> </ng-container>
@ -35,9 +35,9 @@
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade> <div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="Delete content" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="Do you really want to delete the content?"> confirmText="i18n:contents.deleteConfirmText">
Delete {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>
</ng-container> </ng-container>
@ -45,8 +45,8 @@
</ng-container> </ng-container>
<ng-container *ngIf="content?.canUpdate"> <ng-container *ngIf="content?.canUpdate">
<button type="submit" class="btn btn-primary ml-1" title="CTRL + S"> <button type="submit" class="btn btn-primary ml-1" title="i18n:common.saveShortcut">
Save {{ 'common.save' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut> <sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
@ -55,11 +55,11 @@
<ng-template #noContent> <ng-template #noContent>
<button type="button" class="btn btn-secondary" (click)="save()" *ngIf="contentsState.canCreate | async"> <button type="button" class="btn btn-secondary" (click)="save()" *ngIf="contentsState.canCreate | async">
Save {{ 'common.save' | sqxTranslate }}
</button> </button>
<button type="submit" class="btn btn-primary ml-1" title="CTRL + S" *ngIf="contentsState.canCreateAndPublish | async"> <button type="submit" class="btn btn-primary ml-1" title="i18n:common.saveShortcut" *ngIf="contentsState.canCreateAndPublish | async">
Save and Publish {{ 'contents.saveAndPublish' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut> <sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
@ -73,10 +73,10 @@
<ng-container topHeader> <ng-container topHeader>
<div class="panel-alert panel-alert-danger" *ngIf="contentVersion"> <div class="panel-alert panel-alert-danger" *ngIf="contentVersion">
<div class="float-right"> <div class="float-right">
<a class="force" (click)="loadLatest()">View latest</a> <a class="force" (click)="loadLatest()">{{ 'contents.viewLatest' | sqxTranslate }}</a>
</div> </div>
Viewing <strong>version {{contentVersion}}</strong>. <div [innerHTML]="'contents.versionViewing' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline"></div>
</div> </div>
</ng-container> </ng-container>
@ -97,16 +97,16 @@
<ng-container sidebar> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" routerLink="history" routerLinkActive="active" title="Workflow" titlePosition="left" #linkHistory> <a class="panel-link" routerLink="history" routerLinkActive="active" title="i18n:common.workflow" titlePosition="left" #linkHistory>
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<a class="panel-link" routerLink="comments" routerLinkActive="active" title="Comments" titlePosition="left"> <a class="panel-link" routerLink="comments" routerLinkActive="active" title="i18n:common.comments" titlePosition="left">
<i class="icon-comments"></i> <i class="icon-comments"></i>
</a> </a>
<sqx-onboarding-tooltip helpId="history" [for]="linkHistory" position="left-top" after="120000"> <sqx-onboarding-tooltip helpId="history" [for]="linkHistory" position="left-top" after="120000">
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time. {{ 'common.sidebarTour' | sqxTranslate }}
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</div> </div>
</ng-container> </ng-container>

20
frontend/app/features/content/pages/content/content-page.component.ts

@ -98,7 +98,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
if (clone) { if (clone) {
this.loadContent(clone, true); this.loadContent(clone, true);
} else if (isNewContent && autosaved && this.contentForm.hasChanges(autosaved)) { } else if (isNewContent && autosaved && this.contentForm.hasChanges(autosaved)) {
this.dialogs.confirm('Unsaved changes', 'You have unsaved changes. Do you want to load them now?') this.dialogs.confirm('i18n:contents.unsavedChangesTitle', 'i18n:contents.unsavedChangesText')
.subscribe(shouldLoad => { .subscribe(shouldLoad => {
if (shouldLoad) { if (shouldLoad) {
this.loadContent(autosaved, false); this.loadContent(autosaved, false);
@ -120,7 +120,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
} }
public canDeactivate(): Observable<boolean> { public canDeactivate(): Observable<boolean> {
return this.checkPendingChanges('close the current content view').pipe( return this.checkPendingChangesBeforeClose().pipe(
tap(confirmed => { tap(confirmed => {
if (confirmed) { if (confirmed) {
this.autoSaveService.remove(this.autoSaveKey); this.autoSaveService.remove(this.autoSaveKey);
@ -167,7 +167,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
}); });
} }
} else { } else {
this.contentForm.submitFailed('Content element not valid, please check the field with the red bar on the left in all languages (if localizable).'); this.contentForm.submitFailed('i18n:contents.contentNotValid');
} }
} }
@ -194,13 +194,23 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
} }
} }
public checkPendingChanges(action: string) { public checkPendingChangesBeforeClose() {
if (this.content && !this.content.canUpdate) { if (this.content && !this.content.canUpdate) {
return of(true); return of(true);
} }
return this.contentForm.hasChanged() ? return this.contentForm.hasChanged() ?
this.dialogs.confirm('Unsaved changes', `You have unsaved changes.\n\nWhen you ${action} you will loose them.\n\n**Do you want to continue anyway?**`) : this.dialogs.confirm('i18n:contents.pendingChangesTitle', 'i18n:contents.pendingChangesTextToClose') :
of(true);
}
public checkPendingChangesBeforeChangingStatus() {
if (this.content && !this.content.canUpdate) {
return of(true);
}
return this.contentForm.hasChanged() ?
this.dialogs.confirm('i18n:contents.pendingChangesTitle', 'i18n:contents.pendingChangesTextToChange') :
of(true); of(true);
} }

6
frontend/app/features/content/pages/content/field-languages.component.html

@ -1,11 +1,11 @@
<ng-container *ngIf="field.isLocalizable && languages.length > 1"> <ng-container *ngIf="field.isLocalizable && languages.length > 1">
<button *ngIf="!field.properties.isComplexUI" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="toggleShowAllControls()"> <button *ngIf="!field.properties.isComplexUI" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="toggleShowAllControls()">
<ng-container *ngIf="showAllControls; else singleLanguage"> <ng-container *ngIf="showAllControls; else singleLanguage">
<span>Single Language</span> <span>{{ 'contents.languageModeSingle' | sqxTranslate }}</span>
</ng-container> </ng-container>
<ng-template #singleLanguage> <ng-template #singleLanguage>
<span>All Languages</span> <span>{{ 'contents.languageModeAll' | sqxTranslate }}</span>
</ng-template> </ng-template>
</button> </button>
@ -17,7 +17,7 @@
</sqx-language-selector> </sqx-language-selector>
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="top-right" after="120000"> <sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="top-right" after="120000">
Please remember to check all languages when you see validation errors. {{ 'contents.validationHint' | sqxTranslate }}
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>
</ng-container> </ng-container>

13
frontend/app/features/content/pages/contents/contents-filters-page.component.html

@ -1,30 +1,33 @@
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
Filters {{ 'common.filters' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<sqx-query-list <sqx-query-list
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.contentsQuery | async" [queryUsed]="contentsState.contentsQuery | async"
[queries]="schemaQueries.defaultQueries" [queries]="schemaQueries.defaultQueries"
(search)="search($event)"> (search)="search($event)">
</sqx-query-list> </sqx-query-list>
<hr /> <hr>
<div class="sidebar-section"> <div class="sidebar-section">
<h3>Status Queries</h3> <h3>{{ 'contents.statusQueries' | sqxTranslate }}</h3>
<sqx-query-list <sqx-query-list
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.contentsQuery | async" [queryUsed]="contentsState.contentsQuery | async"
[queries]="contentsState.statusQueries | async" [queries]="contentsState.statusQueries | async"
(search)="search($event)"> (search)="search($event)">
</sqx-query-list> </sqx-query-list>
</div> </div>
<hr /> <hr>
<sqx-shared-queries types="contents" <sqx-shared-queries
[types]="'common.contents' | sqxTranslate"
[queryUsed]="contentsState.contentsQuery | async" [queryUsed]="contentsState.contentsQuery | async"
[queries]="schemaQueries" [queries]="schemaQueries"
(search)="search($event)"> (search)="search($event)">

34
frontend/app/features/content/pages/contents/contents-page.component.html

@ -2,7 +2,7 @@
<sqx-panel desiredWidth="*" minWidth="50rem" showSidebar="true" grid="true"> <sqx-panel desiredWidth="*" minWidth="50rem" showSidebar="true" grid="true">
<ng-container title> <ng-container title>
Contents {{ 'common.contents' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
@ -12,26 +12,25 @@
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)"> <button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:contents.refreshTooltip">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
</button> </button>
</div> </div>
<div class="col pl-1"> <div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Fulltext search" <sqx-search-form formClass="form" placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}"
(queryChange)="search($event)" (queryChange)="search($event)"
[query]="contentsState.contentsQuery | async" [query]="contentsState.contentsQuery | async"
[queries]="queries" [queries]="queries"
[queryModel]="queryModel" [queryModel]="queryModel"
[language]="languageMaster" [language]="languageMaster" enableShortcut="true">
enableShortcut="true">
</sqx-search-form> </sqx-search-form>
</div> </div>
<div class="col-auto pl-1" *ngIf="languages.length > 1"> <div class="col-auto pl-1" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages"></sqx-language-selector> <sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div> </div>
<div class="col-auto pl-1"> <div class="col-auto pl-1">
<button type="button" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)" [disabled]="(contentsState.canCreateAny | async) === false"> <button type="button" class="btn btn-success" #newButton routerLink="new" title="i18n:contents.createContentTooltip" [disabled]="(contentsState.canCreateAny | async) === false">
<i class="icon-plus"></i> New <i class="icon-plus"></i> {{ 'contents.create' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()"></sqx-shortcut>
@ -44,7 +43,7 @@
<sqx-list-view [isLoading]="contentsState.isLoading | async" syncedHeader="true" table="true"> <sqx-list-view [isLoading]="contentsState.isLoading | async" syncedHeader="true" table="true">
<ng-container topHeader> <ng-container topHeader>
<div class="selection" *ngIf="selectionCount > 0"> <div class="selection" *ngIf="selectionCount > 0">
{{selectionCount}} items selected&nbsp;&nbsp; {{ 'contents.selectionCount' | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp;
<button type="button" class="btn btn-outline-secondary btn-status mr-1" *ngFor="let status of nextStatuses | sqxKeys" (click)="changeSelectedStatus(status)"> <button type="button" class="btn btn-outline-secondary btn-status mr-1" *ngFor="let status of nextStatuses | sqxKeys" (click)="changeSelectedStatus(status)">
<sqx-content-status layout="text" <sqx-content-status layout="text"
@ -55,9 +54,9 @@
<button type="button" class="btn btn-danger" *ngIf="selectionCanDelete" <button type="button" class="btn btn-danger" *ngIf="selectionCanDelete"
(sqxConfirmClick)="deleteSelected()" (sqxConfirmClick)="deleteSelected()"
confirmTitle="Delete content" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="Do you really want to delete the selected content items?"> confirmText="i18n:contents.deleteManyConfirmText">
Delete {{ 'common.delete' | sqxTranslate }}
</button> </button>
</div> </div>
@ -68,8 +67,7 @@
<ng-container *sqxModal="tableViewModal"> <ng-container *sqxModal="tableViewModal">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonSettings" @fade position="bottom-right"> <div class="dropdown-menu" [sqxAnchoredTo]="buttonSettings" @fade position="bottom-right">
<sqx-custom-view-editor <sqx-custom-view-editor [allFields]="tableView.allFields"
[allFields]="tableView.allFields"
(fieldNamesChange)="tableView.updateFields($event)" (fieldNamesChange)="tableView.updateFields($event)"
[fieldNames]="tableView.listFieldNames | async"> [fieldNames]="tableView.listFieldNames | async">
</sqx-custom-view-editor> </sqx-custom-view-editor>
@ -83,10 +81,10 @@
<thead> <thead>
<tr> <tr>
<th class="cell-select"> <th class="cell-select">
<input type="checkbox" class="form-check" [ngModel]="selectedAll" (ngModelChange)="selectAll($event)" /> <input type="checkbox" class="form-check" [ngModel]="selectedAll" (ngModelChange)="selectAll($event)">
</th> </th>
<th class="cell-actions cell-actions-left"> <th class="cell-actions cell-actions-left">
Actions {{ 'common.actions' | sqxTranslate }}
</th> </th>
<th *ngFor="let field of listFields" [sqxContentListCell]="field"> <th *ngFor="let field of listFields" [sqxContentListCell]="field">
<sqx-content-list-header <sqx-content-list-header
@ -104,7 +102,7 @@
<ng-container syncedContent> <ng-container syncedContent>
<div class="table-container"> <div class="table-container">
<table class="table table-items table-fixed" [style.minWidth]="listFields | sqxContentListWidth" [sqxSyncWidth]="header"> <table class="table table-items table-fixed" [style.minWidth]="listFields | sqxContentListWidth" [sqxSyncWidth]="header">
<tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent" <tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent"
[sqxContent]="content" [sqxContent]="content"
(delete)="delete(content)" (delete)="delete(content)"
[selected]="isItemSelected(content)" [selected]="isItemSelected(content)"
@ -129,7 +127,7 @@
<ng-container sidebar> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<a class="panel-link" routerLink="filters" routerLinkActive="active" title="Filters" titlePosition="left"> <a class="panel-link" routerLink="filters" routerLinkActive="active" title="i18n:common.filters" titlePosition="left">
<i class="icon-filter"></i> <i class="icon-filter"></i>
</a> </a>
</div> </div>

9
frontend/app/features/content/pages/contents/custom-view-editor.component.html

@ -1,17 +1,16 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<button type="button" class="btn btn-secondary btn-sm" (click)="resetDefault()"> <button type="button" class="btn btn-secondary btn-sm" (click)="resetDefault()">
Reset Default View {{ 'contents.viewReset' | sqxTranslate }}
</button> </button>
</div> </div>
<hr /> <hr>
<div <div
cdkDropList cdkDropList
[cdkDropListData]="fieldNames" [cdkDropListData]="fieldNames"
(cdkDropListDropped)="drop($event)"> (cdkDropListDropped)="drop($event)">
<div *ngFor="let field of fieldNames" cdkDrag> <div *ngFor="let field of fieldNames" cdkDrag>
<i class="icon-drag2 drag-handle"></i> <i class="icon-drag2 drag-handle"></i>
@ -24,7 +23,7 @@
</div> </div>
</div> </div>
<hr /> <hr>
<div> <div>
<div *ngFor="let field of fieldsNotAdded"> <div *ngFor="let field of fieldsNotAdded">

8
frontend/app/features/content/pages/schemas/schemas-page.component.html

@ -1,9 +1,9 @@
<sqx-title message="Contents"></sqx-title> <sqx-title message="i18n:contents.schemasPageTitle"></sqx-title>
<sqx-panel theme="dark" [desiredWidth]="width" [showClose]="!isCollapsed" showSecondHeader="true"> <sqx-panel theme="dark" [desiredWidth]="width" [showClose]="!isCollapsed" showSecondHeader="true">
<ng-container title> <ng-container title>
<ng-container *ngIf="!isCollapsed"> <ng-container *ngIf="!isCollapsed">
Schemas {{ 'common.schemas' | sqxTranslate }}
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -18,7 +18,7 @@
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<div class="search-form"> <div class="search-form">
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" /> <input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="{{ 'contents.searchSchemasPlaceholder' | sqxTranslate }}">
<i class="icon-search"></i> <i class="icon-search"></i>
</div> </div>
@ -37,7 +37,7 @@
</div> </div>
<div class="headline" [class.hidden]="!isCollapsed"> <div class="headline" [class.hidden]="!isCollapsed">
Schemas {{ 'common.schemas' | sqxTranslate }}
</div> </div>
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>

4
frontend/app/features/content/shared/content-status.component.html

@ -9,7 +9,7 @@
<div class="content-status-scheduled mt-2" *ngIf="scheduled"> <div class="content-status-scheduled mt-2" *ngIf="scheduled">
<div> <div>
<span class="label">to&nbsp;</span> <span class="label">{{ 'contents.scheduledTo' | sqxTranslate }}&nbsp;</span>
<span class="content-status default mr-1" [style.color]="scheduled?.color" title="{{tooltipText}}"> <span class="content-status default mr-1" [style.color]="scheduled?.color" title="{{tooltipText}}">
<i class="icon-circle icon-sm"></i> <i class="icon-circle icon-sm"></i>
@ -19,7 +19,7 @@
</div> </div>
<div class="truncate"> <div class="truncate">
<span class="label">at&nbsp;</span> <span class="label">{{ 'contents.scheduledAt' | sqxTranslate }}&nbsp;</span>
<span>{{scheduled?.dueTime | sqxFullDateTime}}</span> <span>{{scheduled?.dueTime | sqxFullDateTime}}</span>
</div> </div>

14
frontend/app/features/content/shared/due-time-selector.component.html

@ -1,21 +1,21 @@
<ng-container *sqxModal="dueTimeDialog"> <ng-container *sqxModal="dueTimeDialog">
<sqx-modal-dialog (close)="cancelStatusChange()"> <sqx-modal-dialog (close)="cancelStatusChange()">
<ng-container title> <ng-container title>
Change content item(s) to {{dueTimeAction}} {{ 'contents.changeStatusTo' | sqxTranslate: { action: dueTimeAction } }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode" /> <input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode">
<label class="form-check-label" for="immediately"> <label class="form-check-label" for="immediately">
Set to {{dueTimeAction}} immediately. {{ 'contents.changeStatusToImmediately' | sqxTranslate: { action: dueTimeAction } }}
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode" /> <input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode">
<label class="form-check-label" for="scheduled"> <label class="form-check-label" for="scheduled">
Set to {{dueTimeAction}} at a later point date and time. {{ 'contents.changeStatusToLater' | sqxTranslate: { action: dueTimeAction } }}
</label> </label>
</div> </div>
@ -23,8 +23,8 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="button" class="btn btn-secondary" (click)="cancelStatusChange()">Cancel</button> <button type="button" class="btn btn-secondary" (click)="cancelStatusChange()">{{ 'common.cancel' | sqxTranslate }}</button>
<button type="button" class="btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit>Confirm</button> <button type="button" class="btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit>{{ 'common.confirm' | sqxTranslate }}</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>
</ng-container> </ng-container>

15
frontend/app/features/content/shared/forms/array-editor.component.html

@ -1,12 +1,9 @@
<div class="array-container" *ngIf="formModel.items.length > 0" <div class="array-container" *ngIf="formModel.items.length > 0"
cdkDropList cdkDropList
[cdkDropListDisabled]="false" [cdkDropListDisabled]="false"
[cdkDropListData]="formModel.items" [cdkDropListData]="formModel.items"
(cdkDropListDropped)="sort($event)"> (cdkDropListDropped)="sort($event)">
<div *ngFor="let itemForm of formModel.items; index as i; last as isLast; first as isFirst" <div *ngFor="let itemForm of formModel.items; index as i; last as isLast; first as isFirst" class="table-drag item" cdkDrag cdkDragLockAxis="y">
class="table-drag item"
cdkDrag
cdkDragLockAxis="y">
<sqx-array-item <sqx-array-item
[canUnset]="canUnset" [canUnset]="canUnset"
[form]="form" [form]="form"
@ -27,20 +24,20 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<button type="button" class="btn btn-success" [disabled]="field.nested.length === 0 || formModel.form.disabled" (click)="itemAdd(undefined)"> <button type="button" class="btn btn-success" [disabled]="field.nested.length === 0 || formModel.form.disabled" (click)="itemAdd(undefined)">
Add Item {{ 'contents.arrayAddItem' | sqxTranslate }}
</button> </button>
</div> </div>
<div class="col-auto" *ngIf="formModel.items.length > 0"> <div class="col-auto" *ngIf="formModel.items.length > 0">
<button type="button" class="btn btn-text-secondary" (click)="expandAll()" title="Expand all items"> <button type="button" class="btn btn-text-secondary" (click)="expandAll()" title="i18n:contents.arrayExpandAll">
<i class="icon-plus-square"></i> <i class="icon-plus-square"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" (click)="collapseAll()" title="Collapse all items"> <button type="button" class="btn btn-text-secondary" (click)="collapseAll()" title="i18n:contents.arrayCollapseAll">
<i class="icon-minus-square"></i> <i class="icon-minus-square"></i>
</button> </button>
</div> </div>
</div> </div>
<small class="text-muted ml-2" *ngIf="field.nested.length === 0"> <small class="text-muted ml-2" *ngIf="field.nested.length === 0">
Add a nested field first to add items. {{ 'contents.arrayNoFields' | sqxTranslate }}
</small> </small>

14
frontend/app/features/content/shared/forms/array-item.component.html

@ -11,27 +11,27 @@
</div> </div>
</div> </div>
<div class="col-auto pr-4"> <div class="col-auto pr-4">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="moveTop()" title="Move this item to top"> <button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="moveTop()" title="i18n:contents.arrayMoveTop">
<i class="icon-caret-top"></i> <i class="icon-caret-top"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="moveUp()" title="Move this item up"> <button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isFirst" (click)="moveUp()" title="i18n:contents.arrayMoveUp">
<i class="icon-caret-up"></i> <i class="icon-caret-up"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveDown()" title="Move this item down"> <button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveDown()" title="i18n:contents.arrayMoveDown">
<i class="icon-caret-down"></i> <i class="icon-caret-down"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveBottom()" title="Move this item to bottom"> <button type="button" class="btn btn-text-secondary" [disabled]="isDisabled || isLast" (click)="moveBottom()" title="i18n:contents.arrayMoveBottom">
<i class="icon-caret-bottom"></i> <i class="icon-caret-bottom"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="!isCollapsed" (click)="expand()" title="Expand this item"> <button type="button" class="btn btn-text-secondary" [class.hidden]="!isCollapsed" (click)="expand()" title="i18n:contents.arrayExpandItem">
<i class="icon-plus-square"></i> <i class="icon-plus-square"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" [class.hidden]="isCollapsed" (click)="collapse()" title="Collapse this item"> <button type="button" class="btn btn-text-secondary" [class.hidden]="isCollapsed" (click)="collapse()" title="i18n:contents.arrayCollapseItem">
<i class="icon-minus-square"></i> <i class="icon-minus-square"></i>
</button> </button>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-text-secondary" [disabled]="isDisabled" (click)="clone.emit()" title="Clone this item"> <button type="button" class="btn btn-text-secondary" [disabled]="isDisabled" (click)="clone.emit()" title="i18n:contents.arrayCloneItem">
<i class="icon-clone"></i> <i class="icon-clone"></i>
</button> </button>

53
frontend/app/features/content/shared/forms/assets-editor.component.html

@ -1,14 +1,9 @@
<div class="assets-container" <div class="assets-container" (sqxDropFile)="addFiles($event)" (sqxDropDisabled)="snapshot.isDisabled" tabindex="1000">
(sqxDropFile)="addFiles($event)"
(sqxDropDisabled)="snapshot.isDisabled"
tabindex="1000">
<div class="header list"> <div class="header list">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col" [class.disabled]="snapshot.isDisabled"> <div class="col" [class.disabled]="snapshot.isDisabled">
<div class="drop-area align-items-center" (click)="assetsDialog.show()" <div class="drop-area align-items-center" (click)="assetsDialog.show()" (sqxDropFile)="addFiles($event)" (sqxDropDisabled)="snapshot.isDisabled">
(sqxDropFile)="addFiles($event)" {{ 'contents.assetsUpload' | sqxTranslate }}
(sqxDropDisabled)="snapshot.isDisabled">
Drop files or click
</div> </div>
</div> </div>
<div class="col-auto pl-1"> <div class="col-auto pl-1">
@ -24,55 +19,51 @@
</div> </div>
</div> </div>
<div class="body" <div class="body" (sqxResizeCondition)="setCompact($event)" [sqxResizeMinWidth]="600" [sqxResizeMaxWidth]="0">
(sqxResizeCondition)="setCompact($event)"
[sqxResizeMinWidth]="600"
[sqxResizeMaxWidth]="0">
<ng-container *ngIf="!snapshot.isListView; else listTemplate"> <ng-container *ngIf="!snapshot.isListView; else listTemplate">
<div class="row no-gutters"> <div class="row no-gutters">
<sqx-asset *ngFor="let file of snapshot.assetFiles" [assetFile]="file" <sqx-asset *ngFor="let file of snapshot.assetFiles"
[assetFile]="file"
[isDisabled]="snapshot.isDisabled" [isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact" [isCompact]="snapshot.isCompact"
(loadError)="removeLoadingAsset(file)" (loadError)="removeLoadingAsset(file)"
(load)="addAsset(file, $event)"> (load)="addAsset(file, $event)">
</sqx-asset> </sqx-asset>
<sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset"
<sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset"
[asset]="asset" [asset]="asset"
[isDisabled]="snapshot.isDisabled" [isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact" [isCompact]="snapshot.isCompact"
(update)="notifyOthers(asset)"
[removeMode]="true" [removeMode]="true"
(remove)="removeLoadedAsset(asset)"> (remove)="removeLoadedAsset(asset)"
(update)="notifyOthers(asset)">
</sqx-asset> </sqx-asset>
</div> </div>
</ng-container> </ng-container>
<ng-template #listTemplate> <ng-template #listTemplate>
<div class="list-view"> <div class="list-view">
<sqx-asset *ngFor="let file of snapshot.assetFiles" [assetFile]="file" <sqx-asset *ngFor="let file of snapshot.assetFiles"
[isListView]="true" (load)="addAsset(file, $event)"
[isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact"
(loadError)="removeLoadingAsset(file)" (loadError)="removeLoadingAsset(file)"
(load)="addAsset(file, $event)"> [assetFile]="file"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
[isListView]="true">
</sqx-asset> </sqx-asset>
<div <div cdkDropList
cdkDropList
[cdkDropListDisabled]="snapshot.isDisabled" [cdkDropListDisabled]="snapshot.isDisabled"
[cdkDropListData]="snapshot.assets" [cdkDropListData]="snapshot.assets"
(cdkDropListDropped)="sortAssets($event)"> (cdkDropListDropped)="sortAssets($event)">
<div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" <div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" class="table-drag" cdkDrag cdkDragLockAxis="y">
class="table-drag" <sqx-asset
cdkDrag
cdkDragLockAxis="y">
<sqx-asset
[asset]="asset" [asset]="asset"
[isListView]="true" [isListView]="true"
[isDisabled]="snapshot.isDisabled" [isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact" [isCompact]="snapshot.isCompact"
(update)="notifyOthers(asset)" (update)="notifyOthers(asset)"
[removeMode]="true" [removeMode]="true"
(remove)="removeLoadedAsset(asset)"> (remove)="removeLoadedAsset(asset)">
</sqx-asset> </sqx-asset>
</div> </div>
@ -83,7 +74,5 @@
</div> </div>
<ng-container *sqxModal="assetsDialog"> <ng-container *sqxModal="assetsDialog">
<sqx-assets-selector <sqx-assets-selector (select)="selectAssets($event)"></sqx-assets-selector>
(select)="selectAssets($event)">
</sqx-assets-selector>
</ng-container> </ng-container>

15
frontend/app/features/content/shared/forms/stock-photo-editor.component.html

@ -1,36 +1,33 @@
<div class="row no-gutters" <div class="row no-gutters" (sqxResizeCondition)="setCompact($event)" [sqxResizeMinWidth]="600" [sqxResizeMaxWidth]="0">
(sqxResizeCondition)="setCompact($event)"
[sqxResizeMinWidth]="600"
[sqxResizeMaxWidth]="0">
<div class="col-auto col-image" [class.expand]="snapshot.isCompact"> <div class="col-auto col-image" [class.expand]="snapshot.isCompact">
<input class="form-control value" [formControl]="valueControl" readonly /> <input class="form-control value" [formControl]="valueControl" readonly>
<button type="button" class="btn btn-text-secondary value-clear" (click)="reset()"> <button type="button" class="btn btn-text-secondary value-clear" (click)="reset()">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
<div *ngIf="stockPhotoThumbnail | async; let url; else noThumb" class="preview"> <div *ngIf="stockPhotoThumbnail | async; let url; else noThumb" class="preview">
<img [src]="url" /> <img [src]="url">
</div> </div>
<ng-template #noThumb> <ng-template #noThumb>
<div class="preview preview-empty"> <div class="preview preview-empty">
Nothing selected {{ 'contents.stockPhotoEmpty' | sqxTranslate }}
</div> </div>
</ng-template> </ng-template>
</div> </div>
<div class="col pl-4" *ngIf="!snapshot.isCompact"> <div class="col pl-4" *ngIf="!snapshot.isCompact">
<i class="icon-angle-left icon"></i> <i class="icon-angle-left icon"></i>
<input class="form-control" [formControl]="stockPhotoSearch" placeholder="Search for Photos by Unsplash" /> <input class="form-control" [formControl]="stockPhotoSearch" placeholder="{{ 'contents.stockPhotoSearch' | sqxTranslate }}">
<sqx-list-view [isLoading]="snapshot.isLoading" table="true"> <sqx-list-view [isLoading]="snapshot.isLoading" table="true">
<div content> <div content>
<div class="photos"> <div class="photos">
<ng-container *ngIf="stockPhotos | async; let photos"> <ng-container *ngIf="stockPhotos | async; let photos">
<div *ngFor="let photo of photos" class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)"> <div *ngFor="let photo of photos" class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)">
<img [src]="photo.thumbUrl" /> <img [src]="photo.thumbUrl">
<div class="photo-user"> <div class="photo-user">
<a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick> <a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick>

10
frontend/app/features/content/shared/list/content-list-field.component.html

@ -6,7 +6,7 @@
<small class="truncate">{{content.created | sqxFromNow}}</small> <small class="truncate">{{content.created | sqxFromNow}}</small>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.createdByAvatar"> <ng-container *ngSwitchCase="metaFields.createdByAvatar">
<img class="user-picture" title="{{content.createdBy | sqxUserNameRef}}" [src]="content.createdBy | sqxUserPictureRef" /> <img class="user-picture" title="{{content.createdBy | sqxUserNameRef}}" [src]="content.createdBy | sqxUserPictureRef">
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.createdByName"> <ng-container *ngSwitchCase="metaFields.createdByName">
<small class="truncate">{{content.createdBy | sqxUserNameRef}}</small> <small class="truncate">{{content.createdBy | sqxUserNameRef}}</small>
@ -15,7 +15,7 @@
<small class="truncate">{{content.lastModified | sqxFromNow}}</small> <small class="truncate">{{content.lastModified | sqxFromNow}}</small>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar"> <ng-container *ngSwitchCase="metaFields.lastModifiedByAvatar">
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [src]="content.lastModifiedBy | sqxUserPictureRef" /> <img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [src]="content.lastModifiedBy | sqxUserPictureRef">
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.lastModifiedByName"> <ng-container *ngSwitchCase="metaFields.lastModifiedByName">
<small class="truncate">{{content.lastModifiedBy | sqxUserNameRef}}</small> <small class="truncate">{{content.lastModifiedBy | sqxUserNameRef}}</small>
@ -34,7 +34,7 @@
<i class="icon-caret-right"></i> <i class="icon-caret-right"></i>
</div> </div>
<div class="col"> <div class="col">
<sqx-content-status truncate="true" <sqx-content-status truncate="true"
layout="text" layout="text"
[status]="content.newStatus" [status]="content.newStatus"
[statusColor]="content.newStatusColor" [statusColor]="content.newStatusColor"
@ -45,7 +45,7 @@
</ng-container> </ng-container>
<ng-template #singleStatus> <ng-template #singleStatus>
<sqx-content-status truncate="true" <sqx-content-status truncate="true"
layout="text" layout="text"
[status]="content.status" [status]="content.status"
[statusColor]="content.statusColor" [statusColor]="content.statusColor"
@ -61,7 +61,7 @@
[statusColor]="content.scheduleJob?.color"> [statusColor]="content.scheduleJob?.color">
</sqx-content-status> </sqx-content-status>
at {{content.scheduleJob?.dueTime | sqxShortDate}} {{ 'contents.scheduledAtLabel' | sqxTranslate }}&nbsp;{{content.scheduleJob?.dueTime | sqxShortDate}}
</span> </span>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.statusColor"> <ng-container *ngSwitchCase="metaFields.statusColor">

17
frontend/app/features/content/shared/list/content.component.html

@ -1,8 +1,6 @@
<tr [sqxTabRouterLink]="link"> <tr [sqxTabRouterLink]="link">
<td class="cell-select inline-edit" sqxStopClick> <td class="cell-select inline-edit" sqxStopClick>
<input type="checkbox" class="form-check" <input type="checkbox" class="form-check" [ngModel]="selected" (ngModelChange)="selectedChange.emit($event)">
[ngModel]="selected"
(ngModelChange)="selectedChange.emit($event)" />
<ng-container *ngIf="isDirty"> <ng-container *ngIf="isDirty">
<div class="edit-menu"> <div class="edit-menu">
@ -26,24 +24,25 @@
<ng-container *sqxModal="dropdown;closeAlways:true"> <ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" position="bottom-left" @fade> <div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" position="bottom-left" @fade>
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="statusChange.emit(info.status)"> <a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="statusChange.emit(info.status)">
Change to {{ 'common.statusChangeTo' | sqxTranslate }}
<sqx-content-status layout="text" small="true" <sqx-content-status small="true"
layout="text"
[status]="info.status" [status]="info.status"
[statusColor]="info.color"> [statusColor]="info.color">
</sqx-content-status> </sqx-content-status>
</a> </a>
<a class="dropdown-item" (click)="clone.emit(); dropdown.hide()" *ngIf="canClone"> <a class="dropdown-item" (click)="clone.emit(); dropdown.hide()" *ngIf="canClone">
Clone {{ 'common.clone' | sqxTranslate }}
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete.emit()" (sqxConfirmClick)="delete.emit()"
confirmTitle="Delete content" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="Do you really want to delete the content?"> confirmText="i18n:contents.deleteConfirmText">
Delete {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>
</ng-container> </ng-container>

2
frontend/app/features/content/shared/preview-button.component.html

@ -1,5 +1,5 @@
<ng-container *ngIf="snapshot.previewNameSelected"> <ng-container *ngIf="snapshot.previewNameSelected">
<span>Preview: </span> <span>{{ 'common.preview' | sqxTranslate }}: </span>
<div class="btn-group ml-1" #buttonGroup> <div class="btn-group ml-1" #buttonGroup>
<button type="button" class="btn btn-secondary" (click)="follow(snapshot.previewNameSelected)"> <button type="button" class="btn btn-secondary" (click)="follow(snapshot.previewNameSelected)">

14
frontend/app/features/content/shared/references/content-creator.component.html

@ -2,11 +2,9 @@
<ng-container title> <ng-container title>
<div class="row"> <div class="row">
<div class="col-selector"> <div class="col-selector">
<select class="form-control form-control-dark" <select class="form-control form-control-dark" [ngModel]="schema?.id" (ngModelChange)="selectSchema($event)">
[ngModel]="schema?.id"
(ngModelChange)="selectSchema($event)">
<option *ngFor="let schema of schemas" [ngValue]="schema.id"> <option *ngFor="let schema of schemas" [ngValue]="schema.id">
Select {{schema.displayName}} {{ 'contents.referencesSelectSchema' | sqxTranslate: { schema: schema.displayName } }}
</option> </option>
</select> </select>
</div> </div>
@ -17,19 +15,17 @@
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-auto"> <div class="col-auto">
<div *ngIf="schema && languages.length > 1"> <div *ngIf="schema && languages.length > 1">
<sqx-language-selector class="languages-buttons" <sqx-language-selector class="languages-buttons"(selectedLanguageChange)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
(selectedLanguageChange)="selectLanguage($event)" [languages]="languages">
</sqx-language-selector>
</div> </div>
</div> </div>
<div class="col text-right"> <div class="col text-right">
<button type="button" class="btn btn-outline-success" (click)="save()"> <button type="button" class="btn btn-outline-success" (click)="save()">
Create {{ 'common.create' | sqxTranslate }}
</button> </button>
<button type="button" class="btn btn-success ml-1" (click)="saveAndPublish()" *ngIf="schema?.canContentsCreateAndPublish"> <button type="button" class="btn btn-success ml-1" (click)="saveAndPublish()" *ngIf="schema?.canContentsCreateAndPublish">
Create and Publish {{ 'contents.referencesCreatePublish' | sqxTranslate }}
</button> </button>
<sqx-form-error bubble="true" closeable="true" [error]="contentForm?.error | async"></sqx-form-error> <sqx-form-error bubble="true" closeable="true" [error]="contentForm?.error | async"></sqx-form-error>

2
frontend/app/features/content/shared/references/content-creator.component.ts

@ -100,7 +100,7 @@ export class ContentCreatorComponent extends ResourceOwner implements OnInit {
this.contentForm.submitFailed(error); this.contentForm.submitFailed(error);
}); });
} else { } else {
this.contentForm.submitFailed('Content element not valid, please check the field with the red bar on the left in all languages (if localizable).'); this.contentForm.submitFailed('i18n:contents.contentNotValid');
} }
} }

21
frontend/app/features/content/shared/references/content-selector.component.html

@ -1,12 +1,10 @@
<sqx-modal-dialog (close)="emitComplete()" size="lg" fullHeight="true" grid="true" flexBody="true"> <sqx-modal-dialog (close)="emitComplete()" size="lg" fullHeight="true" grid="true" flexBody="true">
<ng-container title> <ng-container title>
<div class="row"> <div class="row">
<div class="col-selector"> <div class="col-selector">
<select class="form-control form-control-dark" <select class="form-control form-control-dark" [ngModel]="schema?.id" (ngModelChange)="selectSchema($event)">
[ngModel]="schema?.id"
(ngModelChange)="selectSchema($event)">
<option *ngFor="let schema of schemas" [ngValue]="schema.id"> <option *ngFor="let schema of schemas" [ngValue]="schema.id">
Select {{schema.displayName}} {{ 'contents.referencesSelectSchema' | sqxTranslate: { schema: schema.displayName } }}
</option> </option>
</select> </select>
</div> </div>
@ -22,7 +20,7 @@
</button> </button>
</div> </div>
<div class="col pl-1"> <div class="col pl-1">
<sqx-search-form formClass="form" placeholder="Fulltext search" <sqx-search-form formClass="form" placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}"
[query]="contentsState.contentsQuery | async" [query]="contentsState.contentsQuery | async"
[queryModel]="queryModel" [queryModel]="queryModel"
(queryChange)="search($event)"> (queryChange)="search($event)">
@ -46,7 +44,7 @@
<thead> <thead>
<tr> <tr>
<th class="cell-select"> <th class="cell-select">
<input type="checkbox" class="form-check" [ngModel]="selectedAll" (ngModelChange)="selectAll($event)" /> <input type="checkbox" class="form-check" [ngModel]="selectedAll" (ngModelChange)="selectAll($event)">
</th> </th>
<th sqxContentListCell="meta.lastModifiedBy.avatar"> <th sqxContentListCell="meta.lastModifiedBy.avatar">
<sqx-content-list-header field="meta.lastModifiedBy.avatar"></sqx-content-list-header> <sqx-content-list-header field="meta.lastModifiedBy.avatar"></sqx-content-list-header>
@ -87,7 +85,12 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="button" class="btn btn-secondary" (click)="emitComplete()">Cancel</button> <button type="button" class="btn btn-secondary" (click)="emitComplete()">
<button type="submit" class="btn btn-success" (click)="emitSelect()" [disabled]="selectionCount === 0">Link selected contents ({{selectionCount}})</button> {{ 'common.cancel' | sqxTranslate }}
</button>
<button type="submit" class="btn btn-success" (click)="emitSelect()" [disabled]="selectionCount === 0">
{{ 'contents.referencesLink' | sqxTranslate: { count: selectionCount} }})
</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

12
frontend/app/features/content/shared/references/references-editor.component.html

@ -5,15 +5,15 @@
<ng-container> <ng-container>
<div class="drop-area-container"> <div class="drop-area-container">
<div class="drop-area"> <div class="drop-area">
<a (click)="contentCreatorDialog.show()">Add New</a> <a (click)="contentCreatorDialog.show()">{{ 'contents.referencesCreateNew' | sqxTranslate }}</a>
&middot; &middot;
<a (click)="contentSelectorDialog.show()">Select Existing</a> <a (click)="contentSelectorDialog.show()">{{ 'contents.referencesSelectExisting' | sqxTranslate }}</a>
</div> </div>
</div> </div>
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.contentItems && snapshot.contentItems.length > 0" <table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.contentItems && snapshot.contentItems.length > 0"
cdkDropList cdkDropList
[cdkDropListData]="snapshot.contentItems" [cdkDropListData]="snapshot.contentItems"
[cdkDropListDisabled]="snapshot.isDisabled" [cdkDropListDisabled]="snapshot.isDisabled"
@ -22,7 +22,7 @@
[sqxReferenceItem]="content" [sqxReferenceItem]="content"
class="table-drag" class="table-drag"
cdkDrag cdkDrag
cdkDragLockAxis="y" cdkDragLockAxis="y"
[columns]="snapshot.columns" [columns]="snapshot.columns"
[isCompact]="snapshot.isCompact" [isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled" [isDisabled]="snapshot.isDisabled"
@ -35,7 +35,7 @@
</div> </div>
<ng-container *sqxModal="contentCreatorDialog"> <ng-container *sqxModal="contentCreatorDialog">
<sqx-content-creator <sqx-content-creator
(select)="select($event)" (select)="select($event)"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
@ -44,7 +44,7 @@
</ng-container> </ng-container>
<ng-container *sqxModal="contentSelectorDialog"> <ng-container *sqxModal="contentSelectorDialog">
<sqx-content-selector <sqx-content-selector
(select)="select($event)" (select)="select($event)"
[allowDuplicates]="allowDuplicates" [allowDuplicates]="allowDuplicates"
[alreadySelected]="snapshot.contentItems" [alreadySelected]="snapshot.contentItems"

4
frontend/app/features/dashboard/pages/cards/api-calls-card.component.html

@ -1,10 +1,10 @@
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header"> <div class="card-header">
API Calls {{ 'dashboard.apiCallsCard' | sqxTranslate }}
<div class="float-right"> <div class="float-right">
<a class="force" (click)="downloadLog()"> <a class="force" (click)="downloadLog()">
<small>Download Log</small> <small>{{ 'dashboard.downloadLog' | sqxTranslate }}</small>
</a> </a>
</div> </div>
</div> </div>

10
frontend/app/features/dashboard/pages/cards/api-calls-summary-card.component.html

@ -1,10 +1,14 @@
<div class="card card"> <div class="card card">
<div class="card-header">API Calls</div> <div class="card-header">{{ 'dashboard.apiCallsSummaryCard' | sqxTranslate }}</div>
<div class="card-body"> <div class="card-body">
<div class="aggregation" *ngIf="callsTotal >= 0"> <div class="aggregation" *ngIf="callsTotal >= 0">
<div class="aggregation-label">This month</div> <div class="aggregation-label">{{ 'dashboard.currentMonthLabel' | sqxTranslate }}</div>
<div class="aggregation-value">{{callsTotal | sqxKNumber}}</div> <div class="aggregation-value">{{callsTotal | sqxKNumber}}</div>
<div class="aggregation-label" *ngIf="callsAllowed > 0">Monthly limit: {{callsAllowed | sqxKNumber}}</div>
<div class="aggregation-label" *ngIf="callsAllowed > 0">
{{ 'dashboard.apiCallsLimitLabel' | sqxTranslate }}: {{callsAllowed | sqxKNumber}}
</div>
</div> </div>
</div> </div>
</div> </div>

6
frontend/app/features/dashboard/pages/cards/api-card.component.html

@ -1,15 +1,15 @@
<div class="card card-href"> <div class="card card-href">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/dashboard-api.svg" /> <img src="./images/dashboard-api.svg">
</div> </div>
<h4 class="card-title"> <h4 class="card-title">
<a href="/api/content/{{app.name}}/docs" sqxExternalLink>Content API</a> <a href="/api/content/{{app.name}}/docs" sqxExternalLink>{{ 'dashboard.contentApi' | sqxTranslate }}</a>
</h4> </h4>
<div class="card-text"> <div class="card-text">
OpenAPI 3.0 compatible documentation for your app content. {{ 'dashboard.contentApiDescription' | sqxTranslate }}
</div> </div>
</div> </div>
</div> </div>

8
frontend/app/features/dashboard/pages/cards/api-performance-card.component.html

@ -1,15 +1,13 @@
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header"> <div class="card-header">
API Performance (ms): {{chartSummary}}ms avg {{ 'dashboard.apiPerformanceCard' | sqxTranslate: { summary: chartSummary } }}
<div class="float-right"> <div class="float-right">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="stacked" <input class="form-check-input" type="checkbox" id="stacked" [ngModel]="isStacked" (ngModelChange)="isStackedChange.emit($event)">
[ngModel]="isStacked"
(ngModelChange)="isStackedChange.emit($event)" />
<label class="form-check-label" for="stacked"> <label class="form-check-label" for="stacked">
Stacked {{ 'dashboard.stackedChart' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>

8
frontend/app/features/dashboard/pages/cards/api-traffic-card.component.html

@ -1,15 +1,13 @@
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header"> <div class="card-header">
Traffic (MB): {{chartSummary | sqxFileSize}} total {{ 'dashboard.trafficSummaryCard' | sqxTranslate }}: {{chartSummary | sqxFileSize}}
<div class="float-right"> <div class="float-right">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="stacked" <input class="form-check-input" type="checkbox" id="stacked" [ngModel]="isStacked" (ngModelChange)="isStackedChange.emit($event)">
[ngModel]="isStacked"
(ngModelChange)="isStackedChange.emit($event)" />
<label class="form-check-label" for="stacked"> <label class="form-check-label" for="stacked">
Stacked {{ 'dashboard.stackedChart' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>

2
frontend/app/features/dashboard/pages/cards/asset-uploads-count-card.component.html

@ -1,5 +1,5 @@
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header">Assets Uploads</div> <div class="card-header">{{ 'dashboard.assetUploadsCard' | sqxTranslate }}</div>
<div class="card-body"> <div class="card-body">
<chart type="line" [data]="chartData" [options]="chartOptions"></chart> <chart type="line" [data]="chartData" [options]="chartOptions"></chart>
</div> </div>

2
frontend/app/features/dashboard/pages/cards/asset-uploads-size-card.component.html

@ -1,5 +1,5 @@
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header">Assets Uploads</div> <div class="card-header">{{ 'dashboard.assetUploadsCard' | sqxTranslate }}</div>
<div class="card-body"> <div class="card-body">
<chart type="line" [data]="chartData" [options]="chartOptions"></chart> <chart type="line" [data]="chartData" [options]="chartOptions"></chart>
</div> </div>

10
frontend/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.html

@ -1,10 +1,14 @@
<div class="card card"> <div class="card card">
<div class="card-header">Assets Size (MB)</div> <div class="card-header">{{ 'dashboard.assetSizeCard' | sqxTranslate }})</div>
<div class="card-body"> <div class="card-body">
<div class="aggregation" *ngIf="storageCurrent >= 0"> <div class="aggregation" *ngIf="storageCurrent >= 0">
<div class="aggregation-label">Total Size</div> <div class="aggregation-label">{{ 'dashboard.assetSizeLabel' | sqxTranslate }}</div>
<div class="aggregation-value">{{storageCurrent | sqxFileSize}}</div> <div class="aggregation-value">{{storageCurrent | sqxFileSize}}</div>
<div class="aggregation-label" *ngIf="storageAllowed > 0">Total limit: {{storageAllowed | sqxFileSize}}</div>
<div class="aggregation-label" *ngIf="storageAllowed > 0">
{{ 'dashboard.assetSizeLimitLabel' | sqxTranslate }}: {{storageAllowed | sqxFileSize}}
</div>
</div> </div>
</div> </div>
</div> </div>

2
frontend/app/features/dashboard/pages/cards/content-summary-card.component.html

@ -2,7 +2,7 @@
<div class="card-header">{{options?.name}}</div> <div class="card-header">{{options?.name}}</div>
<div class="card-body"> <div class="card-body">
<div class="aggregation"> <div class="aggregation">
<div class="aggregation-label">Number of items</div> <div class="aggregation-label">{{ 'dashboard.contentsSummaryCard' | sqxTranslate }}</div>
<div class="aggregation-value">{{itemCount}}</div> <div class="aggregation-value">{{itemCount}}</div>
</div> </div>
</div> </div>

6
frontend/app/features/dashboard/pages/cards/github-card.component.html

@ -1,15 +1,15 @@
<div class="card card-href"> <div class="card card-href">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/dashboard-github.svg" /> <img src="./images/dashboard-github.svg">
</div> </div>
<h4 class="card-title"> <h4 class="card-title">
<a href="https://github.com/squidex/squidex" sqxExternalLink>Github</a> <a href="https://github.com/squidex/squidex" sqxExternalLink>{{ 'dashboard.github' | sqxTranslate }}</a>
</h4> </h4>
<div class="card-text"> <div class="card-text">
Get the source code from Github and report bugs or ask for support. {{ 'dashboard.githubCardDescription' | sqxTranslate }}
</div> </div>
</div> </div>
</div> </div>

2
frontend/app/features/dashboard/pages/cards/history-card.component.html

@ -1,5 +1,5 @@
<div class="card card-lg"> <div class="card card-lg">
<div class="card-header">History</div> <div class="card-header">{{ 'dashboard.historyCard' | sqxTranslate }}</div>
<div class="card-body card-history card-body-scroll"> <div class="card-body card-history card-body-scroll">
<sqx-history-list [events]="history | async"></sqx-history-list> <sqx-history-list [events]="history | async"></sqx-history-list>
</div> </div>

10
frontend/app/features/dashboard/pages/cards/schema-card.component.html

@ -1,26 +1,26 @@
<div class="card card-href"> <div class="card card-href">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/dashboard-schema.svg" /> <img src="./images/dashboard-schema.svg">
</div> </div>
<ng-container *ngIf="app.canReadSchemas; else noPermission"> <ng-container *ngIf="app.canReadSchemas; else noPermission">
<h4 class="card-title"> <h4 class="card-title">
<a [routerLink]="['schemas', { showDialog: true }]">New Schema</a> <a [routerLink]="['schemas', { showDialog: true }]">{{ 'dashboard.schemaNewCard' | sqxTranslate }}</a>
</h4> </h4>
<div class="card-text"> <div class="card-text">
A schema defines the structure of your content element. {{ 'dashboard.schemaNewCardDescription' | sqxTranslate }}
</div> </div>
</ng-container> </ng-container>
<ng-template #noPermission> <ng-template #noPermission>
<h4 class="card-title"> <h4 class="card-title">
<a [routerLink]="['schemas']">Schemas</a> <a [routerLink]="['schemas']">{{ 'dashboard.schemasCard' | sqxTranslate }}</a>
</h4> </h4>
<div class="card-text"> <div class="card-text">
Get an insight to the data model of this app. {{ 'dashboard.schemasCardDescription' | sqxTranslate }}
</div> </div>
</ng-template> </ng-template>
</div> </div>

6
frontend/app/features/dashboard/pages/cards/support-card.component.html

@ -1,15 +1,15 @@
<div class="card card-href"> <div class="card card-href">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">
<img src="./images/dashboard-feedback.svg" /> <img src="./images/dashboard-feedback.svg">
</div> </div>
<h4 class="card-title"> <h4 class="card-title">
<a href="https://support.squidex.io" sqxExternalLink>Feedback & Support</a> <a href="https://support.squidex.io" sqxExternalLink>{{ 'dashboard.supportCard' | sqxTranslate }}</a>
</h4> </h4>
<div class="card-text"> <div class="card-text">
Provide feedback and request features to help us to improve Squidex. {{ 'dashboard.supportCardDescription' | sqxTranslate }}
</div> </div>
</div> </div>
</div> </div>

22
frontend/app/features/dashboard/pages/dashboard-config.component.html

@ -7,29 +7,27 @@
<div class="dropdown-menu" [sqxAnchoredTo]="buttonSettings" @fade position="bottom-right"> <div class="dropdown-menu" [sqxAnchoredTo]="buttonSettings" @fade position="bottom-right">
<div class="dropdown-item" *ngFor="let item of configDefaults"> <div class="dropdown-item" *ngFor="let item of configDefaults">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="field_{{item.type}}" <input class="form-check-input" type="checkbox" id="field_{{item.type}}" [ngModel]="isSelected(item)" (ngModelChange)="addOrRemove(item)">
[ngModel]="isSelected(item)"
(ngModelChange)="addOrRemove(item)" />
<label class="form-check-label" for="field_{{item.type}}"> <label class="form-check-label" for="field_{{item.type}}">
{{item.name}} {{item.name | sqxTranslate}}
</label> </label>
</div> </div>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" (click)="startExpertMode()">Expert Mode</a> <a class="dropdown-item" (click)="startExpertMode()">{{ 'common.expertMode' | sqxTranslate }}</a>
<a class="dropdown-item" (click)="saveConfig()">Save</a> <a class="dropdown-item" (click)="saveConfig()">{{ 'common.save' | sqxTranslate }}</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" (beforeClick)="dropdownModal.hide()" <a class="dropdown-item dropdown-item-delete" (beforeClick)="dropdownModal.hide()"
(sqxConfirmClick)="resetConfig()" (sqxConfirmClick)="resetConfig()"
confirmTitle="Reset config" confirmTitle="i18n:dashboard.resetConfigConfirmTitle"
confirmText="Do you really want to reset the dashboard to the default?"> confirmText="i18n:dashboard.resetConfigConfirmText">
Reset {{ 'common.reset' | sqxTranslate }}
</a> </a>
</div> </div>
</ng-container> </ng-container>
@ -38,7 +36,7 @@
<ng-container *sqxModal="expertDialog"> <ng-container *sqxModal="expertDialog">
<sqx-modal-dialog (close)="expertDialog.hide()" fullHeight="true" size="lg"> <sqx-modal-dialog (close)="expertDialog.hide()" fullHeight="true" size="lg">
<ng-container title> <ng-container title>
Edit Config {{ 'dashboard.editConfig' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
@ -48,9 +46,9 @@
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="button" class="btn btn-secondary" (click)="expertDialog.hide()">Cancel</button> <button type="button" class="btn btn-secondary" (click)="expertDialog.hide()">{{ 'common.cancel' | sqxTranslate }}</button>
<button type="button" class="btn btn-primary" (click)="completeExpertMode()">Update</button> <button type="button" class="btn btn-primary" (click)="completeExpertMode()">{{ 'common.update' | sqxTranslate }}</button>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>
</ng-container> </ng-container>

26
frontend/app/features/dashboard/pages/dashboard-config.component.ts

@ -88,7 +88,7 @@ export class DashboardConfigComponent implements OnChanges {
public saveConfig() { public saveConfig() {
this.uiState.set('dashboard.grid', this.config, true); this.uiState.set('dashboard.grid', this.config, true);
this.dialogs.notifyInfo('Configuration saved.'); this.dialogs.notifyInfo('i18n:dashboard.configSaved');
} }
public addOrRemove(item: GridsterItem) { public addOrRemove(item: GridsterItem) {
@ -107,20 +107,20 @@ export class DashboardConfigComponent implements OnChanges {
} }
const DEFAULT_CONFIG: GridsterItem[] = [ const DEFAULT_CONFIG: GridsterItem[] = [
{ cols: 1, rows: 1, x: 0, y: 0, type: 'schemas', name: 'Schema' }, { cols: 1, rows: 1, x: 0, y: 0, type: 'schemas', name: 'i18n:common.schemas' },
{ cols: 1, rows: 1, x: 1, y: 0, type: 'api', name: 'API Documentation' }, { cols: 1, rows: 1, x: 1, y: 0, type: 'api', name: 'i18n:dashboard.apiDocumentation' },
{ cols: 1, rows: 1, x: 2, y: 0, type: 'support', name: 'Support' }, { cols: 1, rows: 1, x: 2, y: 0, type: 'support', name: 'i18n:common.schemas' },
{ cols: 1, rows: 1, x: 3, y: 0, type: 'github', name: 'Github' }, { cols: 1, rows: 1, x: 3, y: 0, type: 'github', name: 'i18n:dashboard.github' },
{ cols: 2, rows: 1, x: 0, y: 1, type: 'api-calls', name: 'API Calls Chart' }, { cols: 2, rows: 1, x: 0, y: 1, type: 'api-calls', name: 'i18n:dashboard.apiCallsChart' },
{ cols: 2, rows: 1, x: 2, y: 1, type: 'api-performance', name: 'API Performance Chart' }, { cols: 2, rows: 1, x: 2, y: 1, type: 'api-performance', name: 'i18n:dashboard.apiPerformanceChart' },
{ cols: 1, rows: 1, x: 0, y: 2, type: 'api-calls-summary', name: 'API Calls Summary' }, { cols: 1, rows: 1, x: 0, y: 2, type: 'api-calls-summary', name: 'i18n:dashboard.apiCallsSummaryCard' },
{ cols: 2, rows: 1, x: 1, y: 2, type: 'asset-uploads-count', name: 'Asset Uploads Count Chart' }, { cols: 2, rows: 1, x: 1, y: 2, type: 'asset-uploads-count', name: 'i18n:dashboard.assetUpdloadsCountChart' },
{ cols: 1, rows: 1, x: 2, y: 2, type: 'asset-uploads-size-summary', name: 'Asset Uploads Size Chart' }, { cols: 1, rows: 1, x: 2, y: 2, type: 'asset-uploads-size-summary', name: 'i18n:dashboard.assetUploadsSizeChart' },
{ cols: 2, rows: 1, x: 0, y: 3, type: 'asset-uploads-size', name: 'Asset Total Storage Size' }, { cols: 2, rows: 1, x: 0, y: 3, type: 'asset-uploads-size', name: 'i18n:dashboard.assetTotalSize' },
{ cols: 2, rows: 1, x: 2, y: 3, type: 'api-traffic', name: 'API Traffic Chart' }, { cols: 2, rows: 1, x: 2, y: 3, type: 'api-traffic', name: 'i18n:dashboard.trafficChart' },
{ cols: 2, rows: 1, x: 0, y: 4, type: 'history', name: 'History' } { cols: 2, rows: 1, x: 0, y: 4, type: 'history', name: 'i18n:dashboard.history' }
]; ];

65
frontend/app/features/dashboard/pages/dashboard-page.component.html

@ -1,90 +1,70 @@
<sqx-title message="Dashboard"></sqx-title> <sqx-title message="i18n:dashboard.pageTitle"></sqx-title>
<ng-container *ngIf="appsState.selectedApp | async; let app"> <ng-container *ngIf="appsState.selectedApp | async; let app">
<div class="dashboard" @fade> <div class="dashboard" @fade="">
<div class="dashboard-summary" *ngIf="!isScrolled" @fade> <div class="dashboard-summary" *ngIf="!isScrolled" @fade="">
<h1 class="dashboard-title">Hi {{authState.user?.displayName}}</h1> <h1 class="dashboard-title">{{ 'dashboard.welcomeTitle' | sqxTranslate: { user: authState.user?.displayName } }}</h1>
<div class="subtext"> <div class="subtext" [innerHTML]="'dashboard.welcomeText' | sqxTranslate: { app: app.displayName } | sqxMarkdown"></div>
Welcome to <strong>{{app.displayName}}</strong> dashboard.
</div>
</div> </div>
<gridster [options]="gridOptions" #grid> <gridster [options]="gridOptions" #grid="">
<gridster-item [item]="item" *ngFor="let item of gridConfig"> <gridster-item [item]="item" *ngFor="let item of gridConfig">
<ng-container [ngSwitch]="item.type"> <ng-container [ngSwitch]="item.type">
<ng-container *ngSwitchCase="'schemas'"> <ng-container *ngSwitchCase="'schemas'">
<sqx-schema-card <sqx-schema-card [app]="app">
[app]="app">
</sqx-schema-card> </sqx-schema-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'api'"> <ng-container *ngSwitchCase="'api'">
<sqx-api-card <sqx-api-card [app]="app">
[app]="app">
</sqx-api-card> </sqx-api-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'support'"> <ng-container *ngSwitchCase="'support'">
<sqx-support-card <sqx-support-card [app]="app">
[app]="app">
</sqx-support-card> </sqx-support-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'github'"> <ng-container *ngSwitchCase="'github'">
<sqx-github-card <sqx-github-card [app]="app">
[app]="app">
</sqx-github-card> </sqx-github-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'api-calls'"> <ng-container *ngSwitchCase="'api-calls'">
<sqx-api-calls-card <sqx-api-calls-card [app]="app" [usage]="callsUsage">
[app]="app" [usage]="callsUsage">
</sqx-api-calls-card> </sqx-api-calls-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'api-performance'"> <ng-container *ngSwitchCase="'api-performance'">
<sqx-api-performance-card <sqx-api-performance-card [isStacked]="isStacked" (isStackedChange)="changeIsStacked($event)" [app]="app" [usage]="callsUsage">
[isStacked]="isStacked"
(isStackedChange)="changeIsStacked($event)"
[app]="app" [usage]="callsUsage">
</sqx-api-performance-card> </sqx-api-performance-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'api-calls-summary'"> <ng-container *ngSwitchCase="'api-calls-summary'">
<sqx-api-calls-summary-card <sqx-api-calls-summary-card [app]="app" [usage]="callsUsage">
[app]="app" [usage]="callsUsage">
</sqx-api-calls-summary-card> </sqx-api-calls-summary-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'asset-uploads-count'"> <ng-container *ngSwitchCase="'asset-uploads-count'">
<sqx-asset-uploads-count-card <sqx-asset-uploads-count-card [app]="app" [usage]="storageUsage">
[app]="app" [usage]="storageUsage">
</sqx-asset-uploads-count-card> </sqx-asset-uploads-count-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'asset-uploads-size-summary'"> <ng-container *ngSwitchCase="'asset-uploads-size-summary'">
<sqx-asset-uploads-size-summary-card <sqx-asset-uploads-size-summary-card [app]="app" [usage]="storageCurrent">
[app]="app" [usage]="storageCurrent">
</sqx-asset-uploads-size-summary-card> </sqx-asset-uploads-size-summary-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'asset-uploads-size'"> <ng-container *ngSwitchCase="'asset-uploads-size'">
<sqx-asset-uploads-size-card <sqx-asset-uploads-size-card [app]="app" [usage]="storageUsage">
[app]="app" [usage]="storageUsage">
</sqx-asset-uploads-size-card> </sqx-asset-uploads-size-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'api-traffic'"> <ng-container *ngSwitchCase="'api-traffic'">
<sqx-api-traffic-card <sqx-api-traffic-card [isStacked]="isStacked" (isStackedChange)="changeIsStacked($event)" [app]="app" [usage]="callsUsage">
[isStacked]="isStacked"
(isStackedChange)="changeIsStacked($event)"
[app]="app" [usage]="callsUsage">
</sqx-api-traffic-card> </sqx-api-traffic-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'history'"> <ng-container *ngSwitchCase="'history'">
<sqx-history-card <sqx-history-card [app]="app">
[app]="app">
</sqx-history-card> </sqx-history-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'content-summary'"> <ng-container *ngSwitchCase="'content-summary'">
<sqx-content-summary-card <sqx-content-summary-card [app]="app" [options]="item">
[app]="app" [options]="item">
</sqx-content-summary-card> </sqx-content-summary-card>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'iframe'"> <ng-container *ngSwitchCase="'iframe'">
<sqx-iframe-card <sqx-iframe-card [app]="app" [options]="item">
[app]="app" [options]="item">
</sqx-iframe-card> </sqx-iframe-card>
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -92,10 +72,7 @@
</gridster> </gridster>
<div class="dashboard-settings"> <div class="dashboard-settings">
<sqx-dashboard-config [app]="app" <sqx-dashboard-config [app]="app" [needsAttention]="isScrolled" [config]="gridConfig" (configChange)="changeConfig($event)">
[needsAttention]="isScrolled"
[config]="gridConfig"
(configChange)="changeConfig($event)">
</sqx-dashboard-config> </sqx-dashboard-config>
</div> </div>
</div> </div>

26
frontend/app/features/rules/pages/events/rule-events-page.component.html

@ -1,13 +1,13 @@
<sqx-title message="Events"></sqx-title> <sqx-title message="i18n:rules.ruleEvents.listPageTitle"></sqx-title>
<sqx-panel desiredWidth="63rem" grid="true"> <sqx-panel desiredWidth="63rem" grid="true">
<ng-container title> <ng-container title>
Events {{ 'common.events' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh Events (CTRL + SHIFT + R)"> <button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:rules.refreshEventsTooltip">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
@ -20,16 +20,16 @@
<thead> <thead>
<tr> <tr>
<th class="cell-label"> <th class="cell-label">
Status {{ 'common.status' | sqxTranslate }}
</th> </th>
<th class="cell-40"> <th class="cell-40">
Event {{ 'common.event' | sqxTranslate }}
</th> </th>
<th class="cell-60"> <th class="cell-60">
Description {{ 'common.description' | sqxTranslate }}
</th> </th>
<th class="cell-time"> <th class="cell-time">
Created {{ 'common.created' | sqxTranslate }}
</th> </th>
<th class="cell-actions"></th> <th class="cell-actions"></th>
</tr> </tr>
@ -58,7 +58,7 @@
<tr *ngIf="selectedEventId === event.id"> <tr *ngIf="selectedEventId === event.id">
<td colspan="5"> <td colspan="5">
<div class="event-header"> <div class="event-header">
<h3>Last Invocation</h3> <h3>{{ 'rules.ruleEvents.lastInvokedLabel' | sqxTranslate }}</h3>
</div> </div>
<div class="row no-gutters event-stats align-items-center"> <div class="row no-gutters event-stats align-items-center">
@ -66,18 +66,18 @@
<span class="badge badge-pill badge-{{event.result | sqxRuleEventBadgeClass}}">{{event.result}}</span> <span class="badge badge-pill badge-{{event.result | sqxRuleEventBadgeClass}}">{{event.result}}</span>
</div> </div>
<div class="col-2"> <div class="col-2">
Attempts: {{event.numCalls}} {{ 'rules.ruleEvents.numAttemptsLabel' | sqxTranslate }}: {{event.numCalls}}
</div> </div>
<div class="col-4"> <div class="col-4">
Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container> {{ 'rules.ruleEvents.nextAttemptLabel' | sqxTranslate }}: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container>
</div> </div>
<div class="col-3 text-right"> <div class="col-3 text-right">
<button type="button" class="btn btn-text-danger btn-sm mr-1" (click)="cancel(event)" [class.hidden]="!event.nextAttempt"> <button type="button" class="btn btn-text-danger btn-sm mr-1" (click)="cancel(event)" [class.hidden]="!event.nextAttempt">
Cancel {{ 'common.cancel' | sqxTranslate }}
</button> </button>
<button type="button" class="btn btn-success btn-sm" (click)="enqueue(event)"> <button type="button" class="btn btn-success btn-sm" (click)="enqueue(event)">
Enqueue {{ 'rules.ruleEvents.enqueue' | sqxTranslate }}
</button> </button>
</div> </div>
</div> </div>

2
frontend/app/features/rules/pages/rules/rule-element.component.html

@ -24,7 +24,7 @@
</div> </div>
<div class="large-link" *ngIf="element.readMore"> <div class="large-link" *ngIf="element.readMore">
<a [href]="element.readMore" sqxStopClick sqxExternalLink>Read More</a> <a [href]="element.readMore" sqxStopClick sqxExternalLink>{{ 'rules.readMore' | sqxTranslate }}</a>
</div> </div>
</div> </div>
</div> </div>

53
frontend/app/features/rules/pages/rules/rule-wizard.component.html

@ -1,13 +1,13 @@
<sqx-modal-dialog size="lg" fullHeight="true" (close)="emitComplete()" [showFooter]="step === 2 || step === 4"> <sqx-modal-dialog size="lg" fullHeight="true" (close)="emitComplete()" [showFooter]="step === 2 || step === 4">
<ng-container title> <ng-container title>
<ng-container *ngIf="mode === 'EditTrigger'"> <ng-container *ngIf="mode === 'EditTrigger'">
Edit Trigger {{ 'rules.triggerEdit' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container *ngIf="mode === 'EditAction'"> <ng-container *ngIf="mode === 'EditAction'">
Edit Action {{ 'rules.actionEdit' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container *ngIf="mode === 'Wizard'"> <ng-container *ngIf="mode === 'Wizard'">
Create new Rule {{ 'rules.create' | sqxTranslate }}
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -16,21 +16,21 @@
<ol class="breadcrumb steps"> <ol class="breadcrumb steps">
<li class="breadcrumb-item" [class.active]="step === 1" [class.done]="step > 1"> <li class="breadcrumb-item" [class.active]="step === 1" [class.done]="step > 1">
<a class="force" (click)="go(1)" [class.disabled]="step <= 1"> <a class="force" (click)="go(1)" [class.disabled]="step <= 1">
<i class="icon-checkmark"></i> Select Trigger <i class="icon-checkmark"></i> {{ 'rules.wizard.selectTrigger' | sqxTranslate }}
</a> </a>
</li> </li>
<li class="breadcrumb-item" [class.active]="step === 2" [class.done]="step > 2"> <li class="breadcrumb-item" [class.active]="step === 2" [class.done]="step > 2">
<a class="force" (click)="go(2)" [class.disabled]="step <= 2"> <a class="force" (click)="go(2)" [class.disabled]="step <= 2">
<i class="icon-checkmark"></i> Edit Trigger <i class="icon-checkmark"></i> {{ 'rules.triggerEdit' | sqxTranslate }}
</a> </a>
</li> </li>
<li class="breadcrumb-item" [class.active]="step === 3" [class.done]="step > 3"> <li class="breadcrumb-item" [class.active]="step === 3" [class.done]="step > 3">
<a class="force" (click)="go(3)" [class.disabled]="step <= 3"> <a class="force" (click)="go(3)" [class.disabled]="step <= 3">
<i class="icon-checkmark"></i> Select Action <i class="icon-checkmark"></i> {{ 'rules.wizard.selectAction' | sqxTranslate }}
</a> </a>
</li> </li>
<li class="breadcrumb-item" [class.active]="step === 4"> <li class="breadcrumb-item" [class.active]="step === 4">
<i class="icon-checkmark"></i> Edit Action <i class="icon-checkmark"></i> {{ 'rules.actionEdit' | sqxTranslate }}
</li> </li>
</ol> </ol>
</nav> </nav>
@ -38,7 +38,7 @@
<ng-container [ngSwitch]="step"> <ng-container [ngSwitch]="step">
<ng-container *ngSwitchCase="1"> <ng-container *ngSwitchCase="1">
<sqx-form-alert> <sqx-form-alert>
The selection of the trigger type cannot be changed later. {{ 'rules.wizard.triggerHint' | sqxTranslate }}
</sqx-form-alert> </sqx-form-alert>
<div class="row no-gutters"> <div class="row no-gutters">
@ -57,34 +57,23 @@
<ng-container [ngSwitch]="trigger.triggerType"> <ng-container [ngSwitch]="trigger.triggerType">
<ng-container *ngSwitchCase="'AssetChanged'"> <ng-container *ngSwitchCase="'AssetChanged'">
<sqx-asset-changed-trigger <sqx-asset-changed-trigger [trigger]="trigger" [triggerForm]="triggerForm.form">
[trigger]="trigger"
[triggerForm]="triggerForm.form">
</sqx-asset-changed-trigger> </sqx-asset-changed-trigger>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Comment'"> <ng-container *ngSwitchCase="'Comment'">
<sqx-comment-trigger <sqx-comment-trigger [trigger]="trigger" [triggerForm]="triggerForm.form">
[trigger]="trigger"
[triggerForm]="triggerForm.form">
</sqx-comment-trigger> </sqx-comment-trigger>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'ContentChanged'"> <ng-container *ngSwitchCase="'ContentChanged'">
<sqx-content-changed-trigger <sqx-content-changed-trigger [schemas]="schemas" [trigger]="trigger" [triggerForm]="triggerForm.form">
[schemas]="schemas"
[trigger]="trigger"
[triggerForm]="triggerForm.form">
</sqx-content-changed-trigger> </sqx-content-changed-trigger>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'SchemaChanged'"> <ng-container *ngSwitchCase="'SchemaChanged'">
<sqx-schema-changed-trigger <sqx-schema-changed-trigger [trigger]="trigger" [triggerForm]="triggerForm.form">
[trigger]="trigger"
[triggerForm]="triggerForm.form">
</sqx-schema-changed-trigger> </sqx-schema-changed-trigger>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Usage'"> <ng-container *ngSwitchCase="'Usage'">
<sqx-usage-trigger <sqx-usage-trigger [trigger]="trigger" [triggerForm]="triggerForm.form">
[trigger]="trigger"
[triggerForm]="triggerForm.form">
</sqx-usage-trigger> </sqx-usage-trigger>
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -92,7 +81,7 @@
</ng-container> </ng-container>
<ng-container *ngSwitchCase="3"> <ng-container *ngSwitchCase="3">
<sqx-form-alert marginTop="0"> <sqx-form-alert marginTop="0">
The selection of the action type cannot be changed later. {{ 'rules.wizard.actionHint' | sqxTranslate }}
</sqx-form-alert> </sqx-form-alert>
<div class="row no-gutters"> <div class="row no-gutters">
@ -109,11 +98,7 @@
{{actionElement.display}} {{actionElement.display}}
</h3> </h3>
<sqx-generic-action <sqx-generic-action [definition]="actionElement" [action]="action" [actionForm]="actionForm.form"></sqx-generic-action>
[definition]="actionElement"
[action]="action"
[actionForm]="actionForm.form">
</sqx-generic-action>
</form> </form>
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -121,14 +106,14 @@
<ng-container footer> <ng-container footer>
<ng-container *ngIf="step === 2 || step === 4"> <ng-container *ngIf="step === 2 || step === 4">
<button type="button" class="btn btn-secondary" (click)="emitComplete()">Cancel</button> <button type="button" class="btn btn-secondary" (click)="emitComplete()">{{ 'common.cancel' | sqxTranslate }}</button>
</ng-container> </ng-container>
<ng-container *ngIf="isEditable"> <ng-container *ngIf="isEditable">
<button *ngIf="step === 2 && isWizard" type="submit" class="btn btn-primary" (click)="saveTrigger()">Next</button> <button *ngIf="step === 2 && isWizard" type="submit" class="btn btn-primary" (click)="saveTrigger()">{{ 'common.continue' | sqxTranslate }}</button>
<button *ngIf="step === 2 && !isWizard" type="submit" class="btn btn-primary" (click)="saveTrigger()">Save</button> <button *ngIf="step === 2 && !isWizard" type="submit" class="btn btn-primary" (click)="saveTrigger()">{{ 'common.save' | sqxTranslate }}</button>
<button *ngIf="step === 4" type="submit" class="btn btn-primary" (click)="saveAction()">Save</button> <button *ngIf="step === 4" type="submit" class="btn btn-primary" (click)="saveAction()">{{ 'common.save' | sqxTranslate }}</button>
</ng-container> </ng-container>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

38
frontend/app/features/rules/pages/rules/rule.component.html

@ -2,13 +2,12 @@
<div class="card-header"> <div class="card-header">
<div class="row"> <div class="row">
<div class="col col-name"> <div class="col col-name">
<sqx-editable-title <sqx-editable-title [disabled]="!rule.canUpdate"
fallback="Unnamed Rule" [fallback]="'rules.unnamed' | sqxTranslate"
[name]="rule.name" [name]="rule.name"
(nameChange)="rename($event)" (nameChange)="rename($event)"
[maxLength]="60" [maxLength]="60"
[isRequired]="false" [isRequired]="false">
[disabled]="!rule.canUpdate">
</sqx-editable-title> </sqx-editable-title>
</div> </div>
<div class="col-auto" *ngIf="rule.canDelete || rule.canRun"> <div class="col-auto" *ngIf="rule.canDelete || rule.canRun">
@ -20,16 +19,16 @@
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" position="bottom-right" @fade> <div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" position="bottom-right" @fade>
<a class="dropdown-item" *ngIf="rule.canRun" <a class="dropdown-item" *ngIf="rule.canRun"
(sqxConfirmClick)="run()" (sqxConfirmClick)="run()"
confirmTitle="Run rule" confirmTitle="i18n:rules.runRuleConfirmTitle"
confirmText="Do you really want to run the rule for all events?"> confirmText="i18n:rules.runRuleConfirmText">
Run {{ 'rules.run' | sqxTranslate }}
</a> </a>
<a class="dropdown-item dropdown-item-delete" *ngIf="rule.canDelete" <a class="dropdown-item dropdown-item-delete" *ngIf="rule.canDelete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="Delete rule" confirmTitle="i18n:rules.deleteConfirmTitle"
confirmText="Do you really want to delete the rule?"> confirmText="i18n:rules.deleteConfirmText">
Delete {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>
</ng-container> </ng-container>
@ -39,7 +38,7 @@
<div class="card-body"> <div class="card-body">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-auto"> <div class="col-auto">
<h3>If</h3> <h3>{{ 'rules.ruleSyntax.if' | sqxTranslate }}</h3>
</div> </div>
<div class="col"> <div class="col">
<span (click)="emitEditTrigger()"> <span (click)="emitEditTrigger()">
@ -47,7 +46,7 @@
</span> </span>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<h3>then</h3> <h3>{{ 'rules.ruleSyntax.then' | sqxTranslate }}</h3>
</div> </div>
<div class="col"> <div class="col">
<span (click)="emitEditAction()"> <span (click)="emitEditAction()">
@ -56,11 +55,10 @@
</div> </div>
<div class="col col-last text-right"> <div class="col col-last text-right">
<ng-container *ngIf="isManual; else notManual"> <ng-container *ngIf="isManual; else notManual">
<button class="btn btn-secondary" <button class="btn btn-secondary" [disabled]="!rule.canTrigger"
[disabled]="!rule.canTrigger"
(sqxConfirmClick)="trigger()" (sqxConfirmClick)="trigger()"
confirmTitle="Trigger rule" confirmTitle="i18n:rules.triggerConfirmTitle"
confirmText="Do you really want to trigger the rule?"> confirmText="i18n:rules.triggerConfirmText">
<i class="icon-play-line"></i> <i class="icon-play-line"></i>
</button> </button>
</ng-container> </ng-container>
@ -74,17 +72,17 @@
<div class="card-footer"> <div class="card-footer">
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-3">
Succeeded: <strong>{{rule.numSucceeded}}</strong> {{ 'common.succeeded' | sqxTranslate }}: <strong>{{rule.numSucceeded}}</strong>
</div> </div>
<div class="col-3"> <div class="col-3">
Failed: <strong>{{rule.numFailed}}</strong> {{ 'common.failed' | sqxTranslate }}: <strong>{{rule.numFailed}}</strong>
</div> </div>
<div class="col"> <div class="col">
Executed: <span>{{rule.lastExecuted | sqxFromNow:'-'}}</span> {{ 'common.executed' | sqxTranslate }}: <span>{{rule.lastExecuted | sqxFromNow:'-'}}</span>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<a routerLink="events" [queryParams]="{ ruleId: rule.id }" *ngIf="rule.canTrigger"> <a routerLink="events" [queryParams]="{ ruleId: rule.id }" *ngIf="rule.canTrigger">
Logs {{ 'common.logs' | sqxTranslate }}
</a> </a>
</div> </div>
</div> </div>

29
frontend/app/features/rules/pages/rules/rules-page.component.html

@ -1,13 +1,13 @@
<sqx-title message="Rules"></sqx-title> <sqx-title message="i18n:rules.listPageTitle"></sqx-title>
<sqx-panel desiredWidth="54rem" showSidebar="true" grid="true"> <sqx-panel desiredWidth="54rem" showSidebar="true" grid="true">
<ng-container title> <ng-container title>
Rules {{ 'common.rules' | sqxTranslate }}
</ng-container> </ng-container>
<ng-container menu> <ng-container menu>
<button type="button" class="btn btn-text-secondary mr-1" (click)="reload()" title="Refresh Rules (CTRL + SHIFT + R)"> <button type="button" class="btn btn-text-secondary mr-1" (click)="reload()" title="i18n:rules.refreshTooltip">
<i class="icon-reset"></i> Refresh <i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
@ -15,8 +15,8 @@
<ng-container *ngIf="rulesState.canCreate | async"> <ng-container *ngIf="rulesState.canCreate | async">
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<button type="button" class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)"> <button type="button" class="btn btn-success" #buttonNew (click)="createNew()" title="i18n:rules.createTooltip">
<i class="icon-plus"></i> New <i class="icon-plus"></i> {{ 'rules.create' | sqxTranslate }}
</button> </button>
</ng-container> </ng-container>
</ng-container> </ng-container>
@ -26,20 +26,20 @@
<ng-container topHeader> <ng-container topHeader>
<div class="panel-alert panel-alert-danger" *ngIf="rulesState.runningRule | async; let runningRule"> <div class="panel-alert panel-alert-danger" *ngIf="rulesState.runningRule | async; let runningRule">
<div class="float-right"> <div class="float-right">
<a class="force" (click)="cancelRun()">Cancel</a> <a class="force" (click)="cancelRun()">{{ 'common.cancel' | sqxTranslate }}</a>
</div> </div>
Rule '{{runningRule.name || 'Unnamed Rule'}}' is currently running. {{ 'rules.runningRule' | sqxTranslate: { name: runningRule.name || 'Unnamed Rule' } }}
</div> </div>
</ng-container> </ng-container>
<div content> <div content>
<ng-container *ngIf="ruleActions && ruleTriggers && (rulesState.isLoaded | async) && (rulesState.rules | async); let rules"> <ng-container *ngIf="ruleActions && ruleTriggers && (rulesState.isLoaded | async) && (rulesState.rules | async); let rules">
<div class="table-items-row table-items-row-empty" *ngIf="rules.length === 0"> <div class="table-items-row table-items-row-empty" *ngIf="rules.length === 0">
No rule created yet. {{ 'rules.empty' | sqxTranslate }}
<button type="button" class="btn btn-success btn-sm ml-2" (click)="createNew()" *ngIf="rulesState.canCreate | async"> <button type="button" class="btn btn-success btn-sm ml-2" (click)="createNew()" *ngIf="rulesState.canCreate | async">
<i class="icon icon-plus"></i> Add Rule <i class="icon icon-plus"></i> {{ 'rules.emptyAddRule' | sqxTranslate }}
</button> </button>
</div> </div>
@ -56,8 +56,7 @@
</table> </table>
<ng-container *sqxModal="addRuleDialog"> <ng-container *sqxModal="addRuleDialog">
<sqx-rule-wizard <sqx-rule-wizard [schemas]="schemasState.schemas | async"
[schemas]="schemasState.schemas | async"
[rule]="wizardRule" [rule]="wizardRule"
[ruleActions]="ruleActions" [ruleActions]="ruleActions"
[ruleTriggers]="ruleTriggers" [ruleTriggers]="ruleTriggers"
@ -73,17 +72,17 @@
<ng-container sidebar> <ng-container sidebar>
<div class="panel-nav"> <div class="panel-nav">
<ng-container *ngIf="rulesState.canReadEvents | async"> <ng-container *ngIf="rulesState.canReadEvents | async">
<a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" title="History" titlePosition="left"> <a class="panel-link panel-link-gray" routerLink="events" routerLinkActive="active" title="i18n:common.history" titlePosition="left">
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
</ng-container> </ng-container>
<a class="panel-link" routerLink="help" routerLinkActive="active" title="Help" titlePosition="left" #helpLink> <a class="panel-link" routerLink="help" routerLinkActive="active" title="i18n:common.help" titlePosition="left" #helpLink>
<i class="icon-help2"></i> <i class="icon-help2"></i>
</a> </a>
<sqx-onboarding-tooltip helpId="help" [for]="helpLink" position="left-top" after="180000"> <sqx-onboarding-tooltip helpId="help" [for]="helpLink" position="left-top" after="180000">
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" sqxExternalLink>https://docs.squidex.io</a> for the full documentation. <span [innerHTML]="'common.helpTour' | sqxTranslate | sqxMarkdownInline"></span>
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</div> </div>
</ng-container> </ng-container>

26
frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html

@ -1,45 +1,47 @@
<form [formGroup]="fieldForm.form" (ngSubmit)="saveSchema()"> <form [formGroup]="fieldForm.form" (ngSubmit)="saveSchema()">
<div class="card"> <div class="card">
<div class="card-header">Common</div> <div class="card-header">{{ 'common.generalSettings' | sqxTranslate }}</div>
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="schemaName">Name</label> <label for="name">{{ 'common.name' | sqxTranslate }}</label>
<input type="text" class="form-control" id="schemaName" readonly [ngModel]="schema.name" [ngModelOptions]="standalone" /> <input type="text" class="form-control" id="name" readonly [ngModel]="schema.name" [ngModelOptions]="standalone">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="schemaLabel">Label</label> <label for="label">{{ 'common.label' | sqxTranslate }}</label>
<sqx-control-errors for="label"></sqx-control-errors> <sqx-control-errors for="label"></sqx-control-errors>
<input type="text" class="form-control" id="schemaLabel" formControlName="label" /> <input type="text" class="form-control" id="label" formControlName="label">
<sqx-form-hint>Display name for documentation and user interfaces.</sqx-form-hint> <sqx-form-hint>{{ 'schemas.schemaLabelHint' | sqxTranslate }}</sqx-form-hint>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="schemaHints">Hints</label> <label for="hints">{{ 'common.hints' | sqxTranslate }}</label>
<sqx-control-errors for="hints"></sqx-control-errors> <sqx-control-errors for="hints"></sqx-control-errors>
<textarea type="text" class="form-control" id="schemaHints" formControlName="hints" rows="4"></textarea> <textarea type="text" class="form-control" id="hints" formControlName="hints" rows="4"></textarea>
<sqx-form-hint>{{ 'schemas.schemaHintsHint' | sqxTranslate }}</sqx-form-hint>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="schemaTags">Tags</label> <label for="tags">{{ 'common.tags' | sqxTranslate }}</label>
<sqx-control-errors for="tags"></sqx-control-errors> <sqx-control-errors for="tags"></sqx-control-errors>
<sqx-tag-editor id="schemaTags" formControlName="tags"></sqx-tag-editor> <sqx-tag-editor id="tags" formControlName="tags"></sqx-tag-editor>
<sqx-form-hint>Tags to annotate your schema for automation processes.</sqx-form-hint> <sqx-form-hint>{{ 'schemas.schemaTagsHint' | sqxTranslate }}</sqx-form-hint>
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<button type="submit" class="float-right btn btn-primary" *ngIf="isEditable">Save</button> <button type="submit" class="float-right btn btn-primary" *ngIf="isEditable">{{ 'common.save' | sqxTranslate }}</button>
</div> </div>
</div> </div>
</form> </form>

10
frontend/app/features/schemas/pages/schema/export/schema-export-form.component.html

@ -5,22 +5,22 @@
<div class="col-auto"> <div class="col-auto">
<div class="form-inline"> <div class="form-inline">
<div class="form-check pr-4"> <div class="form-check pr-4">
<input class="form-check-input" type="checkbox" id="fieldsDelete" formControlName="fieldsDelete" /> <input class="form-check-input" type="checkbox" id="fieldsDelete" formControlName="fieldsDelete">
<label class="form-check-label" for="fieldsDelete"> <label class="form-check-label" for="fieldsDelete">
Delete fields {{ 'schemas.export.deleteFields' | sqxTranslate }}
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldsRecreate" formControlName="fieldsRecreate" /> <input class="form-check-input" type="checkbox" id="fieldsRecreate" formControlName="fieldsRecreate">
<label class="form-check-label" for="fieldsRecreate"> <label class="form-check-label" for="fieldsRecreate">
Recreate fields {{ 'schemas.export.recreateFields' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="submit" class="btn btn-primary">Synchronize</button> <button type="submit" class="btn btn-primary">{{ 'schemas.export.synchronize' | sqxTranslate }}</button>
</div> </div>
</div> </div>
</div> </div>

35
frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html

@ -1,7 +1,7 @@
<sqx-modal-dialog (close)="emitComplete()" size="lg"> <sqx-modal-dialog (close)="emitComplete()" size="lg">
<ng-container title> <ng-container title>
<ng-container *ngIf="parent; else noParent"> <ng-container *ngIf="parent; else noParent">
Add Nested Field {{ 'schemas.addNestedField' | sqxTranslate }}
</ng-container> </ng-container>
<ng-template #noParent> <ng-template #noParent>
@ -18,7 +18,7 @@
<div class="row"> <div class="row">
<div class="col-4 type" *ngFor="let fieldType of fieldTypes"> <div class="col-4 type" *ngFor="let fieldType of fieldTypes">
<label> <label>
<input type="radio" class="radio-input" name="type" formControlName="type" value="{{fieldType.type}}" /> <input type="radio" class="radio-input" name="type" formControlName="type" value="{{fieldType.type}}">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-auto"> <div class="col-auto">
@ -28,7 +28,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="type-title">{{fieldType.type}}</div> <div class="type-title">{{fieldType.type}}</div>
<div class="type-text text-muted">{{fieldType.description}}</div> <div class="type-text text-muted">{{fieldType.description | sqxTranslate}}</div>
</div> </div>
</div> </div>
</label> </label>
@ -39,53 +39,48 @@
<div class="form-group"> <div class="form-group">
<sqx-control-errors for="name" submitOnly="true"></sqx-control-errors> <sqx-control-errors for="name" submitOnly="true"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" #nameInput <input type="text" class="form-control" formControlName="name" maxlength="40" #nameInput placeholder="{{ 'schemas.field.namePlaceholder' | sqxTranslate }}" sqxFocusOnInit>
placeholder="Enter field name" sqxFocusOnInit />
</div> </div>
<div class="form-group" *ngIf="!parent && (addFieldForm.isContentField | async)"> <div class="form-group" *ngIf="!parent && (addFieldForm.isContentField | async)">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable" /> <input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable">
<label class="form-check-label" for="isLocalizable"> <label class="form-check-label" for="isLocalizable">
Localizable {{ 'schemas.field.localizable' | sqxTranslate }}
</label> </label>
</div> </div>
<sqx-form-hint> <sqx-form-hint>
You can mark the field as localizable. It means that is dependent on the language, for example a city name. {{ 'schemas.field.localizableHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
<sqx-form-alert class="mt-4"> <sqx-form-alert class="mt-4">
These values cannot be changed later. {{ 'schemas.nameWarning' | sqxTranslate }}
</sqx-form-alert> </sqx-form-alert>
</form> </form>
</ng-container> </ng-container>
<ng-template #notEditing> <ng-template #notEditing>
<form [formGroup]="editForm.form" class="edit-form" (ngSubmit)="save()"> <form [formGroup]="editForm.form" class="edit-form" (ngSubmit)="save()">
<sqx-field-form <sqx-field-form [isEditable]="true" [field]="field" [fieldForm]="editForm.form" [patterns]="patternsState.patterns | async">
[isEditable]="true"
[field]="field"
[fieldForm]="editForm.form"
[patterns]="patternsState.patterns | async">
</sqx-field-form> </sqx-field-form>
</form> </form>
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">Cancel</button> <button type="reset" class="float-left btn btn-secondary" (click)="emitComplete()">{{ 'common.cancel' | sqxTranslate }}</button>
<div *ngIf="!editing"> <div *ngIf="!editing">
<button type="button" class="btn btn-outline-success" (click)="addField(false)">Create and close</button> <button type="button" class="btn btn-outline-success" (click)="addField(false)">{{ 'schemas.addFieldAndClose' | sqxTranslate }}</button>
<button type="button" class="btn btn-success ml-1" (click)="addField(true)">Create and add field</button> <button type="button" class="btn btn-success ml-1" (click)="addField(true)">{{ 'schemas.addFieldAndCreate' | sqxTranslate }}</button>
<button type="button" class="btn btn-success ml-1" (click)="addField(false, true)">Create and edit field</button> <button type="button" class="btn btn-success ml-1" (click)="addField(false, true)">{{ 'schemas.addFieldAndEdit' | sqxTranslate }}</button>
</div> </div>
<div *ngIf="editing"> <div *ngIf="editing">
<button type="button" class="btn btn-success" (click)="save(true)">Save and add field</button> <button type="button" class="btn btn-success" (click)="save(true)">{{ 'schemas.saveFieldAndNew' | sqxTranslate }}</button>
<button type="button" class="btn btn-primary ml-1" (click)="save()">Save and close</button> <button type="button" class="btn btn-primary ml-1" (click)="save()">{{ 'schemas.saveFieldAndClose' | sqxTranslate }}</button>
</div> </div>
</ng-container> </ng-container>
</sqx-modal-dialog> </sqx-modal-dialog>

67
frontend/app/features/schemas/pages/schema/fields/field.component.html

@ -9,15 +9,30 @@
<span class="field-name"> <span class="field-name">
<i class="field-icon icon-type-{{field.properties.fieldType}}"></i> <i class="field-icon icon-type-{{field.properties.fieldType}}"></i>
<span [class.field-hidden]="field.isHidden" title="{{field.isHidden ? 'Hidden Field' : 'Visible Field'}}">{{field.displayName}}</span> <ng-container *ngIf="field.isHidden else visible">
<span class="field-partitioning ml-2" *ngIf="field['isLocalizable']">localizable</span> <span class="field-hidden" title="i18n:schemas.field.hiddenMarker">{{field.displayName}}</span>
</ng-container>
<ng-template #visible>
<span title="schema.field.visibleMarker" title="i18n:schema.field.visibleMarker">{{field.displayName}}</span>
</ng-template>
<span class="field-partitioning ml-2" *ngIf="field['isLocalizable']">{{ 'schemas.field.localizableMarker' | sqxTranslate }}</span>
</span> </span>
</div> </div>
<div class="col col-tags"> <div class="col col-tags">
<div class="float-right"> <div class="float-right">
<span class="ml-1 badge badge-pill badge-danger" *ngIf="field.isLocked">Locked</span> <span class="ml-1 badge badge-pill badge-danger" *ngIf="field.isLocked">
<span class="ml-1 badge badge-pill badge-success" *ngIf="!field.isDisabled">Enabled</span> {{ 'schemas.field.lockedMarker' | sqxTranslate }}
<span class="ml-1 badge badge-pill badge-danger" *ngIf="field.isDisabled">Disabled</span> </span>
<span class="ml-1 badge badge-pill badge-success" *ngIf="!field.isDisabled">
{{ 'schemas.field.enabledMarker' | sqxTranslate }}
</span>
<span class="ml-1 badge badge-pill badge-danger" *ngIf="field.isDisabled">
{{ 'schemas.field.disabledMarker' | sqxTranslate }}
</span>
</div> </div>
</div> </div>
<div class="col col-options"> <div class="col col-options">
@ -35,39 +50,38 @@
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade> <div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" @fade>
<ng-container *ngIf="field.properties.isContentField"> <ng-container *ngIf="field.properties.isContentField">
<a class="dropdown-item" (click)="enableField()" *ngIf="field.canEnable"> <a class="dropdown-item" (click)="enableField()" *ngIf="field.canEnable">
Enable in UI {{ 'schemas.field.enable' | sqxTranslate }}
</a> </a>
<a class="dropdown-item" (click)="disableField()" *ngIf="field.canDisable"> <a class="dropdown-item" (click)="disableField()" *ngIf="field.canDisable">
Disable in UI {{ 'schemas.field.disable' | sqxTranslate }}
</a> </a>
<a class="dropdown-item" (click)="hideField()" *ngIf="field.canHide"> <a class="dropdown-item" (click)="hideField()" *ngIf="field.canHide">
Hide in API {{ 'schemas.field.hide' | sqxTranslate }}
</a> </a>
<a class="dropdown-item" (click)="showField()" *ngIf="field.canShow"> <a class="dropdown-item" (click)="showField()" *ngIf="field.canShow">
Show in API {{ 'schemas.field.show' | sqxTranslate }}
</a> </a>
</ng-container> </ng-container>
<ng-container *ngIf="field.canLock"> <ng-container *ngIf="field.canLock">
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" <a class="dropdown-item"
(sqxConfirmClick)="lockField()" (sqxConfirmClick)="lockField()"
confirmTitle="Lock field" confirmTitle="i18n:schemas.field.lockConfirmText"
confirmText="WARNING: Locking a field cannot be undone! Locked field definitions cannot be unlocked, deleted, or changed anymore. Do you really want to lock this field?"> confirmText="i18n:schemas.field.lockConfirmText">
Lock and prevent changes {{ 'schemas.field.lock' | sqxTranslate }}
</a> </a>
</ng-container> </ng-container>
<ng-container> <ng-container>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete" [class.disabled]="!field.canDelete"
[class.disabled]="!field.canDelete"
(sqxConfirmClick)="deleteField()" (sqxConfirmClick)="deleteField()"
confirmTitle="Delete field" confirmTitle="i18n:schemas.field.deleteConfirmTitle"
confirmText="Do you really want to delete the field?"> confirmText="i18n:schemas.field.deleteConfirmText">
Delete {{ 'common.delete' | sqxTranslate }}
</a> </a>
</ng-container> </ng-container>
</div> </div>
@ -80,12 +94,7 @@
<div class="table-items-row-details" *ngIf="isEditing"> <div class="table-items-row-details" *ngIf="isEditing">
<form [formGroup]="editForm.form" (ngSubmit)="save()"> <form [formGroup]="editForm.form" (ngSubmit)="save()">
<sqx-field-form showButtons="true" <sqx-field-form showButtons="true" (cancel)="toggleEditing()" [patterns]="patterns" [fieldForm]="editForm.form" [field]="field" [isEditable]="isEditable">
(cancel)="toggleEditing()"
[patterns]="patterns"
[fieldForm]="editForm.form"
[field]="field"
[isEditable]="isEditable">
</sqx-field-form> </sqx-field-form>
</form> </form>
</div> </div>
@ -99,10 +108,7 @@
[cdkDropListDisabled]="!isEditable" [cdkDropListDisabled]="!isEditable"
[cdkDropListData]="nested" [cdkDropListData]="nested"
(cdkDropListDropped)="sortFields($event)"> (cdkDropListDropped)="sortFields($event)">
<div *ngFor="let nested of nested; trackBy: trackByFieldFn" <div *ngFor="let nested of nested; trackBy: trackByFieldFn" class="nested-field table-drag" cdkDrag cdkDragLockAxis="y">
class="nested-field table-drag"
cdkDrag
cdkDragLockAxis="y">
<span class="nested-field-line-h"></span> <span class="nested-field-line-h"></span>
@ -116,13 +122,12 @@
<span class="nested-field-line-h"></span> <span class="nested-field-line-h"></span>
<button type="button" class="btn btn-success btn-sm" (click)="addFieldDialog.show()"> <button type="button" class="btn btn-success btn-sm" (click)="addFieldDialog.show()">
<i class="icon icon-plus"></i> Add Nested Field <i class="icon icon-plus"></i> {{ 'schemas.addNestedField' | sqxTranslate }}
</button> </button>
</div> </div>
<ng-container *sqxModal="addFieldDialog"> <ng-container *sqxModal="addFieldDialog">
<sqx-field-wizard [schema]="schema" [parent]="field" <sqx-field-wizard [schema]="schema" [parent]="field" (complete)="addFieldDialog.hide()">
(complete)="addFieldDialog.hide()">
</sqx-field-wizard> </sqx-field-wizard>
</ng-container> </ng-container>
</ng-container> </ng-container>

22
frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html

@ -1,52 +1,52 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldName">Name</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_fieldName">{{ 'common.name' | sqxTranslate }}</label>
<div class="col-7"> <div class="col-7">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldName" readonly [ngModel]="field.name" [ngModelOptions]="standalone" /> <input type="text" class="form-control" id="{{field.fieldId}}_fieldName" readonly [ngModel]="field.name" [ngModelOptions]="standalone">
<sqx-form-hint> <sqx-form-hint>
The name of the field in the API response. {{ 'schemas.field.nameHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldLabel">Label</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_fieldLabel">{{ 'common.label' | sqxTranslate }}</label>
<div class="col-7"> <div class="col-7">
<sqx-control-errors for="label"></sqx-control-errors> <sqx-control-errors for="label"></sqx-control-errors>
<input type="text" class="form-control" id="{{field.fieldId}}_fieldLabel" maxlength="100" formControlName="label" /> <input type="text" class="form-control" id="{{field.fieldId}}_fieldLabel" maxlength="100" formControlName="label">
<sqx-form-hint> <sqx-form-hint>
Display name for documentation and user interfaces. {{ 'schemas.field.labelHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldHints">Hints</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_fieldHints">{{ 'common.hints' | sqxTranslate }}</label>
<div class="col-7"> <div class="col-7">
<sqx-control-errors for="hints"></sqx-control-errors> <sqx-control-errors for="hints"></sqx-control-errors>
<input type="text" class="form-control" id="{{field.fieldId}}_fieldHints" maxlength="100" formControlName="hints" /> <input type="text" class="form-control" id="{{field.fieldId}}_fieldHints" maxlength="100" formControlName="hints">
<sqx-form-hint> <sqx-form-hint>
Describe this field for documentation and user interfaces. {{ 'schemas.field.hintsHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>
<div class="form-group row" *ngIf="field.properties.isContentField"> <div class="form-group row" *ngIf="field.properties.isContentField">
<label class="col-3 col-form-label">Tags</label> <label class="col-3 col-form-label">{{ 'common.tags' | sqxTranslate }}</label>
<div class="col-7"> <div class="col-7">
<sqx-tag-editor id="schemaTags" formControlName="tags"></sqx-tag-editor> <sqx-tag-editor id="schemaTags" formControlName="tags"></sqx-tag-editor>
<sqx-form-hint> <sqx-form-hint>
Tags to annotate your field for automation processes. {{ 'schemas.field.tagsHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>

6
frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html

@ -1,12 +1,12 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_editorUrl">Editor Url</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_editorUrl">{{ 'schemas.field.editorUrl' | sqxTranslate }}</label>
<div class="col-6"> <div class="col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl" /> <input type="text" class="form-control" id="{{field.fieldId}}_editorUrl" formControlName="editorUrl">
<sqx-form-hint> <sqx-form-hint>
Url to your plugin if you use a custom editor. {{ 'schemas.field.editorUrlHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>

4
frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html

@ -2,9 +2,9 @@
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldRequired" formControlName="isRequired" /> <input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldRequired" formControlName="isRequired">
<label class="form-check-label" for="{{field.fieldId}}_fieldRequired"> <label class="form-check-label" for="{{field.fieldId}}_fieldRequired">
Required {{ 'schemas.field.required' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>

10
frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.html

@ -1,20 +1,20 @@
<div class="table-items-row-details-tabs clearfix"> <div class="table-items-row-details-tabs clearfix">
<ul class="nav nav-tabs2"> <ul class="nav nav-tabs2">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" (click)="selectTab(0)" [class.active]="selectedTab === 0">Common</a> <a class="nav-link" (click)="selectTab(0)" [class.active]="selectedTab === 0">{{ 'schemas.field.tabCommon' | sqxTranslate }}</a>
</li> </li>
<li class="nav-item" [class.hidden]="!field.properties.isContentField"> <li class="nav-item" [class.hidden]="!field.properties.isContentField">
<a class="nav-link" (click)="selectTab(1)" [class.active]="selectedTab === 1">Validation</a> <a class="nav-link" (click)="selectTab(1)" [class.active]="selectedTab === 1">{{ 'schemas.field.tabValidation' | sqxTranslate }}</a>
</li> </li>
<li class="nav-item" [class.hidden]="!field.properties.isContentField || field.properties.fieldType === 'Array'"> <li class="nav-item" [class.hidden]="!field.properties.isContentField || field.properties.fieldType === 'Array'">
<a class="nav-link" (click)="selectTab(2)" [class.active]="selectedTab === 2">Editing</a> <a class="nav-link" (click)="selectTab(2)" [class.active]="selectedTab === 2">{{ 'schemas.field.tabEditing' | sqxTranslate }}</a>
</li> </li>
</ul> </ul>
<div class="float-right" *ngIf="showButtons"> <div class="float-right" *ngIf="showButtons">
<button [disabled]="field.isLocked" type="reset" class="btn btn-text-secondary2" (click)="cancel.emit()">Cancel</button> <button [disabled]="field.isLocked" type="reset" class="btn btn-text-secondary2" (click)="cancel.emit()">{{ 'common.cancel' | sqxTranslate }}</button>
<button [disabled]="field.isLocked" type="submit" class="btn btn-primary ml-1" *ngIf="isEditable">Save</button> <button [disabled]="field.isLocked" type="submit" class="btn btn-primary ml-1" *ngIf="isEditable">{{ 'common.save' | sqxTranslate }}</button>
</div> </div>
</div> </div>

14
frontend/app/features/schemas/pages/schema/fields/schema-fields.component.html

@ -1,8 +1,8 @@
<div class="table-items-row table-items-row-empty" *ngIf="schema && schema.fields.length === 0"> <div class="table-items-row table-items-row-empty" *ngIf="schema && schema.fields.length === 0">
No field created yet. {{ 'schemas.field.empty' | sqxTranslate }}
<button type="button" class="btn btn-success btn-sm ml-2" (click)="addFieldDialog.show()" *ngIf="schema.canAddField"> <button type="button" class="btn btn-success btn-sm ml-2" (click)="addFieldDialog.show()" *ngIf="schema.canAddField">
<i class="icon icon-plus"></i> Add Field <i class="icon icon-plus"></i> {{ 'schemas.addField' | sqxTranslate }}
</button> </button>
</div> </div>
@ -12,10 +12,7 @@
[cdkDropListDisabled]="!schema.canOrderFields" [cdkDropListDisabled]="!schema.canOrderFields"
[cdkDropListData]="schema.fields" [cdkDropListData]="schema.fields"
(cdkDropListDropped)="sortFields($event)"> (cdkDropListDropped)="sortFields($event)">
<div *ngFor="let field of schema.fields; trackBy: trackByFieldFn" <div *ngFor="let field of schema.fields; trackBy: trackByFieldFn" class="table-drag" cdkDrag cdkDragLockAxis="y">
class="table-drag"
cdkDrag
cdkDragLockAxis="y">
<sqx-field [field]="field" [schema]="schema" [patterns]="patterns"> <sqx-field [field]="field" [schema]="schema" [patterns]="patterns">
<i cdkDragHandle class="icon-drag2 drag-handle"></i> <i cdkDragHandle class="icon-drag2 drag-handle"></i>
</sqx-field> </sqx-field>
@ -23,12 +20,11 @@
</div> </div>
<button type="button" class="btn btn-success field-button" (click)="addFieldDialog.show()" *ngIf="schema.canAddField"> <button type="button" class="btn btn-success field-button" (click)="addFieldDialog.show()" *ngIf="schema.canAddField">
<i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">Add Field</div> <i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">{{ 'schemas.addField' | sqxTranslate }}</div>
</button> </button>
</ng-container> </ng-container>
<ng-container *sqxModal="addFieldDialog"> <ng-container *sqxModal="addFieldDialog">
<sqx-field-wizard [schema]="schema" <sqx-field-wizard [schema]="schema" (complete)="addFieldDialog.hide()">
(complete)="addFieldDialog.hide()">
</sqx-field-wizard> </sqx-field-wizard>
</ng-container> </ng-container>

6
frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html

@ -1,14 +1,14 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Items</label> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.array.count' | sqxTranslate }}</label>
<div class="col-3 minmax-col"> <div class="col-3 minmax-col">
<input type="number" class="form-control" formControlName="minItems" placeholder="Min Items" /> <input type="number" class="form-control" formControlName="minItems" placeholder="{{ 'schemas.fieldTypes.array.countMin' | sqxTranslate }}">
<label class="col-form-label minmax-label">-</label> <label class="col-form-label minmax-label">-</label>
</div> </div>
<div class="col-3"> <div class="col-3">
<input type="number" class="form-control" formControlName="maxItems" placeholder="Max Items" /> <input type="number" class="form-control" formControlName="maxItems" placeholder="{{ 'schemas.fieldTypes.array.countMax' | sqxTranslate }}">
</div> </div>
</div> </div>
</div> </div>

16
frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html

@ -1,17 +1,17 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_previewMode">PreviewMode</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_previewMode">{{ 'schemas.fieldTypes.assets.previewMode' | sqxTranslate }}</label>
<div class="col-6"> <div class="col-6">
<select type="text" class="form-control" id="{{field.fieldId}}_previewMode" formControlName="previewMode"> <select type="text" class="form-control" id="{{field.fieldId}}_previewMode" formControlName="previewMode">
<option value="ImageAndFileName">Thumbnail and file name</option> <option value="ImageAndFileName">{{ 'schemas.fieldTypes.assets.previewModeTumbnailName' | sqxTranslate }}</option>
<option value="Image">Only thumbnail or file name if not an image</option> <option value="Image">{{ 'schemas.fieldTypes.assets.previewModeTumbnailOrName' | sqxTranslate }}</option>
<option value="FileName">Only file name</option> <option value="FileName">{{ 'schemas.fieldTypes.assets.previewModeName' | sqxTranslate }}</option>
</select> </select>
<sqx-form-hint> <sqx-form-hint>
The preview mode for assets in content lists. {{ 'schemas.fieldTypes.assets.previewModeHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>
@ -19,14 +19,14 @@
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldResolveFirst" formControlName="resolveFirst" /> <input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldResolveFirst" formControlName="resolveFirst">
<label class="form-check-label" for="{{field.fieldId}}_fieldResolveFirst"> <label class="form-check-label" for="{{field.fieldId}}_fieldResolveFirst">
Resolve first asset {{ 'schemas.fieldTypes.assets.resolve' | sqxTranslate }}
</label> </label>
</div> </div>
<sqx-form-hint> <sqx-form-hint>
Show the first referenced asset in the content list. {{ 'schemas.fieldTypes.assets.resolveHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>

42
frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html

@ -1,54 +1,54 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Items</label> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.assets.count' | sqxTranslate }}</label>
<div class="col-3 minmax-col"> <div class="col-3 minmax-col">
<input type="number" class="form-control" formControlName="minItems" placeholder="Min Assets" /> <input type="number" class="form-control" formControlName="minItems" placeholder="{{ 'schemas.fieldTypes.assets.countMin' | sqxTranslate }}">
<label class="col-form-label minmax-label">-</label> <label class="col-form-label minmax-label">-</label>
</div> </div>
<div class="col-3"> <div class="col-3">
<input type="number" class="form-control" formControlName="maxItems" placeholder="Max Assets" /> <input type="number" class="form-control" formControlName="maxItems" placeholder="{{ 'schemas.fieldTypes.assets.countMax' | sqxTranslate }}">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Size</label> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.assets.size' | sqxTranslate }}</label>
<div class="col-3 minmax-col"> <div class="col-3 minmax-col">
<input type="number" class="form-control" formControlName="minSize" placeholder="Min Size" /> <input type="number" class="form-control" formControlName="minSize" placeholder="{{ 'schemas.fieldTypes.assets.sizeMin' | sqxTranslate }}">
<label class="col-form-label minmax-label">-</label> <label class="col-form-label minmax-label">-</label>
</div> </div>
<div class="col-3"> <div class="col-3">
<input type="number" class="form-control" formControlName="maxSize" placeholder="Max Size" /> <input type="number" class="form-control" formControlName="maxSize" placeholder="{{ 'schemas.fieldTypes.assets.sizeMax' | sqxTranslate }}">
</div> </div>
<div class="col-3"> <div class="col-3">
<label class="col-form-label">bytes</label> <label class="col-form-label">{{ 'common.bytes' | sqxTranslate }}</label>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldMustBeImage" formControlName="mustBeImage" /> <input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldMustBeImage" formControlName="mustBeImage">
<label class="form-check-label" for="{{field.fieldId}}_fieldMustBeImage"> <label class="form-check-label" for="{{field.fieldId}}_fieldMustBeImage">
Must be Image {{ 'schemas.fieldTypes.assets.mustBeImage' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Width</label> <label class="col-3 col-form-label">{{ 'common.width' | sqxTranslate }}</label>
<div class="col-2 minmax-col"> <div class="col-2 minmax-col">
<input type="number" class="form-control" formControlName="minWidth" /> <input type="number" class="form-control" formControlName="minWidth">
<label class="col-form-label minmax-label">-</label> <label class="col-form-label minmax-label">-</label>
</div> </div>
<div class="col-2"> <div class="col-2">
<input type="number" class="form-control" formControlName="maxWidth" /> <input type="number" class="form-control" formControlName="maxWidth">
</div> </div>
<div class="col-2"> <div class="col-2">
<label class="col-form-label">px</label> <label class="col-form-label">px</label>
@ -56,15 +56,15 @@
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Height</label> <label class="col-3 col-form-label">{{ 'common.height' | sqxTranslate }}</label>
<div class="col-2 minmax-col"> <div class="col-2 minmax-col">
<input type="number" class="form-control" formControlName="minHeight" /> <input type="number" class="form-control" formControlName="minHeight">
<label class="col-form-label minmax-label">-</label> <label class="col-form-label minmax-label">-</label>
</div> </div>
<div class="col-2"> <div class="col-2">
<input type="number" class="form-control" formControlName="maxHeight" /> <input type="number" class="form-control" formControlName="maxHeight">
</div> </div>
<div class="col-2"> <div class="col-2">
<label class="col-form-label">px</label> <label class="col-form-label">px</label>
@ -72,15 +72,15 @@
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">AspectRatio</label> <label class="col-3 col-form-label">{{ 'common.aspectRatio' | sqxTranslate }}</label>
<div class="col-2 minmax-col"> <div class="col-2 minmax-col">
<input type="number" class="form-control" formControlName="aspectWidth" placeholder="4" /> <input type="number" class="form-control" formControlName="aspectWidth" placeholder="4">
<label class="col-form-label minmax-label">:</label> <label class="col-form-label minmax-label">:</label>
</div> </div>
<div class="col-2"> <div class="col-2">
<input type="number" class="form-control" formControlName="aspectHeight" placeholder="3" /> <input type="number" class="form-control" formControlName="aspectHeight" placeholder="3">
</div> </div>
<div class="col-2"> <div class="col-2">
<label class="col-form-label">px</label> <label class="col-form-label">px</label>
@ -90,9 +90,9 @@
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldAllowDuplicates" formControlName="allowDuplicates" /> <input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldAllowDuplicates" formControlName="allowDuplicates">
<label class="form-check-label" for="{{field.fieldId}}_fieldAllowDuplicates"> <label class="form-check-label" for="{{field.fieldId}}_fieldAllowDuplicates">
Allow duplicate values {{ 'schemas.fieldTypes.assets.allowDuplicates' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>
@ -100,7 +100,7 @@
<div class="form-group2 row"> <div class="form-group2 row">
<label class="col-3 col-form-label"> <label class="col-3 col-form-label">
File Extensions {{ 'schemas.fieldTypes.assets.fileExtensions' | sqxTranslate }}
</label> </label>
<div class="col-6"> <div class="col-6">

16
frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html

@ -1,28 +1,28 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">{{ 'schemas.field.placeholder' | sqxTranslate }}</label>
<div class="col-6"> <div class="col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder" /> <input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<sqx-form-hint> <sqx-form-hint>
Define the placeholder for the input control. {{ 'schemas.field.placeholderHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Editor</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Checkbox'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Checkbox'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Checkbox" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="Checkbox">
<i class="icon-control-Checkbox"></i> <i class="icon-control-Checkbox"></i>
<span class="radio-label">Checkbox</span> <span class="radio-label">Checkbox</span>
</label> </label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Toggle'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Toggle'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Toggle" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="Toggle">
<i class="icon-control-Toggle"></i> <i class="icon-control-Toggle"></i>
@ -33,9 +33,9 @@
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldInlineEditable" formControlName="inlineEditable" /> <input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldInlineEditable" formControlName="inlineEditable">
<label class="form-check-label" for="{{field.fieldId}}_fieldInlineEditable"> <label class="form-check-label" for="{{field.fieldId}}_fieldInlineEditable">
Inline Editable {{ 'schemas.field.inlineEditable' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>

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

@ -4,7 +4,7 @@
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldDefaultValue" formControlName="defaultValue" sqxIndeterminateValue /> <input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldDefaultValue" formControlName="defaultValue" sqxIndeterminateValue />
<label class="form-check-label" for="{{field.fieldId}}_fieldDefaultValue"> <label class="form-check-label" for="{{field.fieldId}}_fieldDefaultValue">
Default Value {{ 'schemas.field.defaultValue' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>

10
frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html

@ -3,26 +3,26 @@
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label>
<div class="col-6"> <div class="col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder" /> <input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<sqx-form-hint> <sqx-form-hint>
Define the placeholder for the input control. {{ 'schemas.field.placeholderHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Editor</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Date'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Date'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Date" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="Date">
<i class="icon-control-Date"></i> <i class="icon-control-Date"></i>
<span class="radio-label">Date</span> <span class="radio-label">Date</span>
</label> </label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'DateTime'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'DateTime'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="DateTime" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="DateTime">
<i class="icon-control-DateTime"></i> <i class="icon-control-DateTime"></i>

8
frontend/app/features/schemas/pages/schema/fields/types/date-time-validation.component.html

@ -1,6 +1,6 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Min Value</label> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.dateTime.rangeMin' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<sqx-date-time-editor enforceTime="true" mode="DateTime" formControlName="minValue"></sqx-date-time-editor> <sqx-date-time-editor enforceTime="true" mode="DateTime" formControlName="minValue"></sqx-date-time-editor>
@ -8,7 +8,7 @@
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Max Value</label> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.dateTime.rangeMax' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<sqx-date-time-editor enforceTime="true" mode="DateTime" formControlName="maxValue"></sqx-date-time-editor> <sqx-date-time-editor enforceTime="true" mode="DateTime" formControlName="maxValue"></sqx-date-time-editor>
@ -17,7 +17,7 @@
<ng-container *ngIf="showDefaultValues | async"> <ng-container *ngIf="showDefaultValues | async">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Default Mode</label> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.dateTime.defaultMode' | sqxTranslate }}</label>
<div class="col-3"> <div class="col-3">
<select class="form-control" formControlName="calculatedDefaultValue"> <select class="form-control" formControlName="calculatedDefaultValue">
@ -28,7 +28,7 @@
</div> </div>
<div class="form-group row" *ngIf="showDefaultValue | async"> <div class="form-group row" *ngIf="showDefaultValue | async">
<label class="col-3 col-form-label">Default Value</label> <label class="col-3 col-form-label">{{ 'schemas.field.defaultValue' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<sqx-date-time-editor enforceTime="true" mode="DateTime" formControlName="defaultValue"></sqx-date-time-editor> <sqx-date-time-editor enforceTime="true" mode="DateTime" formControlName="defaultValue"></sqx-date-time-editor>

4
frontend/app/features/schemas/pages/schema/fields/types/geolocation-ui.component.html

@ -1,10 +1,10 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Editor</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Map'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Map'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Map" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="Map">
<i class="icon-control-Map"></i> <i class="icon-control-Map"></i>

22
frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html

@ -1,42 +1,42 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">Placeholder</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_fieldPlaceholder">{{ 'schemas.field.placeholder' | sqxTranslate }}</label>
<div class="col-6"> <div class="col-6">
<input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder" /> <input type="text" class="form-control" id="{{field.fieldId}}_fieldPlaceholder" maxlength="100" formControlName="placeholder">
<sqx-form-hint> <sqx-form-hint>
Define the placeholder for the input control. {{ 'schemas.field.placeholderHint' | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Editor</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Input'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Input'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Input" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="Input">
<i class="icon-control-Input"></i> <i class="icon-control-Input"></i>
<span class="radio-label">Input</span> <span class="radio-label">Input</span>
</label> </label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Dropdown'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Dropdown'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Dropdown" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="Dropdown">
<i class="icon-control-Dropdown"></i> <i class="icon-control-Dropdown"></i>
<span class="radio-label" clas>Dropdown</span> <span class="radio-label" clas>Dropdown</span>
</label> </label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Radio'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Radio'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Radio" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="Radio">
<i class="icon-control-Radio"></i> <i class="icon-control-Radio"></i>
<span class="radio-label">Radio</span> <span class="radio-label">Radio</span>
</label> </label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Stars'"> <label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Stars'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Stars" /> <input type="radio" class="radio-input" name="editor" formControlName="editor" value="Stars">
<i class="icon-control-Stars"></i> <i class="icon-control-Stars"></i>
@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div class="form-group row" [class.hidden]="hideAllowedValues | async"> <div class="form-group row" [class.hidden]="hideAllowedValues | async">
<label class="col-3 col-form-label">Allowed Values</label> <label class="col-3 col-form-label">{{ 'schemas.field.allowedValues' | sqxTranslate }}</label>
<div class="col-6"> <div class="col-6">
<sqx-tag-editor formControlName="allowedValues" [converter]="converter"></sqx-tag-editor> <sqx-tag-editor formControlName="allowedValues" [converter]="converter"></sqx-tag-editor>
@ -54,9 +54,9 @@
<div class="form-group row" [class.hidden]="hideInlineEditable | async"> <div class="form-group row" [class.hidden]="hideInlineEditable | async">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldInlineEditable" formControlName="inlineEditable" /> <input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldInlineEditable" formControlName="inlineEditable">
<label class="form-check-label" for="{{field.fieldId}}_fieldInlineEditable"> <label class="form-check-label" for="{{field.fieldId}}_fieldInlineEditable">
Inline Editable {{ 'schemas.field.inlineEditable' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>

14
frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.html

@ -2,32 +2,32 @@
<div class="form-group row" *ngIf="showUnique"> <div class="form-group row" *ngIf="showUnique">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldUnique" formControlName="isUnique" /> <input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldUnique" formControlName="isUnique">
<label class="form-check-label" for="{{field.fieldId}}_fieldUnique"> <label class="form-check-label" for="{{field.fieldId}}_fieldUnique">
Unique {{ 'schemas.field.unique' | sqxTranslate }}
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">Range</label> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.number.range' | sqxTranslate }}</label>
<div class="col-3 minmax-col"> <div class="col-3 minmax-col">
<input type="number" class="form-control" formControlName="minValue" placeholder="Min Value" /> <input type="number" class="form-control" formControlName="minValue" placeholder="{{ 'schemas.fieldTypes.number.rangeMin' | sqxTranslate }}">
<label class="col-form-label minmax-label">-</label> <label class="col-form-label minmax-label">-</label>
</div> </div>
<div class="col-3"> <div class="col-3">
<input type="number" class="form-control" formControlName="maxValue" placeholder="Max Value" /> <input type="number" class="form-control" formControlName="maxValue" placeholder="{{ 'schemas.fieldTypes.number.rangeMax' | sqxTranslate }}">
</div> </div>
</div> </div>
<div class="form-group row" *ngIf="showDefaultValue | async"> <div class="form-group row" *ngIf="showDefaultValue | async">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldDefaultValue">Default Value</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_fieldDefaultValue">{{ 'schemas.field.defaultValue' | sqxTranslate }}</label>
<div class="col-6"> <div class="col-6">
<input type="number" class="form-control" id="{{field.fieldId}}_fieldDefaultValue" formControlName="defaultValue" /> <input type="number" class="form-control" id="{{field.fieldId}}_fieldDefaultValue" formControlName="defaultValue">
</div> </div>
</div> </div>
</div> </div>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save