Browse Source

Feature/settings (#686)

* First settings.

* Migrations.

* Angular update.

* Type safe templates

* Type safe templates

* Backend Tests and minor fixes.

* Some tests.

* Cleanup and tests.

* Fix tests for title component.

* Remove patterns test.

* Tests for settings.
pull/687/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
d43abcc8c9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
  2. 16
      backend/i18n/frontend_en.json
  3. 16
      backend/i18n/frontend_it.json
  4. 16
      backend/i18n/frontend_nl.json
  5. 6
      backend/i18n/source/backend_en.json
  6. 5
      backend/i18n/source/backend_it.json
  7. 5
      backend/i18n/source/backend_nl.json
  8. 16
      backend/i18n/source/frontend_en.json
  9. 6
      backend/i18n/source/frontend_it.json
  10. 6
      backend/i18n/source/frontend_nl.json
  11. 8
      backend/src/Migrations/MigrationPath.cs
  12. 112
      backend/src/Migrations/Migrations/CreateAppSettings.cs
  13. 5
      backend/src/Migrations/OldEvents/AppPatternAdded.cs
  14. 5
      backend/src/Migrations/OldEvents/AppPatternDeleted.cs
  15. 5
      backend/src/Migrations/OldEvents/AppPatternUpdated.cs
  16. 71
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  17. 28
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppSettings.cs
  18. 15
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs
  19. 33
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsSurrogate.cs
  20. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs
  21. 26
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  22. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  23. 12
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  24. 28
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  25. 3
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs
  26. 27
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs
  27. 14
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs
  28. 52
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs
  29. 81
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs
  30. 66
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs
  31. 109
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppPatterns.cs
  32. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
  33. 25
      backend/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs
  34. 8
      backend/src/Squidex.Domain.Apps.Entities/Apps/InitialSettings.cs
  35. 32
      backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs
  36. 3
      backend/src/Squidex.Domain.Apps.Entities/Context.cs
  37. 2
      backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs
  38. 18
      backend/src/Squidex.Domain.Apps.Events/Apps/AppSettingsUpdated.cs
  39. 12
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
  40. 36
      backend/src/Squidex.Shared/PermissionExtensions.cs
  41. 101
      backend/src/Squidex.Shared/Permissions.cs
  42. 18
      backend/src/Squidex.Shared/Texts.it.resx
  43. 18
      backend/src/Squidex.Shared/Texts.nl.resx
  44. 18
      backend/src/Squidex.Shared/Texts.resx
  45. 109
      backend/src/Squidex.Web/Resources.cs
  46. 151
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  47. 84
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  48. 47
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  49. 74
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs
  50. 26
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/EditorDto.cs
  51. 31
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs
  52. 47
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs
  53. 54
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppSettingsDto.cs
  54. 44
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs
  55. 2
      backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs
  56. 1
      backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
  57. 15
      backend/src/Squidex/Config/Domain/AppsServices.cs
  58. 3
      backend/src/Squidex/Config/Domain/MigrationServices.cs
  59. 1
      backend/src/Squidex/Config/Domain/SerializationServices.cs
  60. 4
      backend/src/Squidex/appsettings.json
  61. 39
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs
  62. 86
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs
  63. 8
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs
  64. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs
  65. 17
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs
  66. 112
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs
  67. 192
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppPatternsTests.cs
  68. 121
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs
  69. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs
  70. 72
      backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs
  71. 2
      backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
  72. 2
      backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj
  73. 4
      backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
  74. 7
      frontend/app-config/webpack.config.js
  75. 2
      frontend/app/app.component.ts
  76. 2
      frontend/app/features/administration/pages/cluster/cluster-page.component.html
  77. 2
      frontend/app/features/administration/pages/event-consumers/event-consumer.component.html
  78. 4
      frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  79. 2
      frontend/app/features/administration/pages/users/user-page.component.html
  80. 2
      frontend/app/features/administration/pages/users/user-page.component.ts
  81. 4
      frontend/app/features/administration/pages/users/users-page.component.html
  82. 14
      frontend/app/features/administration/services/event-consumers.service.spec.ts
  83. 18
      frontend/app/features/administration/services/users.service.spec.ts
  84. 18
      frontend/app/features/administration/services/users.service.ts
  85. 2
      frontend/app/features/administration/state/users.state.ts
  86. 2
      frontend/app/features/api/pages/graphql/graphql-page.component.html
  87. 2
      frontend/app/features/apps/pages/onboarding-dialog.component.ts
  88. 6
      frontend/app/features/assets/pages/assets-filters-page.component.html
  89. 6
      frontend/app/features/assets/pages/assets-page.component.html
  90. 2
      frontend/app/features/content/pages/comments/comments-page.component.html
  91. 16
      frontend/app/features/content/pages/content/content-history-page.component.html
  92. 4
      frontend/app/features/content/pages/content/content-history-page.component.ts
  93. 6
      frontend/app/features/content/pages/content/content-page.component.html
  94. 2
      frontend/app/features/content/pages/content/content-page.component.ts
  95. 2
      frontend/app/features/content/pages/content/editor/content-editor.component.ts
  96. 2
      frontend/app/features/content/pages/content/editor/content-field.component.html
  97. 4
      frontend/app/features/content/pages/content/editor/content-field.component.ts
  98. 8
      frontend/app/features/content/pages/content/editor/content-section.component.html
  99. 4
      frontend/app/features/content/pages/content/editor/content-section.component.ts
  100. 2
      frontend/app/features/content/pages/content/editor/field-languages.component.html

8
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs

@ -79,12 +79,12 @@ namespace Squidex.Extensions.Actions.Webhook
if (indexEqual > 0 && indexEqual < line.Length - 1) if (indexEqual > 0 && indexEqual < line.Length - 1)
{ {
var key = line.Substring(0, indexEqual); var headerKey = line.Substring(0, indexEqual);
var val = line[(indexEqual + 1)..]; var headerValue = line[(indexEqual + 1)..];
val = await FormatAsync(val, @event); headerValue = await FormatAsync(headerValue, @event);
headersDictionary[key] = val; headersDictionary[headerKey] = headerValue;
} }
} }

16
backend/i18n/frontend_en.json

@ -52,6 +52,11 @@
"apps.uploadImageTooBig": "App image is too big.", "apps.uploadImageTooBig": "App image is too big.",
"apps.welcomeSubtitle": "Welcome to Squidex.", "apps.welcomeSubtitle": "Welcome to Squidex.",
"apps.welcomeTitle": "Hi {user}", "apps.welcomeTitle": "Hi {user}",
"appSettings.hideScheduler": "Hide dialog for scheduled publishing",
"appSettings.refreshTooltip": "Refresh UI Settings (CTRL + SHIFT + R)",
"appSettings.reloaded": "UI Settings reloaded.",
"appSettings.title": "UI Settings",
"appSettings.updateFailed": "Failed to update UI settings. Please reload.",
"assets.createFolder": "Create Folder", "assets.createFolder": "Create Folder",
"assets.createFolderFailed": "Failed to create asset folder. Please reload.", "assets.createFolderFailed": "Failed to create asset folder. Please reload.",
"assets.createFolderTooltip": "Create new folder (CTRL + SHIFT + G)", "assets.createFolderTooltip": "Create new folder (CTRL + SHIFT + G)",
@ -520,6 +525,10 @@
"dashboard.trafficSummaryCard": "API Traffic Summary", "dashboard.trafficSummaryCard": "API Traffic Summary",
"dashboard.welcomeText": "Welcome to **{app}** dashboard.", "dashboard.welcomeText": "Welcome to **{app}** dashboard.",
"dashboard.welcomeTitle": "Hi {user}", "dashboard.welcomeTitle": "Hi {user}",
"editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
"editors.deleteConfirmTitle": "Delete Editor URL",
"editors.empty": "No Editor URL created yet.",
"editors.title": "Custom Editors",
"eventConsumers.count": "Count", "eventConsumers.count": "Count",
"eventConsumers.loadFailed": "Failed to load event consumers. Please reload.", "eventConsumers.loadFailed": "Failed to load event consumers. Please reload.",
"eventConsumers.pageTitle": "Event Consumers", "eventConsumers.pageTitle": "Event Consumers",
@ -553,13 +562,8 @@
"notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.", "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.deleteConfirmText": "Do you really want to remove this pattern?",
"patterns.deleteConfirmTitle": "Delete pattern", "patterns.deleteConfirmTitle": "Delete pattern",
"patterns.deleteFailed": "Failed to remove pattern. Please reload.",
"patterns.empty": "No pattern created yet.", "patterns.empty": "No pattern created yet.",
"patterns.loadFailed": "Failed to add pattern. Please reload.", "patterns.title": "Patterns",
"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.billingPortal": "Billing Portal",
"plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.", "plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.",
"plans.change": "Change", "plans.change": "Change",

16
backend/i18n/frontend_it.json

@ -52,6 +52,11 @@
"apps.uploadImageTooBig": "L'immagine dell'app è troppo grande.", "apps.uploadImageTooBig": "L'immagine dell'app è troppo grande.",
"apps.welcomeSubtitle": "Benvenuto su Squidex.", "apps.welcomeSubtitle": "Benvenuto su Squidex.",
"apps.welcomeTitle": "Ciao {user}", "apps.welcomeTitle": "Ciao {user}",
"appSettings.hideScheduler": "Hide dialog for scheduled publishing",
"appSettings.refreshTooltip": "Refresh UI Settings (CTRL + SHIFT + R)",
"appSettings.reloaded": "UI Settings reloaded.",
"appSettings.title": "UI Settings",
"appSettings.updateFailed": "Failed to update UI settings. Please reload.",
"assets.createFolder": "Crea cartella", "assets.createFolder": "Crea cartella",
"assets.createFolderFailed": "Non è stato possibile creare la cartella degli asset. Per favore ricarica.", "assets.createFolderFailed": "Non è stato possibile creare la cartella degli asset. Per favore ricarica.",
"assets.createFolderTooltip": "Crea una nuova cartella (CTRL + SHIFT + G)", "assets.createFolderTooltip": "Crea una nuova cartella (CTRL + SHIFT + G)",
@ -520,6 +525,10 @@
"dashboard.trafficSummaryCard": "Riepilogo del traffico delle API", "dashboard.trafficSummaryCard": "Riepilogo del traffico delle API",
"dashboard.welcomeText": "Benvenuto sulla dashboard **{app}**.", "dashboard.welcomeText": "Benvenuto sulla dashboard **{app}**.",
"dashboard.welcomeTitle": "Ciao {user}", "dashboard.welcomeTitle": "Ciao {user}",
"editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
"editors.deleteConfirmTitle": "Delete Editor URL",
"editors.empty": "No Editor URL created yet.",
"editors.title": "Custom Editors",
"eventConsumers.count": "Conteggio", "eventConsumers.count": "Conteggio",
"eventConsumers.loadFailed": "Non è stato possibile caricare event consumers. Per favore ricarica.", "eventConsumers.loadFailed": "Non è stato possibile caricare event consumers. Per favore ricarica.",
"eventConsumers.pageTitle": "Eventi degli utenti", "eventConsumers.pageTitle": "Eventi degli utenti",
@ -553,13 +562,8 @@
"notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.", "notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.",
"patterns.deleteConfirmText": "Sei sicuro di voler rimuovere il pattern?", "patterns.deleteConfirmText": "Sei sicuro di voler rimuovere il pattern?",
"patterns.deleteConfirmTitle": "Cancella il pattern", "patterns.deleteConfirmTitle": "Cancella il pattern",
"patterns.deleteFailed": "Non è stato possibile rimuovere il pattern. Per favore ricarica.",
"patterns.empty": "Nessun pattern è stato ancora creato.", "patterns.empty": "Nessun pattern è stato ancora creato.",
"patterns.loadFailed": "Non è stato possibile aggiungere il pattern. Per favore ricarica.", "patterns.title": "Patterns",
"patterns.nameValidationMessage": "Il nome può contenere solo lettere, numeri, trattini e spazi.",
"patterns.refreshTooltip": "Aggiorna i pattern (CTRL + SHIFT + R)",
"patterns.reloaded": "Pattern ricaricati.",
"patterns.updateFailed": "Non è stato possibile aggiornare pattern. Per favore ricarica.",
"plans.billingPortal": "Portale di fatturazione", "plans.billingPortal": "Portale di fatturazione",
"plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.", "plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.",
"plans.change": "Cambia", "plans.change": "Cambia",

16
backend/i18n/frontend_nl.json

@ -52,6 +52,11 @@
"apps.uploadImageTooBig": "App-afbeelding is te groot.", "apps.uploadImageTooBig": "App-afbeelding is te groot.",
"apps.welcomeSubtitle": "Welkom bij Squidex.", "apps.welcomeSubtitle": "Welkom bij Squidex.",
"apps.welcomeTitle": "Hallo {user}", "apps.welcomeTitle": "Hallo {user}",
"appSettings.hideScheduler": "Hide dialog for scheduled publishing",
"appSettings.refreshTooltip": "Refresh UI Settings (CTRL + SHIFT + R)",
"appSettings.reloaded": "UI Settings reloaded.",
"appSettings.title": "UI Settings",
"appSettings.updateFailed": "Failed to update UI settings. Please reload.",
"assets.createFolder": "Map maken", "assets.createFolder": "Map maken",
"assets.createFolderFailed": "Maken van een map is mislukt. Laad opnieuw.", "assets.createFolderFailed": "Maken van een map is mislukt. Laad opnieuw.",
"assets.createFolderTooltip": "Nieuwe map maken (CTRL + SHIFT + G)", "assets.createFolderTooltip": "Nieuwe map maken (CTRL + SHIFT + G)",
@ -520,6 +525,10 @@
"dashboard.trafficSummaryCard": "API Verkeer Samenvatting", "dashboard.trafficSummaryCard": "API Verkeer Samenvatting",
"dashboard.welcomeText": "Welkom bij **{app}** dashboard.", "dashboard.welcomeText": "Welkom bij **{app}** dashboard.",
"dashboard.welcomeTitle": "Hallo {user}", "dashboard.welcomeTitle": "Hallo {user}",
"editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
"editors.deleteConfirmTitle": "Delete Editor URL",
"editors.empty": "No Editor URL created yet.",
"editors.title": "Custom Editors",
"eventConsumers.count": "Tellen", "eventConsumers.count": "Tellen",
"eventConsumers.loadFailed": "Kan gebeurtenisgebruikers niet laden. Laad opnieuw.", "eventConsumers.loadFailed": "Kan gebeurtenisgebruikers niet laden. Laad opnieuw.",
"eventConsumers.pageTitle": "Evenementconsumenten", "eventConsumers.pageTitle": "Evenementconsumenten",
@ -553,13 +562,8 @@
"notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.", "notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.",
"patterns.deleteConfirmText": "Wil je dit patroon echt verwijderen?", "patterns.deleteConfirmText": "Wil je dit patroon echt verwijderen?",
"patterns.deleteConfirmTitle": "Verwijder patroon", "patterns.deleteConfirmTitle": "Verwijder patroon",
"patterns.deleteFailed": "Verwijderen van patroon is mislukt. Laad opnieuw.",
"patterns.empty": "Nog geen patroon gemaakt.", "patterns.empty": "Nog geen patroon gemaakt.",
"patterns.loadFailed": "Toevoegen van patroon is mislukt. Laad opnieuw.", "patterns.title": "Patterns",
"patterns.nameValidationMessage": "Naam mag alleen letters, cijfers, streepjes en spaties bevatten.",
"patterns.refreshTooltip": "Ververs patronen (CTRL + SHIFT + R)",
"patterns.reloaded": "Patronen herladen.",
"patterns.updateFailed": "Bijwerken van patroon is mislukt. Laad opnieuw.",
"plans.billingPortal": "Factureringsportal", "plans.billingPortal": "Factureringsportal",
"plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.", "plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.",
"plans.change": "Wijzigen", "plans.change": "Wijzigen",

6
backend/i18n/source/backend_en.json

@ -18,8 +18,6 @@
"apps.languages.masterLanguageNotRemovable": "Master language cannot be removed.", "apps.languages.masterLanguageNotRemovable": "Master language cannot be removed.",
"apps.nameAlreadyExists": "An app with the same name already exists.", "apps.nameAlreadyExists": "An app with the same name already exists.",
"apps.notImage": "File is not an image", "apps.notImage": "File is not an image",
"apps.patterns.nameAlreadyExists": "A pattern with the same name already exists.",
"apps.patterns.patternAlreadyExists": "This pattern already exists but with another name.",
"apps.plans.notFound": "A plan with this id does not exist.", "apps.plans.notFound": "A plan with this id does not exist.",
"apps.plans.notPlanOwner": "Plan can only changed from the user who configured the plan initially.", "apps.plans.notPlanOwner": "Plan can only changed from the user who configured the plan initially.",
"apps.roles.defaultRoleNotRemovable": "Cannot delete a default role.", "apps.roles.defaultRoleNotRemovable": "Cannot delete a default role.",
@ -212,14 +210,12 @@
"history.apps.languagedRemoved": "removed language {[Language]}", "history.apps.languagedRemoved": "removed language {[Language]}",
"history.apps.languagedSetToMaster": "changed master language to {[Language]}", "history.apps.languagedSetToMaster": "changed master language to {[Language]}",
"history.apps.languagedUpdated": "updated language {[Language]}", "history.apps.languagedUpdated": "updated language {[Language]}",
"history.apps.patternAdded": "added pattern {[Name]}",
"history.apps.patternDeleted": "deleted pattern {[PatternId]}",
"history.apps.patternUpdated": "updated pattern {[Name]}",
"history.apps.planChanged": "changed plan to {[Plan]}", "history.apps.planChanged": "changed plan to {[Plan]}",
"history.apps.planReset": "resetted plan", "history.apps.planReset": "resetted plan",
"history.apps.roleAdded": "added role {[Name]}", "history.apps.roleAdded": "added role {[Name]}",
"history.apps.roleDeleted": "deleted role {[Name]}", "history.apps.roleDeleted": "deleted role {[Name]}",
"history.apps.roleUpdated": "updated role {[Name]}", "history.apps.roleUpdated": "updated role {[Name]}",
"history.apps.settingsUpdated": "updated UI settings",
"history.assets.replaced": "replaced asset.", "history.assets.replaced": "replaced asset.",
"history.assets.updated": "updated asset.", "history.assets.updated": "updated asset.",
"history.assets.uploaded": "uploaded asset.", "history.assets.uploaded": "uploaded asset.",

5
backend/i18n/source/backend_it.json

@ -18,8 +18,6 @@
"apps.languages.masterLanguageNotRemovable": "La lingua master non può essere rimossa.", "apps.languages.masterLanguageNotRemovable": "La lingua master non può essere rimossa.",
"apps.nameAlreadyExists": "Esiste già un'app con lo stesso nome.", "apps.nameAlreadyExists": "Esiste già un'app con lo stesso nome.",
"apps.notImage": "Il file non è una immagine", "apps.notImage": "Il file non è una immagine",
"apps.patterns.nameAlreadyExists": "Esiste già un pattern con lo stesso nome.",
"apps.patterns.patternAlreadyExists": "Questo pattern esiste già con un altro nome.",
"apps.plans.notFound": "Non esiste un piano con questo id.", "apps.plans.notFound": "Non esiste un piano con questo id.",
"apps.plans.notPlanOwner": "Solo l'utente che ha configurato il piano inizialmente può modificarlo.", "apps.plans.notPlanOwner": "Solo l'utente che ha configurato il piano inizialmente può modificarlo.",
"apps.roles.defaultRoleNotRemovable": "Non è possibile cancellare un ruolo predefinito.", "apps.roles.defaultRoleNotRemovable": "Non è possibile cancellare un ruolo predefinito.",
@ -213,9 +211,6 @@
"history.apps.languagedRemoved": "rimossa lingua {[Language]}", "history.apps.languagedRemoved": "rimossa lingua {[Language]}",
"history.apps.languagedSetToMaster": "cambiata la lingua master in {[Language]}", "history.apps.languagedSetToMaster": "cambiata la lingua master in {[Language]}",
"history.apps.languagedUpdated": "aggiornata la lingua {[Language]}", "history.apps.languagedUpdated": "aggiornata la lingua {[Language]}",
"history.apps.patternAdded": "ha aggiunto pattern {[Name]}",
"history.apps.patternDeleted": "ha eliminato pattern {[PatternId]}",
"history.apps.patternUpdated": "ha modificato pattern {[Name]}",
"history.apps.planChanged": "ha cambiato il piano in {[Plan]}", "history.apps.planChanged": "ha cambiato il piano in {[Plan]}",
"history.apps.planReset": "ha riconfigurato il piano", "history.apps.planReset": "ha riconfigurato il piano",
"history.apps.roleAdded": "ha aggiunto il ruolo {[Name]}", "history.apps.roleAdded": "ha aggiunto il ruolo {[Name]}",

5
backend/i18n/source/backend_nl.json

@ -18,8 +18,6 @@
"apps.languages.masterLanguageNotRemovable": "Hoofdtaal kan niet worden verwijderd.", "apps.languages.masterLanguageNotRemovable": "Hoofdtaal kan niet worden verwijderd.",
"apps.nameAlreadyExists": "Er bestaat al een app met dezelfde naam.", "apps.nameAlreadyExists": "Er bestaat al een app met dezelfde naam.",
"apps.notImage": "Bestand is geen afbeelding", "apps.notImage": "Bestand is geen afbeelding",
"apps.patterns.nameAlreadyExists": "Er bestaat al een patroon met dezelfde naam.",
"apps.patterns.patternAlreadyExists": "Dit patroon bestaat al maar met een andere naam.",
"apps.plans.notFound": "Een plan met deze id bestaat niet.", "apps.plans.notFound": "Een plan met deze id bestaat niet.",
"apps.plans.notPlanOwner": "Plan kan alleen worden gewijzigd van de gebruiker die het plan aanvankelijk heeft geconfigureerd.", "apps.plans.notPlanOwner": "Plan kan alleen worden gewijzigd van de gebruiker die het plan aanvankelijk heeft geconfigureerd.",
"apps.roles.defaultRoleNotRemovable": "Kan een standaardrol niet verwijderen.", "apps.roles.defaultRoleNotRemovable": "Kan een standaardrol niet verwijderen.",
@ -207,9 +205,6 @@
"history.apps.languagedRemoved": "verwijderde taal {[Language]}", "history.apps.languagedRemoved": "verwijderde taal {[Language]}",
"history.apps.languagedSetToMaster": "hoofdtaal gewijzigd in {[Language]}", "history.apps.languagedSetToMaster": "hoofdtaal gewijzigd in {[Language]}",
"history.apps.languagedUpdated": "bijgewerkte taal {[Language]}", "history.apps.languagedUpdated": "bijgewerkte taal {[Language]}",
"history.apps.patternAdded": "toegevoegd patroon {[Name]}",
"history.apps.patternDeleted": "verwijderd patroon {[PatternId]}",
"history.apps.patternUpdated": "bijgewerkt patroon {[Name]}",
"history.apps.planChanged": "plan gewijzigd in {[Plan]}", "history.apps.planChanged": "plan gewijzigd in {[Plan]}",
"history.apps.planReset": "gereset plan", "history.apps.planReset": "gereset plan",
"history.apps.roleAdded": "toegevoegde rol {[Name]}", "history.apps.roleAdded": "toegevoegde rol {[Name]}",

16
backend/i18n/source/frontend_en.json

@ -52,6 +52,11 @@
"apps.uploadImageTooBig": "App image is too big.", "apps.uploadImageTooBig": "App image is too big.",
"apps.welcomeSubtitle": "Welcome to Squidex.", "apps.welcomeSubtitle": "Welcome to Squidex.",
"apps.welcomeTitle": "Hi {user}", "apps.welcomeTitle": "Hi {user}",
"appSettings.hideScheduler": "Hide dialog for scheduled publishing",
"appSettings.refreshTooltip": "Refresh UI Settings (CTRL + SHIFT + R)",
"appSettings.reloaded": "UI Settings reloaded.",
"appSettings.title": "UI Settings",
"appSettings.updateFailed": "Failed to update UI settings. Please reload.",
"assets.createFolder": "Create Folder", "assets.createFolder": "Create Folder",
"assets.createFolderFailed": "Failed to create asset folder. Please reload.", "assets.createFolderFailed": "Failed to create asset folder. Please reload.",
"assets.createFolderTooltip": "Create new folder (CTRL + SHIFT + G)", "assets.createFolderTooltip": "Create new folder (CTRL + SHIFT + G)",
@ -520,6 +525,10 @@
"dashboard.trafficSummaryCard": "API Traffic Summary", "dashboard.trafficSummaryCard": "API Traffic Summary",
"dashboard.welcomeText": "Welcome to **{app}** dashboard.", "dashboard.welcomeText": "Welcome to **{app}** dashboard.",
"dashboard.welcomeTitle": "Hi {user}", "dashboard.welcomeTitle": "Hi {user}",
"editors.deleteConfirmText": "Do you really want to remove this Editor URL?",
"editors.deleteConfirmTitle": "Delete Editor URL",
"editors.empty": "No Editor URL created yet.",
"editors.title": "Custom Editors",
"eventConsumers.count": "Count", "eventConsumers.count": "Count",
"eventConsumers.loadFailed": "Failed to load event consumers. Please reload.", "eventConsumers.loadFailed": "Failed to load event consumers. Please reload.",
"eventConsumers.pageTitle": "Event Consumers", "eventConsumers.pageTitle": "Event Consumers",
@ -553,13 +562,8 @@
"notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.", "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.deleteConfirmText": "Do you really want to remove this pattern?",
"patterns.deleteConfirmTitle": "Delete pattern", "patterns.deleteConfirmTitle": "Delete pattern",
"patterns.deleteFailed": "Failed to remove pattern. Please reload.",
"patterns.empty": "No pattern created yet.", "patterns.empty": "No pattern created yet.",
"patterns.loadFailed": "Failed to add pattern. Please reload.", "patterns.title": "Patterns",
"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.billingPortal": "Billing Portal",
"plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.", "plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.",
"plans.change": "Change", "plans.change": "Change",

6
backend/i18n/source/frontend_it.json

@ -547,13 +547,7 @@
"notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.", "notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.",
"patterns.deleteConfirmText": "Sei sicuro di voler rimuovere il pattern?", "patterns.deleteConfirmText": "Sei sicuro di voler rimuovere il pattern?",
"patterns.deleteConfirmTitle": "Cancella il pattern", "patterns.deleteConfirmTitle": "Cancella il pattern",
"patterns.deleteFailed": "Non è stato possibile rimuovere il pattern. Per favore ricarica.",
"patterns.empty": "Nessun pattern è stato ancora creato.", "patterns.empty": "Nessun pattern è stato ancora creato.",
"patterns.loadFailed": "Non è stato possibile aggiungere il pattern. Per favore ricarica.",
"patterns.nameValidationMessage": "Il nome può contenere solo lettere, numeri, trattini e spazi.",
"patterns.refreshTooltip": "Aggiorna i pattern (CTRL + SHIFT + R)",
"patterns.reloaded": "Pattern ricaricati.",
"patterns.updateFailed": "Non è stato possibile aggiornare pattern. Per favore ricarica.",
"plans.billingPortal": "Portale di fatturazione", "plans.billingPortal": "Portale di fatturazione",
"plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.", "plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.",
"plans.change": "Cambia", "plans.change": "Cambia",

6
backend/i18n/source/frontend_nl.json

@ -519,13 +519,7 @@
"notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.", "notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.",
"patterns.deleteConfirmText": "Wil je dit patroon echt verwijderen?", "patterns.deleteConfirmText": "Wil je dit patroon echt verwijderen?",
"patterns.deleteConfirmTitle": "Verwijder patroon", "patterns.deleteConfirmTitle": "Verwijder patroon",
"patterns.deleteFailed": "Verwijderen van patroon is mislukt. Laad opnieuw.",
"patterns.empty": "Nog geen patroon gemaakt.", "patterns.empty": "Nog geen patroon gemaakt.",
"patterns.loadFailed": "Toevoegen van patroon is mislukt. Laad opnieuw.",
"patterns.nameValidationMessage": "Naam mag alleen letters, cijfers, streepjes en spaties bevatten.",
"patterns.refreshTooltip": "Ververs patronen (CTRL + SHIFT + R)",
"patterns.reloaded": "Patronen herladen.",
"patterns.updateFailed": "Bijwerken van patroon is mislukt. Laad opnieuw.",
"plans.billingPortal": "Factureringsportal", "plans.billingPortal": "Factureringsportal",
"plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.", "plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.",
"plans.change": "Wijzigen", "plans.change": "Wijzigen",

8
backend/src/Migrations/MigrationPath.cs

@ -18,7 +18,7 @@ namespace Migrations
{ {
public sealed class MigrationPath : IMigrationPath public sealed class MigrationPath : IMigrationPath
{ {
private const int CurrentVersion = 25; private const int CurrentVersion = 26;
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public MigrationPath(IServiceProvider serviceProvider) public MigrationPath(IServiceProvider serviceProvider)
@ -136,6 +136,12 @@ namespace Migrations
yield return serviceProvider.GetRequiredService<PopulateGrainIndexes>(); yield return serviceProvider.GetRequiredService<PopulateGrainIndexes>();
} }
// Version 26: UI Settings.
if (version < 26)
{
yield return serviceProvider.GetRequiredService<CreateAppSettings>();
}
yield return serviceProvider.GetRequiredService<StartEventConsumers>(); yield return serviceProvider.GetRequiredService<StartEventConsumers>();
} }
} }

112
backend/src/Migrations/Migrations/CreateAppSettings.cs

@ -0,0 +1,112 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Migrations.OldEvents;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Migrations;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Migrations.Migrations
{
public sealed class CreateAppSettings : IMigration
{
private readonly ICommandBus commandBus;
private readonly IEventDataFormatter eventDataFormatter;
private readonly IEventStore eventStore;
public CreateAppSettings(ICommandBus commandBus,
IEventDataFormatter eventDataFormatter,
IEventStore eventStore)
{
this.commandBus = commandBus;
this.eventDataFormatter = eventDataFormatter;
this.eventStore = eventStore;
}
public async Task UpdateAsync()
{
var apps = new Dictionary<NamedId<DomainId>, Dictionary<DomainId, (string Name, string Pattern, string? Message)>>();
await eventStore.QueryAsync(storedEvent =>
{
var @event = eventDataFormatter.ParseIfKnown(storedEvent);
if (@event != null)
{
switch (@event.Payload)
{
case AppPatternAdded patternAdded:
{
var patterns = apps.GetOrAddNew(patternAdded.AppId);
patterns[patternAdded.PatternId] = (patternAdded.Name, patternAdded.Pattern, patternAdded.Message);
break;
}
case AppPatternUpdated patternUpdated:
{
var patterns = apps.GetOrAddNew(patternUpdated.AppId);
patterns[patternUpdated.PatternId] = (patternUpdated.Name, patternUpdated.Pattern, patternUpdated.Message);
break;
}
case AppPatternDeleted patternDeleted:
{
var patterns = apps.GetOrAddNew(patternDeleted.AppId);
patterns.Remove(patternDeleted.PatternId);
break;
}
case AppArchived appArchived:
{
apps.Remove(appArchived.AppId);
break;
}
}
}
return Task.CompletedTask;
}, "^app\\-");
var actor = RefToken.Client("Migrator");
foreach (var (appId, patterns) in apps)
{
if (patterns.Count > 0)
{
var settings = new AppSettings
{
Patterns = patterns.Values.Select(x => new Pattern(x.Name, x.Pattern)
{
Message = x.Message
}).ToReadOnlyCollection()
};
await commandBus.PublishAsync(new UpdateAppSettings
{
AppId = appId,
Settings = settings,
FromRule = true,
Actor = actor
});
}
}
}
}
}

5
backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs → backend/src/Migrations/OldEvents/AppPatternAdded.cs

@ -5,12 +5,15 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps namespace Migrations.OldEvents
{ {
[EventType(nameof(AppPatternAdded))] [EventType(nameof(AppPatternAdded))]
[Obsolete("New Event introduced")]
public sealed class AppPatternAdded : AppEvent public sealed class AppPatternAdded : AppEvent
{ {
public DomainId PatternId { get; set; } public DomainId PatternId { get; set; }

5
backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs → backend/src/Migrations/OldEvents/AppPatternDeleted.cs

@ -5,12 +5,15 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps namespace Migrations.OldEvents
{ {
[EventType(nameof(AppPatternDeleted))] [EventType(nameof(AppPatternDeleted))]
[Obsolete("New Event introduced")]
public sealed class AppPatternDeleted : AppEvent public sealed class AppPatternDeleted : AppEvent
{ {
public DomainId PatternId { get; set; } public DomainId PatternId { get; set; }

5
backend/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs → backend/src/Migrations/OldEvents/AppPatternUpdated.cs

@ -5,12 +5,15 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps namespace Migrations.OldEvents
{ {
[EventType(nameof(AppPatternUpdated))] [EventType(nameof(AppPatternUpdated))]
[Obsolete("New Event introduced")]
public sealed class AppPatternUpdated : AppEvent public sealed class AppPatternUpdated : AppEvent
{ {
public DomainId PatternId { get; set; } public DomainId PatternId { get; set; }

71
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs

@ -1,71 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppPatterns : ImmutableDictionary<DomainId, AppPattern>
{
public static readonly AppPatterns Empty = new AppPatterns();
private AppPatterns()
{
}
public AppPatterns(Dictionary<DomainId, AppPattern> inner)
: base(inner)
{
}
[Pure]
public AppPatterns Remove(DomainId id)
{
return Without<AppPatterns>(id);
}
[Pure]
public AppPatterns Add(DomainId id, string name, string pattern, string? message = null)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNullOrEmpty(pattern, nameof(pattern));
var newPattern = new AppPattern(name, pattern) { Message = message };
return With<AppPatterns>(id, newPattern);
}
[Pure]
public AppPatterns Update(DomainId id, string? name = null, string? pattern = null, string? message = null)
{
if (!TryGetValue(id, out var appPattern))
{
return this;
}
var newPattern = appPattern with
{
Message = message
};
if (!string.IsNullOrWhiteSpace(name))
{
newPattern = newPattern with { Name = name };
}
if (!string.IsNullOrWhiteSpace(pattern))
{
newPattern = newPattern with { Pattern = pattern };
}
return With<AppPatterns>(id, newPattern);
}
}
}

28
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppSettings.cs

@ -0,0 +1,28 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
[Equals(DoNotAddEqualityOperators = true)]
public sealed class AppSettings
{
public static readonly AppSettings Empty = new AppSettings();
public ReadOnlyCollection<Pattern> Patterns { get; init; } = ReadOnlyCollection.Empty<Pattern>();
public ReadOnlyCollection<Editor> Editors { get; init; } = ReadOnlyCollection.Empty<Editor>();
public bool HideScheduler { get; init; }
public bool HideDateTimeModeButton { get; init; }
public bool HideDateTimeQuickButtons { get; init; }
}
}

15
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed record Editor(string Name, string Url)
{
}
}

33
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsSurrogate.cs

@ -1,33 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class AppPatternsSurrogate : Dictionary<DomainId, AppPattern>, ISurrogate<AppPatterns>
{
public void FromSource(AppPatterns source)
{
foreach (var (key, pattern) in source)
{
Add(key, pattern);
}
}
public AppPatterns ToSource()
{
if (Count == 0)
{
return AppPatterns.Empty;
}
return new AppPatterns(this);
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs

@ -1,7 +1,7 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
@ -9,8 +9,8 @@
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed record AppPattern(string Name, string Pattern) public sealed record Pattern(string Name, string Regex)
{ {
public string? Message { get; init; } public string? Message { get; init; }
} }
} }

26
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -12,7 +12,6 @@ using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using P = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
@ -21,17 +20,16 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
private static readonly HashSet<string> ExtraPermissions = new HashSet<string> private static readonly HashSet<string> ExtraPermissions = new HashSet<string>
{ {
P.AppComments, Shared.Permissions.AppComments,
P.AppContributorsRead, Shared.Permissions.AppContributorsRead,
P.AppHistory, Shared.Permissions.AppHistory,
P.AppLanguagesRead, Shared.Permissions.AppLanguagesRead,
P.AppPatternsRead, Shared.Permissions.AppPing,
P.AppPing, Shared.Permissions.AppRolesRead,
P.AppRolesRead, Shared.Permissions.AppSchemasRead,
P.AppSchemasRead, Shared.Permissions.AppSearch,
P.AppSearch, Shared.Permissions.AppTranslate,
P.AppTranslate, Shared.Permissions.AppUsage
P.AppUsage
}; };
public const string Editor = "Editor"; public const string Editor = "Editor";
@ -93,7 +91,7 @@ namespace Squidex.Domain.Apps.Core.Apps
if (Permissions.Any()) if (Permissions.Any())
{ {
var prefix = P.ForApp(P.App, app).Id; var prefix = Shared.Permissions.ForApp(Shared.Permissions.App, app).Id;
foreach (var permission in Permissions) foreach (var permission in Permissions)
{ {
@ -105,7 +103,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
foreach (var extraPermissionId in ExtraPermissions) foreach (var extraPermissionId in ExtraPermissions)
{ {
var extraPermission = P.ForApp(extraPermissionId, app); var extraPermission = Shared.Permissions.ForApp(extraPermissionId, app);
result.Add(extraPermission); result.Add(extraPermission);
} }

1
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -50,7 +50,6 @@ namespace Squidex.Domain.Apps.Core.Apps
new PermissionSet( new PermissionSet(
WithoutPrefix(Permissions.AppAssets), WithoutPrefix(Permissions.AppAssets),
WithoutPrefix(Permissions.AppContents), WithoutPrefix(Permissions.AppContents),
WithoutPrefix(Permissions.AppPatterns),
WithoutPrefix(Permissions.AppRolesRead), WithoutPrefix(Permissions.AppRolesRead),
WithoutPrefix(Permissions.AppRules), WithoutPrefix(Permissions.AppRules),
WithoutPrefix(Permissions.AppSchemas), WithoutPrefix(Permissions.AppSchemas),

12
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -6,8 +6,10 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
#pragma warning disable IDE0051 // Remove unused private members #pragma warning disable IDE0051 // Remove unused private members
@ -19,20 +21,20 @@ namespace Squidex.Domain.Apps.Core.Contents
private const string DefaultName = "Unnamed"; private const string DefaultName = "Unnamed";
public static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>(); public static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>();
public static readonly IReadOnlyList<DomainId> EmptySchemaIds = new List<DomainId>();
public static readonly Workflow Default = CreateDefault(); public static readonly Workflow Default = CreateDefault();
public static readonly Workflow Empty = new Workflow(default, EmptySteps); public static readonly Workflow Empty = new Workflow(default, null);
[IgnoreDuringEquals] [IgnoreDuringEquals]
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps; public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps;
public IReadOnlyList<DomainId> SchemaIds { get; } = EmptySchemaIds; public ReadOnlyCollection<DomainId> SchemaIds { get; } = ReadOnlyCollection.Empty<DomainId>();
public Status Initial { get; } public Status Initial { get; }
public Workflow( public Workflow(
Status initial, Status initial,
IReadOnlyDictionary<Status, WorkflowStep>? steps, IReadOnlyDictionary<Status, WorkflowStep>? steps = null,
IReadOnlyList<DomainId>? schemaIds = null, IReadOnlyList<DomainId>? schemaIds = null,
string? name = null) string? name = null)
: base(name.Or(DefaultName)) : base(name.Or(DefaultName))
@ -46,7 +48,7 @@ namespace Squidex.Domain.Apps.Core.Contents
if (schemaIds != null) if (schemaIds != null)
{ {
SchemaIds = schemaIds; SchemaIds = schemaIds.ToReadOnlyCollection();
} }
} }

28
backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -52,14 +52,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
AddEventMessage<AppMasterLanguageSet>( AddEventMessage<AppMasterLanguageSet>(
"history.apps.languagedSetToMaster"); "history.apps.languagedSetToMaster");
AddEventMessage<AppPatternAdded>( AddEventMessage<AppSettingsUpdated>(
"history.apps.patternAdded"); "history.apps.settingsUpdated");
AddEventMessage<AppPatternDeleted>(
"history.apps.patternDeleted");
AddEventMessage<AppPatternUpdated>(
"history.apps.patternUpdated");
AddEventMessage<AppRoleAdded>( AddEventMessage<AppRoleAdded>(
"history.apps.roleAdded"); "history.apps.roleAdded");
@ -93,12 +87,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
return CreateLanguagesEvent(e, e.Language); return CreateLanguagesEvent(e, e.Language);
case AppLanguageRemoved e: case AppLanguageRemoved e:
return CreateLanguagesEvent(e, e.Language); return CreateLanguagesEvent(e, e.Language);
case AppPatternAdded e:
return CreatePatternsEvent(e, e.PatternId, e.Name);
case AppPatternUpdated e:
return CreatePatternsEvent(e, e.PatternId, e.Name);
case AppPatternDeleted e:
return CreatePatternsEvent(e, e.PatternId);
case AppRoleAdded e: case AppRoleAdded e:
return CreateRolesEvent(e, e.Name); return CreateRolesEvent(e, e.Name);
case AppRoleUpdated e: case AppRoleUpdated e:
@ -109,11 +97,18 @@ namespace Squidex.Domain.Apps.Entities.Apps
return CreatePlansEvent(e, e.PlanId); return CreatePlansEvent(e, e.PlanId);
case AppPlanReset e: case AppPlanReset e:
return CreatePlansEvent(e); return CreatePlansEvent(e);
case AppSettingsUpdated e:
return CreateAppSettingsEvent(e);
} }
return null; return null;
} }
private HistoryEvent CreateAppSettingsEvent(AppSettingsUpdated e)
{
return ForEvent(e, "settings.appSettings");
}
private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null) private HistoryEvent CreateContributorsEvent(IEvent e, string contributor, string? role = null)
{ {
return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role); return ForEvent(e, "settings.contributors").Param("Contributor", contributor).Param("Role", role);
@ -129,11 +124,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
return ForEvent(e, "settings.roles").Param("Name", name); return ForEvent(e, "settings.roles").Param("Name", name);
} }
private HistoryEvent CreatePatternsEvent(IEvent e, DomainId id, string? name = null)
{
return ForEvent(e, "settings.patterns").Param("PatternId", id).Param("Name", name);
}
private HistoryEvent CreateClientsEvent(IEvent e, string id) private HistoryEvent CreateClientsEvent(IEvent e, string id)
{ {
return ForEvent(e, "settings.clients").Param("Id", id); return ForEvent(e, "settings.clients").Param("Id", id);

3
backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs

@ -63,9 +63,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
Search("Languages", Permissions.AppLanguagesRead, Search("Languages", Permissions.AppLanguagesRead,
urlGenerator.LanguagesUI, SearchResultType.Setting); urlGenerator.LanguagesUI, SearchResultType.Setting);
Search("Patterns", Permissions.AppPatternsRead,
urlGenerator.PatternsUI, SearchResultType.Setting);
Search("Roles", Permissions.AppRolesRead, Search("Roles", Permissions.AppRolesRead,
urlGenerator.RolesUI, SearchResultType.Setting); urlGenerator.RolesUI, SearchResultType.Setting);

27
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs

@ -1,27 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class AddPattern : AppUpdateCommand
{
public DomainId PatternId { get; set; }
public string Name { get; set; }
public string Pattern { get; set; }
public string? Message { get; set; }
public AddPattern()
{
PatternId = DomainId.NewGuid();
}
}
}

14
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs → backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateAppSettings.cs

@ -1,22 +1,16 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure; using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class UpdatePattern : AppUpdateCommand public sealed class UpdateAppSettings : AppUpdateCommand
{ {
public DomainId PatternId { get; set; } public AppSettings Settings { get; set; }
public string Name { get; set; }
public string Pattern { get; set; }
public string? Message { get; set; }
} }
} }

52
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs

@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
public AppClients Clients { get; set; } = AppClients.Empty; public AppClients Clients { get; set; } = AppClients.Empty;
public AppPatterns Patterns { get; set; } = AppPatterns.Empty; public AppSettings Settings { get; set; } = AppSettings.Empty;
public AppContributors Contributors { get; set; } = AppContributors.Empty; public AppContributors Contributors { get; set; } = AppContributors.Empty;
@ -73,17 +73,20 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return true; return true;
} }
case AppImageUploaded e: case AppSettingsUpdated e when Is.Change(Settings, e.Settings):
return UpdateImage(e, e => e.Image); return UpdateSettings(e.Settings);
case AppImageRemoved e when Image != null:
return UpdateImage(e, e => null);
case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId):
return UpdatePlan(e, e => e.ToAppPlan()); return UpdatePlan(e.ToPlan());
case AppPlanReset e when Plan != null: case AppPlanReset e when Plan != null:
return UpdatePlan(e, e => null); return UpdatePlan(null);
case AppImageUploaded e:
return UpdateImage(e.Image);
case AppImageRemoved e when Image != null:
return UpdateImage(null);
case AppContributorAssigned e: case AppContributorAssigned e:
return UpdateContributors(e, (e, c) => c.Assign(e.ContributorId, e.Role)); return UpdateContributors(e, (e, c) => c.Assign(e.ContributorId, e.Role));
@ -109,15 +112,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
case AppWorkflowDeleted e: case AppWorkflowDeleted e:
return UpdateWorkflows(e, (e, w) => w.Remove(e.WorkflowId)); return UpdateWorkflows(e, (e, w) => w.Remove(e.WorkflowId));
case AppPatternAdded e:
return UpdatePatterns(e, (ev, p) => p.Add(ev.PatternId, ev.Name, ev.Pattern, ev.Message));
case AppPatternDeleted e:
return UpdatePatterns(e, (e, p) => p.Remove(e.PatternId));
case AppPatternUpdated e:
return UpdatePatterns(e, (e, p) => p.Update(e.PatternId, e.Name, e.Pattern, e.Message));
case AppRoleAdded e: case AppRoleAdded e:
return UpdateRoles(e, (e, r) => r.Add(e.Name)); return UpdateRoles(e, (e, r) => r.Add(e.Name));
@ -186,15 +180,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return !ReferenceEquals(previous, Languages); return !ReferenceEquals(previous, Languages);
} }
private bool UpdatePatterns<T>(T @event, Func<T, AppPatterns, AppPatterns> update)
{
var previous = Patterns;
Patterns = update(@event, previous);
return !ReferenceEquals(previous, Patterns);
}
private bool UpdateRoles<T>(T @event, Func<T, Roles, Roles> update) private bool UpdateRoles<T>(T @event, Func<T, Roles, Roles> update)
{ {
var previous = Roles; var previous = Roles;
@ -213,16 +198,23 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return !ReferenceEquals(previous, Workflows); return !ReferenceEquals(previous, Workflows);
} }
private bool UpdateImage<T>(T @event, Func<T, AppImage?> update) private bool UpdateImage(AppImage? image)
{
Image = image;
return true;
}
private bool UpdateSettings(AppSettings settings)
{ {
Image = update(@event); Settings = settings;
return true; return true;
} }
private bool UpdatePlan<T>(T @event, Func<T, AppPlan?> update) private bool UpdatePlan(AppPlan? plan)
{ {
Plan = update(@event); Plan = plan;
return true; return true;
} }

81
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs

@ -26,13 +26,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
{ {
public sealed partial class AppDomainObject : DomainObject<AppDomainObject.State> public sealed partial class AppDomainObject : DomainObject<AppDomainObject.State>
{ {
private readonly InitialPatterns initialPatterns; private readonly InitialSettings initialSettings;
private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlansProvider appPlansProvider;
private readonly IAppPlanBillingManager appPlansBillingManager; private readonly IAppPlanBillingManager appPlansBillingManager;
private readonly IUserResolver userResolver; private readonly IUserResolver userResolver;
public AppDomainObject(IPersistenceFactory<State> persistence, ISemanticLog log, public AppDomainObject(IPersistenceFactory<State> persistence, ISemanticLog log,
InitialPatterns initialPatterns, InitialSettings initialPatterns,
IAppPlansProvider appPlansProvider, IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager, IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver) IUserResolver userResolver)
@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
this.userResolver = userResolver; this.userResolver = userResolver;
this.appPlansProvider = appPlansProvider; this.appPlansProvider = appPlansProvider;
this.appPlansBillingManager = appPlansBillingManager; this.appPlansBillingManager = appPlansBillingManager;
this.initialPatterns = initialPatterns; this.initialSettings = initialPatterns;
} }
protected override bool IsDeleted() protected override bool IsDeleted()
@ -88,6 +88,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return Snapshot; return Snapshot;
}); });
case UpdateAppSettings updateSettings:
return UpdateReturn(updateSettings, c =>
{
GuardApp.CanUpdateSettings(c);
UpdateSettings(c);
return Snapshot;
});
case UploadAppImage uploadImage: case UploadAppImage uploadImage:
return UpdateReturn(uploadImage, c => return UpdateReturn(uploadImage, c =>
{ {
@ -248,36 +258,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return Snapshot; return Snapshot;
}); });
case AddPattern addPattern:
return UpdateReturn(addPattern, c =>
{
GuardAppPatterns.CanAdd(c, Snapshot);
AddPattern(c);
return Snapshot;
});
case DeletePattern deletePattern:
return UpdateReturn(deletePattern, c =>
{
GuardAppPatterns.CanDelete(c, Snapshot);
DeletePattern(c);
return Snapshot;
});
case UpdatePattern updatePattern:
return UpdateReturn(updatePattern, c =>
{
GuardAppPatterns.CanUpdate(c, Snapshot);
UpdatePattern(c);
return Snapshot;
});
case ChangePlan changePlan: case ChangePlan changePlan:
return UpdateReturnAsync(changePlan, async c => return UpdateReturnAsync(changePlan, async c =>
{ {
@ -339,10 +319,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
events.Add(CreateInitialOwner(command.Actor)); events.Add(CreateInitialOwner(command.Actor));
} }
foreach (var (key, value) in initialPatterns) events.Add(CreateInitialSettings());
{
events.Add(CreateInitialPattern(key, value));
}
foreach (var @event in events) foreach (var @event in events)
{ {
@ -369,6 +346,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
Raise(command, new AppUpdated()); Raise(command, new AppUpdated());
} }
private void UpdateSettings(UpdateAppSettings command)
{
Raise(command, new AppSettingsUpdated());
}
private void UpdateClient(UpdateClient command) private void UpdateClient(UpdateClient command)
{ {
Raise(command, new AppClientUpdated()); Raise(command, new AppClientUpdated());
@ -434,21 +416,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
Raise(command, new AppLanguageRemoved()); Raise(command, new AppLanguageRemoved());
} }
private void AddPattern(AddPattern command)
{
Raise(command, new AppPatternAdded());
}
private void DeletePattern(DeletePattern command)
{
Raise(command, new AppPatternDeleted());
}
private void UpdatePattern(UpdatePattern command)
{
Raise(command, new AppPatternUpdated());
}
private void AddRole(AddRole command) private void AddRole(AddRole command)
{ {
Raise(command, new AppRoleAdded()); Raise(command, new AppRoleAdded());
@ -493,15 +460,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner };
} }
private static AppPatternAdded CreateInitialPattern(DomainId id, AppPattern pattern) private AppSettingsUpdated CreateInitialSettings()
{ {
return new AppPatternAdded return new AppSettingsUpdated { Settings = initialSettings.Settings };
{
Name = pattern.Name,
PatternId = id,
Pattern = pattern.Pattern,
Message = pattern.Message
};
} }
} }
} }

66
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs

@ -52,6 +52,72 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
} }
public static void CanUpdateSettings(UpdateAppSettings command)
{
Guard.NotNull(command, nameof(command));
Validate.It(e =>
{
var prefix = nameof(command.Settings);
var settings = command.Settings;
if (settings == null)
{
e(Not.Defined(nameof(settings)), prefix);
return;
}
var patternsPrefix = $"{prefix}.{nameof(settings.Patterns)}";
if (settings.Patterns == null)
{
e(Not.Defined(nameof(settings.Patterns)), patternsPrefix);
}
else
{
settings.Patterns.Foreach((pattern, index) =>
{
var patternPrefix = $"{patternsPrefix}[{index}]";
if (string.IsNullOrWhiteSpace(pattern.Name))
{
e(Not.Defined(nameof(pattern.Name)), $"{patternPrefix}.{nameof(pattern.Name)}");
}
if (string.IsNullOrWhiteSpace(pattern.Regex))
{
e(Not.Defined(nameof(pattern.Regex)), $"{patternPrefix}.{nameof(pattern.Regex)}");
}
});
}
var editorsPrefix = $"{prefix}.{nameof(settings.Editors)}";
if (settings.Editors == null)
{
e(Not.Defined(nameof(settings.Editors)), editorsPrefix);
}
else
{
settings.Editors.Foreach((editor, index) =>
{
var editorPrefix = $"{editorsPrefix}[{index}]";
if (string.IsNullOrWhiteSpace(editor.Name))
{
e(Not.Defined(nameof(editor.Name)), $"{editorPrefix}.{nameof(editor.Name)}");
}
if (string.IsNullOrWhiteSpace(editor.Url))
{
e(Not.Defined(nameof(editor.Url)), $"{editorPrefix}.{nameof(editor.Url)}");
}
});
}
});
}
public static void CanChangePlan(ChangePlan command, IAppEntity app, IAppPlansProvider appPlans) public static void CanChangePlan(ChangePlan command, IAppEntity app, IAppPlansProvider appPlans)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));

109
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardAppPatterns.cs

@ -1,109 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
{
public static class GuardAppPatterns
{
public static void CanAdd(AddPattern command, IAppEntity app)
{
Guard.NotNull(command, nameof(command));
var patterns = app.Patterns;
Validate.It(e =>
{
if (command.PatternId == DomainId.Empty)
{
e(Not.Defined(nameof(command.PatternId)), nameof(command.PatternId));
}
if (string.IsNullOrWhiteSpace(command.Name))
{
e(Not.Defined(nameof(command.Name)), nameof(command.Name));
}
if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase)))
{
e(T.Get("apps.patterns.nameAlreadyExists"));
}
if (string.IsNullOrWhiteSpace(command.Pattern))
{
e(Not.Defined(nameof(command.Pattern)), nameof(command.Pattern));
}
else if (!command.Pattern.IsValidRegex())
{
e(Not.Valid(nameof(command.Pattern)), nameof(command.Pattern));
}
if (patterns.Values.Any(x => x.Pattern == command.Pattern))
{
e(T.Get("apps.patterns.patternAlreadyExists"));
}
});
}
public static void CanDelete(DeletePattern command, IAppEntity app)
{
Guard.NotNull(command, nameof(command));
var patterns = app.Patterns;
if (!patterns.ContainsKey(command.PatternId))
{
throw new DomainObjectNotFoundException(command.PatternId.ToString());
}
}
public static void CanUpdate(UpdatePattern command, IAppEntity app)
{
Guard.NotNull(command, nameof(command));
var patterns = app.Patterns;
if (!patterns.ContainsKey(command.PatternId))
{
throw new DomainObjectNotFoundException(command.PatternId.ToString());
}
Validate.It(e =>
{
if (string.IsNullOrWhiteSpace(command.Name))
{
e(Not.Defined(nameof(command.Name)), nameof(command.Name));
}
if (patterns.Any(x => x.Key != command.PatternId && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase)))
{
e(T.Get("apps.patterns.nameAlreadyExists"));
}
if (string.IsNullOrWhiteSpace(command.Pattern))
{
e(Not.Defined(nameof(command.Pattern)), nameof(command.Pattern));
}
else if (!command.Pattern.IsValidRegex())
{
e(Not.Valid(nameof(command.Pattern)), nameof(command.Pattern));
}
if (patterns.Any(x => x.Key != command.PatternId && x.Value.Pattern == command.Pattern))
{
e(T.Get("apps.patterns.patternAlreadyExists"));
}
});
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
AppClients Clients { get; } AppClients Clients { get; }
AppPatterns Patterns { get; } AppSettings Settings { get; }
AppContributors Contributors { get; } AppContributors Contributors { get; }

25
backend/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs

@ -1,25 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class InitialPatterns : Dictionary<DomainId, AppPattern>
{
public InitialPatterns()
{
}
public InitialPatterns(Dictionary<DomainId, AppPattern> patterns)
: base(patterns)
{
}
}
}

8
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs → backend/src/Squidex.Domain.Apps.Entities/Apps/InitialSettings.cs

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure; using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps
{ {
public sealed class DeletePattern : AppUpdateCommand public sealed class InitialSettings
{ {
public DomainId PatternId { get; set; } public AppSettings Settings { get; set; }
} }
} }

32
backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs

@ -5,8 +5,10 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
@ -16,13 +18,39 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
public sealed class RolePermissionsProvider public sealed class RolePermissionsProvider
{ {
private readonly List<string> forAppSchemas = new List<string>();
private readonly List<string> forAppWithoutSchemas = new List<string>();
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
static RolePermissionsProvider()
{
}
public RolePermissionsProvider(IAppProvider appProvider) public RolePermissionsProvider(IAppProvider appProvider)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider; this.appProvider = appProvider;
foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (field.IsLiteral && !field.IsInitOnly)
{
var value = field.GetValue(null) as string;
if (value?.StartsWith(Permissions.App, StringComparison.OrdinalIgnoreCase) == true)
{
if (value.IndexOf("{name}", Permissions.App.Length, StringComparison.OrdinalIgnoreCase) >= 0)
{
forAppSchemas.Add(value);
}
else
{
forAppWithoutSchemas.Add(value);
}
}
}
}
} }
public async Task<List<string>> GetPermissionsAsync(IAppEntity app) public async Task<List<string>> GetPermissionsAsync(IAppEntity app)
@ -31,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = new List<string> { Permission.Any }; var result = new List<string> { Permission.Any };
foreach (var permission in Permissions.ForAppsNonSchema) foreach (var permission in forAppWithoutSchemas)
{ {
if (permission.Length > Permissions.App.Length + 1) if (permission.Length > Permissions.App.Length + 1)
{ {
@ -44,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
} }
foreach (var permission in Permissions.ForAppsSchema) foreach (var permission in forAppSchemas)
{ {
var trimmed = permission[(Permissions.App.Length + 1)..]; var trimmed = permission[(Permissions.App.Length + 1)..];

3
backend/src/Squidex.Domain.Apps.Entities/Context.cs

@ -14,7 +14,6 @@ using Squidex.Infrastructure.Security;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using ClaimsPermissions = Squidex.Infrastructure.Security.PermissionSet; using ClaimsPermissions = Squidex.Infrastructure.Security.PermissionSet;
using P = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
@ -61,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities
var claimsIdentity = new ClaimsIdentity(); var claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, P.All)); claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.All));
return new Context(claimsPrincipal, app); return new Context(claimsPrincipal, app);
} }

2
backend/src/Squidex.Domain.Apps.Events/Apps/AppPlanChanged.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Events.Apps
{ {
public string PlanId { get; set; } public string PlanId { get; set; }
public AppPlan ToAppPlan() public AppPlan ToPlan()
{ {
return new AppPlan(Actor, PlanId); return new AppPlan(Actor, PlanId);
} }

18
backend/src/Squidex.Domain.Apps.Events/Apps/AppSettingsUpdated.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppSettingsUpdated))]
public sealed class AppSettingsUpdated : AppEvent
{
public AppSettings Settings { get; set; }
}
}

12
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs

@ -190,17 +190,9 @@ namespace Squidex.Infrastructure.MongoDb
var actionBlock = var actionBlock =
new ActionBlock<T>(async x => new ActionBlock<T>(async x =>
{ {
try if (!combined.IsCancellationRequested)
{ {
if (!combined.IsCancellationRequested) await processor(x);
{
await processor(x);
}
}
catch (OperationCanceledException ex)
{
// Dataflow swallows operation cancelled exception.
throw new AggregateException(ex);
} }
}, },
new ExecutionDataflowBlockOptions new ExecutionDataflowBlockOptions

36
backend/src/Squidex.Shared/PermissionExtensions.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Squidex.Infrastructure.Security;
namespace Squidex.Shared
{
public static class PermissionExtensions
{
public static bool Allows(this PermissionSet permissions, string id, string app = Permission.Any, string schema = Permission.Any)
{
var permission = Permissions.ForApp(id, app, schema);
return permissions.Allows(permission);
}
public static string[] ToAppNames(this PermissionSet permissions)
{
var matching = permissions.Where(x => x.StartsWith("squidex.apps."));
var result =
matching
.Select(x => x.Id.Split('.')).Where(x => x.Length > 2)
.Select(x => x[2])
.Distinct()
.ToArray();
return result;
}
}
}

101
backend/src/Squidex.Shared/Permissions.cs

@ -5,10 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
@ -16,32 +12,25 @@ namespace Squidex.Shared
{ {
public static class Permissions public static class Permissions
{ {
private static readonly List<string> ForAppsNonSchemaList = new List<string>();
private static readonly List<string> ForAppsSchemaList = new List<string>();
public static IReadOnlyList<string> ForAppsNonSchema
{
get => ForAppsNonSchemaList;
}
public static IReadOnlyList<string> ForAppsSchema
{
get => ForAppsSchemaList;
}
public const string All = "squidex.*"; public const string All = "squidex.*";
public const string Admin = "squidex.admin.*"; public const string Admin = "squidex.admin.*";
// Orleans
public const string AdminOrleans = "squidex.admin.orleans"; public const string AdminOrleans = "squidex.admin.orleans";
// Admin App Creation
public const string AdminAppCreate = "squidex.admin.apps.create"; public const string AdminAppCreate = "squidex.admin.apps.create";
// Backup Admin
public const string AdminRestore = "squidex.admin.restore"; public const string AdminRestore = "squidex.admin.restore";
// Event Admin
public const string AdminEvents = "squidex.admin.events"; public const string AdminEvents = "squidex.admin.events";
public const string AdminEventsRead = "squidex.admin.events.read"; public const string AdminEventsRead = "squidex.admin.events.read";
public const string AdminEventsManage = "squidex.admin.events.manage"; public const string AdminEventsManage = "squidex.admin.events.manage";
// User Admin
public const string AdminUsers = "squidex.admin.users"; public const string AdminUsers = "squidex.admin.users";
public const string AdminUsersRead = "squidex.admin.users.read"; public const string AdminUsersRead = "squidex.admin.users.read";
public const string AdminUsersCreate = "squidex.admin.users.create"; public const string AdminUsersCreate = "squidex.admin.users.create";
@ -51,67 +40,84 @@ namespace Squidex.Shared
public const string App = "squidex.apps.{app}"; public const string App = "squidex.apps.{app}";
// App General
public const string AppAdmin = "squidex.apps.{app}.*"; public const string AppAdmin = "squidex.apps.{app}.*";
public const string AppDelete = "squidex.apps.{app}.delete"; public const string AppDelete = "squidex.apps.{app}.delete";
public const string AppUpdate = "squidex.apps.{app}.update"; public const string AppUpdate = "squidex.apps.{app}.update";
public const string AppUpdateImage = "squidex.apps.{app}.image"; public const string AppUpdateSettings = "squidex.apps.{app}.settings";
// App Image
public const string AppImageUpload = "squidex.apps.{app}.image";
public const string AppImageDelete = "squidex.apps.{app}.image";
// History
public const string AppHistory = "squidex.apps.{app}.history"; public const string AppHistory = "squidex.apps.{app}.history";
// Ping
public const string AppPing = "squidex.apps.{app}.ping"; public const string AppPing = "squidex.apps.{app}.ping";
// Search
public const string AppSearch = "squidex.apps.{app}.search"; public const string AppSearch = "squidex.apps.{app}.search";
// Translate
public const string AppTranslate = "squidex.apps.{app}.translate"; public const string AppTranslate = "squidex.apps.{app}.translate";
// Usage
public const string AppUsage = "squidex.apps.{app}.usage"; public const string AppUsage = "squidex.apps.{app}.usage";
// Comments
public const string AppComments = "squidex.apps.{app}.comments"; public const string AppComments = "squidex.apps.{app}.comments";
public const string AppCommentsRead = "squidex.apps.{app}.comments.read"; public const string AppCommentsRead = "squidex.apps.{app}.comments.read";
public const string AppCommentsCreate = "squidex.apps.{app}.comments.create"; public const string AppCommentsCreate = "squidex.apps.{app}.comments.create";
public const string AppCommentsUpdate = "squidex.apps.{app}.comments.update"; public const string AppCommentsUpdate = "squidex.apps.{app}.comments.update";
public const string AppCommentsDelete = "squidex.apps.{app}.comments.delete"; public const string AppCommentsDelete = "squidex.apps.{app}.comments.delete";
// Clients
public const string AppClients = "squidex.apps.{app}.clients"; public const string AppClients = "squidex.apps.{app}.clients";
public const string AppClientsRead = "squidex.apps.{app}.clients.read"; public const string AppClientsRead = "squidex.apps.{app}.clients.read";
public const string AppClientsCreate = "squidex.apps.{app}.clients.create"; public const string AppClientsCreate = "squidex.apps.{app}.clients.create";
public const string AppClientsUpdate = "squidex.apps.{app}.clients.update"; public const string AppClientsUpdate = "squidex.apps.{app}.clients.update";
public const string AppClientsDelete = "squidex.apps.{app}.clients.delete"; public const string AppClientsDelete = "squidex.apps.{app}.clients.delete";
// Contributors
public const string AppContributors = "squidex.apps.{app}.contributors"; public const string AppContributors = "squidex.apps.{app}.contributors";
public const string AppContributorsRead = "squidex.apps.{app}.contributors.read"; public const string AppContributorsRead = "squidex.apps.{app}.contributors.read";
public const string AppContributorsAssign = "squidex.apps.{app}.contributors.assign"; public const string AppContributorsAssign = "squidex.apps.{app}.contributors.assign";
public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke"; public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke";
// Languages
public const string AppLanguages = "squidex.apps.{app}.languages"; public const string AppLanguages = "squidex.apps.{app}.languages";
public const string AppLanguagesRead = "squidex.apps.{app}.languages.read"; public const string AppLanguagesRead = "squidex.apps.{app}.languages.read";
public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create"; public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create";
public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update"; public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update";
public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete"; public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete";
// Roles
public const string AppRoles = "squidex.apps.{app}.roles"; public const string AppRoles = "squidex.apps.{app}.roles";
public const string AppRolesRead = "squidex.apps.{app}.roles.read"; public const string AppRolesRead = "squidex.apps.{app}.roles.read";
public const string AppRolesCreate = "squidex.apps.{app}.roles.create"; public const string AppRolesCreate = "squidex.apps.{app}.roles.create";
public const string AppRolesUpdate = "squidex.apps.{app}.roles.update"; public const string AppRolesUpdate = "squidex.apps.{app}.roles.update";
public const string AppRolesDelete = "squidex.apps.{app}.roles.delete"; public const string AppRolesDelete = "squidex.apps.{app}.roles.delete";
public const string AppPatterns = "squidex.apps.{app}.patterns"; // Workflows
public const string AppPatternsRead = "squidex.apps.{app}.patterns.read";
public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create";
public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update";
public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete";
public const string AppWorkflows = "squidex.apps.{app}.workflows"; public const string AppWorkflows = "squidex.apps.{app}.workflows";
public const string AppWorkflowsRead = "squidex.apps.{app}.workflows.read"; public const string AppWorkflowsRead = "squidex.apps.{app}.workflows.read";
public const string AppWorkflowsCreate = "squidex.apps.{app}.workflows.create"; public const string AppWorkflowsCreate = "squidex.apps.{app}.workflows.create";
public const string AppWorkflowsUpdate = "squidex.apps.{app}.workflows.update"; public const string AppWorkflowsUpdate = "squidex.apps.{app}.workflows.update";
public const string AppWorkflowsDelete = "squidex.apps.{app}.workflows.delete"; public const string AppWorkflowsDelete = "squidex.apps.{app}.workflows.delete";
// Backups
public const string AppBackups = "squidex.apps.{app}.backups"; public const string AppBackups = "squidex.apps.{app}.backups";
public const string AppBackupsRead = "squidex.apps.{app}.backups.read"; public const string AppBackupsRead = "squidex.apps.{app}.backups.read";
public const string AppBackupsCreate = "squidex.apps.{app}.backups.create"; public const string AppBackupsCreate = "squidex.apps.{app}.backups.create";
public const string AppBackupsDelete = "squidex.apps.{app}.backups.delete"; public const string AppBackupsDelete = "squidex.apps.{app}.backups.delete";
// Plans
public const string AppPlans = "squidex.apps.{app}.plans"; public const string AppPlans = "squidex.apps.{app}.plans";
public const string AppPlansRead = "squidex.apps.{app}.plans.read"; public const string AppPlansRead = "squidex.apps.{app}.plans.read";
public const string AppPlansChange = "squidex.apps.{app}.plans.change"; public const string AppPlansChange = "squidex.apps.{app}.plans.change";
// Assets
public const string AppAssets = "squidex.apps.{app}.assets"; public const string AppAssets = "squidex.apps.{app}.assets";
public const string AppAssetsRead = "squidex.apps.{app}.assets.read"; public const string AppAssetsRead = "squidex.apps.{app}.assets.read";
public const string AppAssetsCreate = "squidex.apps.{app}.assets.create"; public const string AppAssetsCreate = "squidex.apps.{app}.assets.create";
@ -119,6 +125,7 @@ namespace Squidex.Shared
public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update"; public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update";
public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete"; public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete";
// Rules
public const string AppRules = "squidex.apps.{app}.rules"; public const string AppRules = "squidex.apps.{app}.rules";
public const string AppRulesRead = "squidex.apps.{app}.rules.read"; public const string AppRulesRead = "squidex.apps.{app}.rules.read";
public const string AppRulesEvents = "squidex.apps.{app}.rules.events"; public const string AppRulesEvents = "squidex.apps.{app}.rules.events";
@ -127,6 +134,7 @@ namespace Squidex.Shared
public const string AppRulesDisable = "squidex.apps.{app}.rules.disable"; public const string AppRulesDisable = "squidex.apps.{app}.rules.disable";
public const string AppRulesDelete = "squidex.apps.{app}.rules.delete"; public const string AppRulesDelete = "squidex.apps.{app}.rules.delete";
// Schemas
public const string AppSchemas = "squidex.apps.{app}.schemas"; public const string AppSchemas = "squidex.apps.{app}.schemas";
public const string AppSchemasRead = "squidex.apps.{app}.schemas.read"; public const string AppSchemasRead = "squidex.apps.{app}.schemas.read";
public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create"; public const string AppSchemasCreate = "squidex.apps.{app}.schemas.create";
@ -135,6 +143,7 @@ namespace Squidex.Shared
public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish"; public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish";
public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{name}.delete"; public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{name}.delete";
// Contents
public const string AppContents = "squidex.apps.{app}.contents.{name}"; public const string AppContents = "squidex.apps.{app}.contents.{name}";
public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read"; public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read";
public const string AppContentsReadOwn = "squidex.apps.{app}.contents.{name}.read.own"; public const string AppContentsReadOwn = "squidex.apps.{app}.contents.{name}.read.own";
@ -151,55 +160,11 @@ namespace Squidex.Shared
public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete"; public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete";
public const string AppContentsDeleteOwn = "squidex.apps.{app}.contents.{name}.delete.own"; public const string AppContentsDeleteOwn = "squidex.apps.{app}.contents.{name}.delete.own";
static Permissions()
{
foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (field.IsLiteral && !field.IsInitOnly)
{
var value = field.GetValue(null) as string;
if (value?.StartsWith(App, StringComparison.OrdinalIgnoreCase) == true)
{
if (value.IndexOf("{name}", App.Length, StringComparison.OrdinalIgnoreCase) >= 0)
{
ForAppsSchemaList.Add(value);
}
else
{
ForAppsNonSchemaList.Add(value);
}
}
}
}
}
public static bool Allows(this PermissionSet permissions, string id, string app = Permission.Any, string schema = Permission.Any)
{
var permission = ForApp(id, app, schema);
return permissions.Allows(permission);
}
public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any) public static Permission ForApp(string id, string app = Permission.Any, string schema = Permission.Any)
{ {
Guard.NotNull(id, nameof(id)); Guard.NotNull(id, nameof(id));
return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{name}", schema ?? Permission.Any)); return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{name}", schema ?? Permission.Any));
} }
public static string[] ToAppNames(this PermissionSet permissions)
{
var matching = permissions.Where(x => x.StartsWith("squidex.apps."));
var result =
matching
.Select(x => x.Id.Split('.')).Where(x => x.Length > 2)
.Select(x => x[2])
.Distinct()
.ToArray();
return result;
}
} }
} }

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

@ -139,12 +139,6 @@
<data name="apps.notImage" xml:space="preserve"> <data name="apps.notImage" xml:space="preserve">
<value>Il file non è una immagine</value> <value>Il file non è una immagine</value>
</data> </data>
<data name="apps.patterns.nameAlreadyExists" xml:space="preserve">
<value>Esiste già un pattern con lo stesso nome.</value>
</data>
<data name="apps.patterns.patternAlreadyExists" xml:space="preserve">
<value>Questo pattern esiste già con un altro nome.</value>
</data>
<data name="apps.plans.notFound" xml:space="preserve"> <data name="apps.plans.notFound" xml:space="preserve">
<value>Non esiste un piano con questo id.</value> <value>Non esiste un piano con questo id.</value>
</data> </data>
@ -721,15 +715,6 @@
<data name="history.apps.languagedUpdated" xml:space="preserve"> <data name="history.apps.languagedUpdated" xml:space="preserve">
<value>aggiornata la lingua {[Language]}</value> <value>aggiornata la lingua {[Language]}</value>
</data> </data>
<data name="history.apps.patternAdded" xml:space="preserve">
<value>ha aggiunto pattern {[Name]}</value>
</data>
<data name="history.apps.patternDeleted" xml:space="preserve">
<value>ha eliminato pattern {[PatternId]}</value>
</data>
<data name="history.apps.patternUpdated" xml:space="preserve">
<value>ha modificato pattern {[Name]}</value>
</data>
<data name="history.apps.planChanged" xml:space="preserve"> <data name="history.apps.planChanged" xml:space="preserve">
<value>ha cambiato il piano in {[Plan]}</value> <value>ha cambiato il piano in {[Plan]}</value>
</data> </data>
@ -745,6 +730,9 @@
<data name="history.apps.roleUpdated" xml:space="preserve"> <data name="history.apps.roleUpdated" xml:space="preserve">
<value>ha aggiornato il ruolo {[Name]}</value> <value>ha aggiornato il ruolo {[Name]}</value>
</data> </data>
<data name="history.apps.settingsUpdated" xml:space="preserve">
<value>updated UI settings</value>
</data>
<data name="history.assets.replaced" xml:space="preserve"> <data name="history.assets.replaced" xml:space="preserve">
<value>ha sostituito la risorsa.</value> <value>ha sostituito la risorsa.</value>
</data> </data>

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

@ -139,12 +139,6 @@
<data name="apps.notImage" xml:space="preserve"> <data name="apps.notImage" xml:space="preserve">
<value>Bestand is geen afbeelding</value> <value>Bestand is geen afbeelding</value>
</data> </data>
<data name="apps.patterns.nameAlreadyExists" xml:space="preserve">
<value>Er bestaat al een patroon met dezelfde naam.</value>
</data>
<data name="apps.patterns.patternAlreadyExists" xml:space="preserve">
<value>Dit patroon bestaat al maar met een andere naam.</value>
</data>
<data name="apps.plans.notFound" xml:space="preserve"> <data name="apps.plans.notFound" xml:space="preserve">
<value>Een plan met deze id bestaat niet.</value> <value>Een plan met deze id bestaat niet.</value>
</data> </data>
@ -721,15 +715,6 @@
<data name="history.apps.languagedUpdated" xml:space="preserve"> <data name="history.apps.languagedUpdated" xml:space="preserve">
<value>bijgewerkte taal {[Language]}</value> <value>bijgewerkte taal {[Language]}</value>
</data> </data>
<data name="history.apps.patternAdded" xml:space="preserve">
<value>toegevoegd patroon {[Name]}</value>
</data>
<data name="history.apps.patternDeleted" xml:space="preserve">
<value>verwijderd patroon {[PatternId]}</value>
</data>
<data name="history.apps.patternUpdated" xml:space="preserve">
<value>bijgewerkt patroon {[Name]}</value>
</data>
<data name="history.apps.planChanged" xml:space="preserve"> <data name="history.apps.planChanged" xml:space="preserve">
<value>plan gewijzigd in {[Plan]}</value> <value>plan gewijzigd in {[Plan]}</value>
</data> </data>
@ -745,6 +730,9 @@
<data name="history.apps.roleUpdated" xml:space="preserve"> <data name="history.apps.roleUpdated" xml:space="preserve">
<value>bijgewerkte rol {[Name]}</value> <value>bijgewerkte rol {[Name]}</value>
</data> </data>
<data name="history.apps.settingsUpdated" xml:space="preserve">
<value>updated UI settings</value>
</data>
<data name="history.assets.replaced" xml:space="preserve"> <data name="history.assets.replaced" xml:space="preserve">
<value>item vervangen.</value> <value>item vervangen.</value>
</data> </data>

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

@ -139,12 +139,6 @@
<data name="apps.notImage" xml:space="preserve"> <data name="apps.notImage" xml:space="preserve">
<value>File is not an image</value> <value>File is not an image</value>
</data> </data>
<data name="apps.patterns.nameAlreadyExists" xml:space="preserve">
<value>A pattern with the same name already exists.</value>
</data>
<data name="apps.patterns.patternAlreadyExists" xml:space="preserve">
<value>This pattern already exists but with another name.</value>
</data>
<data name="apps.plans.notFound" xml:space="preserve"> <data name="apps.plans.notFound" xml:space="preserve">
<value>A plan with this id does not exist.</value> <value>A plan with this id does not exist.</value>
</data> </data>
@ -721,15 +715,6 @@
<data name="history.apps.languagedUpdated" xml:space="preserve"> <data name="history.apps.languagedUpdated" xml:space="preserve">
<value>updated language {[Language]}</value> <value>updated language {[Language]}</value>
</data> </data>
<data name="history.apps.patternAdded" xml:space="preserve">
<value>added pattern {[Name]}</value>
</data>
<data name="history.apps.patternDeleted" xml:space="preserve">
<value>deleted pattern {[PatternId]}</value>
</data>
<data name="history.apps.patternUpdated" xml:space="preserve">
<value>updated pattern {[Name]}</value>
</data>
<data name="history.apps.planChanged" xml:space="preserve"> <data name="history.apps.planChanged" xml:space="preserve">
<value>changed plan to {[Plan]}</value> <value>changed plan to {[Plan]}</value>
</data> </data>
@ -745,6 +730,9 @@
<data name="history.apps.roleUpdated" xml:space="preserve"> <data name="history.apps.roleUpdated" xml:space="preserve">
<value>updated role {[Name]}</value> <value>updated role {[Name]}</value>
</data> </data>
<data name="history.apps.settingsUpdated" xml:space="preserve">
<value>updated UI settings</value>
</data>
<data name="history.assets.replaced" xml:space="preserve"> <data name="history.assets.replaced" xml:space="preserve">
<value>replaced asset.</value> <value>replaced asset.</value>
</data> </data>

109
backend/src/Squidex.Web/Resources.cs

@ -11,7 +11,7 @@ using Lazy;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using P = Squidex.Shared.Permissions; using Squidex.Shared;
namespace Squidex.Web namespace Squidex.Web
{ {
@ -20,155 +20,148 @@ namespace Squidex.Web
private readonly Dictionary<(string, string), bool> schemaPermissions = new Dictionary<(string, string), bool>(); private readonly Dictionary<(string, string), bool> schemaPermissions = new Dictionary<(string, string), bool>();
// Contents // Contents
public bool CanReadContent(string schema) => IsAllowedForSchema(P.AppContentsReadOwn, schema); public bool CanReadContent(string schema) => IsAllowedForSchema(Permissions.AppContentsReadOwn, schema);
public bool CanCreateContent(string schema) => IsAllowedForSchema(P.AppContentsCreate, schema); public bool CanCreateContent(string schema) => IsAllowedForSchema(Permissions.AppContentsCreate, schema);
public bool CanCreateContentVersion(string schema) => IsAllowedForSchema(P.AppContentsVersionCreateOwn, schema); public bool CanCreateContentVersion(string schema) => IsAllowedForSchema(Permissions.AppContentsVersionCreateOwn, schema);
public bool CanDeleteContent(string schema) => IsAllowedForSchema(P.AppContentsDeleteOwn, schema); public bool CanDeleteContent(string schema) => IsAllowedForSchema(Permissions.AppContentsDeleteOwn, schema);
public bool CanDeleteContentVersion(string schema) => IsAllowedForSchema(P.AppContentsVersionDeleteOwn, schema); public bool CanDeleteContentVersion(string schema) => IsAllowedForSchema(Permissions.AppContentsVersionDeleteOwn, schema);
public bool CanUpdateContent(string schema) => IsAllowedForSchema(P.AppContentsUpdateOwn, schema); public bool CanUpdateContent(string schema) => IsAllowedForSchema(Permissions.AppContentsUpdateOwn, schema);
// Schemas // Schemas
public bool CanUpdateSchema(string schema) => IsAllowedForSchema(P.AppSchemasDelete, schema); public bool CanUpdateSchema(string schema) => IsAllowedForSchema(Permissions.AppSchemasDelete, schema);
public bool CanUpdateSchemaScripts(string schema) => IsAllowedForSchema(P.AppSchemasScripts, schema); public bool CanUpdateSchemaScripts(string schema) => IsAllowedForSchema(Permissions.AppSchemasScripts, schema);
public bool CanPublishSchema(string schema) => IsAllowedForSchema(P.AppSchemasPublish, schema); public bool CanPublishSchema(string schema) => IsAllowedForSchema(Permissions.AppSchemasPublish, schema);
public bool CanDeleteSchema(string schema) => IsAllowedForSchema(P.AppSchemasDelete, schema); public bool CanDeleteSchema(string schema) => IsAllowedForSchema(Permissions.AppSchemasDelete, schema);
[Lazy] [Lazy]
public bool CanCreateSchema => IsAllowed(P.AppSchemasCreate); public bool CanCreateSchema => IsAllowed(Permissions.AppSchemasCreate);
[Lazy]
public bool CanUpdateSettings => IsAllowed(Permissions.AppUpdateSettings);
// Contributors // Contributors
[Lazy] [Lazy]
public bool CanAssignContributor => IsAllowed(P.AppContributorsAssign); public bool CanAssignContributor => IsAllowed(Permissions.AppContributorsAssign);
[Lazy] [Lazy]
public bool CanRevokeContributor => IsAllowed(P.AppContributorsRevoke); public bool CanRevokeContributor => IsAllowed(Permissions.AppContributorsRevoke);
// Workflows // Workflows
[Lazy] [Lazy]
public bool CanCreateWorkflow => IsAllowed(P.AppWorkflowsCreate); public bool CanCreateWorkflow => IsAllowed(Permissions.AppWorkflowsCreate);
[Lazy] [Lazy]
public bool CanUpdateWorkflow => IsAllowed(P.AppWorkflowsUpdate); public bool CanUpdateWorkflow => IsAllowed(Permissions.AppWorkflowsUpdate);
[Lazy] [Lazy]
public bool CanDeleteWorkflow => IsAllowed(P.AppWorkflowsDelete); public bool CanDeleteWorkflow => IsAllowed(Permissions.AppWorkflowsDelete);
// Roles // Roles
[Lazy] [Lazy]
public bool CanCreateRole => IsAllowed(P.AppRolesCreate); public bool CanCreateRole => IsAllowed(Permissions.AppRolesCreate);
[Lazy] [Lazy]
public bool CanUpdateRole => IsAllowed(P.AppRolesUpdate); public bool CanUpdateRole => IsAllowed(Permissions.AppRolesUpdate);
[Lazy] [Lazy]
public bool CanDeleteRole => IsAllowed(P.AppRolesDelete); public bool CanDeleteRole => IsAllowed(Permissions.AppRolesDelete);
// Languages // Languages
[Lazy] [Lazy]
public bool CanCreateLanguage => IsAllowed(P.AppLanguagesCreate); public bool CanCreateLanguage => IsAllowed(Permissions.AppLanguagesCreate);
[Lazy]
public bool CanUpdateLanguage => IsAllowed(P.AppLanguagesUpdate);
[Lazy]
public bool CanDeleteLanguage => IsAllowed(P.AppLanguagesDelete);
// Patterns
[Lazy]
public bool CanCreatePattern => IsAllowed(P.AppClientsCreate);
[Lazy] [Lazy]
public bool CanUpdatePattern => IsAllowed(P.AppPatternsUpdate); public bool CanUpdateLanguage => IsAllowed(Permissions.AppLanguagesUpdate);
[Lazy] [Lazy]
public bool CanDeletePattern => IsAllowed(P.AppPatternsDelete); public bool CanDeleteLanguage => IsAllowed(Permissions.AppLanguagesDelete);
// Clients // Clients
[Lazy] [Lazy]
public bool CanCreateClient => IsAllowed(P.AppClientsCreate); public bool CanCreateClient => IsAllowed(Permissions.AppClientsCreate);
[Lazy] [Lazy]
public bool CanUpdateClient => IsAllowed(P.AppClientsUpdate); public bool CanUpdateClient => IsAllowed(Permissions.AppClientsUpdate);
[Lazy] [Lazy]
public bool CanDeleteClient => IsAllowed(P.AppClientsDelete); public bool CanDeleteClient => IsAllowed(Permissions.AppClientsDelete);
// Rules // Rules
[Lazy] [Lazy]
public bool CanDisableRule => IsAllowed(P.AppRulesDisable); public bool CanDisableRule => IsAllowed(Permissions.AppRulesDisable);
[Lazy] [Lazy]
public bool CanCreateRule => IsAllowed(P.AppRulesCreate); public bool CanCreateRule => IsAllowed(Permissions.AppRulesCreate);
[Lazy] [Lazy]
public bool CanUpdateRule => IsAllowed(P.AppRulesUpdate); public bool CanUpdateRule => IsAllowed(Permissions.AppRulesUpdate);
[Lazy] [Lazy]
public bool CanDeleteRule => IsAllowed(P.AppRulesDelete); public bool CanDeleteRule => IsAllowed(Permissions.AppRulesDelete);
[Lazy] [Lazy]
public bool CanReadRuleEvents => IsAllowed(P.AppRulesEvents); public bool CanReadRuleEvents => IsAllowed(Permissions.AppRulesEvents);
// Users // Users
[Lazy] [Lazy]
public bool CanReadUsers => IsAllowed(P.AdminUsersRead); public bool CanReadUsers => IsAllowed(Permissions.AdminUsersRead);
[Lazy] [Lazy]
public bool CanCreateUser => IsAllowed(P.AdminUsersCreate); public bool CanCreateUser => IsAllowed(Permissions.AdminUsersCreate);
[Lazy] [Lazy]
public bool CanLockUser => IsAllowed(P.AdminUsersLock); public bool CanLockUser => IsAllowed(Permissions.AdminUsersLock);
[Lazy] [Lazy]
public bool CanUnlockUser => IsAllowed(P.AdminUsersUnlock); public bool CanUnlockUser => IsAllowed(Permissions.AdminUsersUnlock);
[Lazy] [Lazy]
public bool CanUpdateUser => IsAllowed(P.AdminUsersUpdate); public bool CanUpdateUser => IsAllowed(Permissions.AdminUsersUpdate);
// Assets // Assets
[Lazy] [Lazy]
public bool CanUploadAsset => IsAllowed(P.AppAssetsUpload); public bool CanUploadAsset => IsAllowed(Permissions.AppAssetsUpload);
[Lazy] [Lazy]
public bool CanCreateAsset => IsAllowed(P.AppAssetsCreate); public bool CanCreateAsset => IsAllowed(Permissions.AppAssetsCreate);
[Lazy] [Lazy]
public bool CanDeleteAsset => IsAllowed(P.AppAssetsDelete); public bool CanDeleteAsset => IsAllowed(Permissions.AppAssetsDelete);
[Lazy] [Lazy]
public bool CanUpdateAsset => IsAllowed(P.AppAssetsUpdate); public bool CanUpdateAsset => IsAllowed(Permissions.AppAssetsUpdate);
[Lazy] [Lazy]
public bool CanReadAssets => IsAllowed(P.AppAssetsRead); public bool CanReadAssets => IsAllowed(Permissions.AppAssetsRead);
// Events // Events
[Lazy] [Lazy]
public bool CanReadEvents => IsAllowed(P.AdminEventsRead); public bool CanReadEvents => IsAllowed(Permissions.AdminEventsRead);
[Lazy] [Lazy]
public bool CanManageEvents => IsAllowed(P.AdminEventsManage); public bool CanManageEvents => IsAllowed(Permissions.AdminEventsManage);
// Orleans // Orleans
[Lazy] [Lazy]
public bool CanReadOrleans => IsAllowed(P.AdminOrleans); public bool CanReadOrleans => IsAllowed(Permissions.AdminOrleans);
// Backups // Backups
[Lazy] [Lazy]
public bool CanRestoreBackup => IsAllowed(P.AdminEventsRead); public bool CanRestoreBackup => IsAllowed(Permissions.AdminEventsRead);
[Lazy] [Lazy]
public bool CanCreateBackup => IsAllowed(P.AppBackupsCreate); public bool CanCreateBackup => IsAllowed(Permissions.AppBackupsCreate);
[Lazy] [Lazy]
public bool CanDeleteBackup => IsAllowed(P.AppBackupsDelete); public bool CanDeleteBackup => IsAllowed(Permissions.AppBackupsDelete);
[Lazy] [Lazy]
public string? App => GetAppName(); public string? App => GetAppName();
@ -228,7 +221,7 @@ namespace Squidex.Web
} }
} }
var permission = P.ForApp(id, app, schema); var permission = Permissions.ForApp(id, app, schema);
return Context.UserPermissions.Allows(permission) || additional?.Allows(permission) == true; return Context.UserPermissions.Allows(permission) || additional?.Allows(permission) == true;
} }

151
backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -1,151 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures app patterns.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppPatternsController : ApiController
{
public AppPatternsController(ICommandBus commandBus)
: base(commandBus)
{
}
/// <summary>
/// Get app patterns.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Patterns returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// Gets all configured regex patterns for the app with the specified name.
/// </remarks>
[HttpGet]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(PatternsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppPatternsRead)]
[ApiCosts(0)]
public IActionResult GetPatterns(string app)
{
var response = Deferred.Response(() =>
{
return GetResponse(App);
});
Response.Headers[HeaderNames.ETag] = App.ToEtag();
return Ok(response);
}
/// <summary>
/// Create a new app pattern.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">Pattern to be added to the app.</param>
/// <returns>
/// 201 => Pattern created.
/// 400 => Pattern request not valid.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(PatternsDto), 201)]
[ApiPermissionOrAnonymous(Permissions.AppPatternsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request)
{
var command = request.ToAddCommand();
var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetPatterns), new { app }, response);
}
/// <summary>
/// Update an app pattern.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the pattern to be updated.</param>
/// <param name="request">Pattern to be updated for the app.</param>
/// <returns>
/// 200 => Pattern updated.
/// 400 => Pattern request not valid.
/// 404 => Pattern or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(PatternsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppPatternsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutPattern(string app, DomainId id, [FromBody] UpdatePatternDto request)
{
var command = request.ToUpdateCommand(id);
var response = await InvokeCommandAsync(command);
return Ok(response);
}
/// <summary>
/// Delete an app pattern.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the pattern to be deleted.</param>
/// <returns>
/// 200 => Pattern deleted.
/// 404 => Pattern or app not found.
/// </returns>
/// <remarks>
/// Schemas using this pattern will still function using the same Regular Expression.
/// </remarks>
[HttpDelete]
[Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(PatternsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppPatternsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeletePattern(string app, DomainId id)
{
var command = new DeletePattern { PatternId = id };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<PatternsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = GetResponse(result);
return response;
}
private PatternsDto GetResponse(IAppEntity result)
{
return PatternsDto.FromApp(result, Resources);
}
}
}

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -38,24 +39,24 @@ namespace Squidex.Areas.Api.Controllers.Apps
{ {
private static readonly ResizeOptions ResizeOptions = new ResizeOptions { Width = 50, Height = 50, Mode = ResizeMode.Crop }; private static readonly ResizeOptions ResizeOptions = new ResizeOptions { Width = 50, Height = 50, Mode = ResizeMode.Crop };
private readonly IAppImageStore appImageStore; private readonly IAppImageStore appImageStore;
private readonly IAppPlansProvider appPlansProvider;
private readonly IAppProvider appProvider;
private readonly IAssetStore assetStore; private readonly IAssetStore assetStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IAppProvider appProvider;
private readonly IAppPlansProvider appPlansProvider;
public AppsController(ICommandBus commandBus, public AppsController(ICommandBus commandBus,
IAppImageStore appImageStore, IAppImageStore appImageStore,
IAssetStore assetStore, IAppPlansProvider appPlansProvider,
IAssetThumbnailGenerator assetThumbnailGenerator,
IAppProvider appProvider, IAppProvider appProvider,
IAppPlansProvider appPlansProvider) IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator)
: base(commandBus) : base(commandBus)
{ {
this.appImageStore = appImageStore; this.appImageStore = appImageStore;
this.appPlansProvider = appPlansProvider;
this.appProvider = appProvider;
this.assetStore = assetStore; this.assetStore = assetStore;
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
this.appProvider = appProvider;
this.appPlansProvider = appPlansProvider;
} }
/// <summary> /// <summary>
@ -161,13 +162,58 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppUpdate)] [ApiPermissionOrAnonymous(Permissions.AppUpdate)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> UpdateApp(string app, [FromBody] UpdateAppDto request) public async Task<IActionResult> PutApp(string app, [FromBody] UpdateAppDto request)
{ {
var response = await InvokeCommandAsync(request.ToCommand()); var response = await InvokeCommandAsync(request.ToCommand());
return Ok(response); return Ok(response);
} }
/// <summary>
/// Get the app settings.
/// </summary>
/// <param name="app">The name of the app to get the settings for.</param>
/// <returns>
/// 200 => App settingsd returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(0)]
public IActionResult GetAppSettings(string app)
{
var response = Deferred.Response(() =>
{
return AppSettingsDto.FromApp(App, Resources);
});
return Ok(response);
}
/// <summary>
/// Update the app settings.
/// </summary>
/// <param name="app">The name of the app to update.</param>
/// <param name="request">The values to update.</param>
/// <returns>
/// 200 => App updated.
/// 400 => App request not valid.
/// 404 => App not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppUpdate)]
[ApiCosts(0)]
public async Task<IActionResult> PutAppSettings(string app, [FromBody] UpdateAppSettingsDto request)
{
var response = await InvokeCommandAsync(request.ToCommand(), x => AppSettingsDto.FromApp(x, Resources));
return Ok(response);
}
/// <summary> /// <summary>
/// Upload the app image. /// Upload the app image.
/// </summary> /// </summary>
@ -181,7 +227,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost] [HttpPost]
[Route("apps/{app}/image")] [Route("apps/{app}/image")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppUpdateImage)] [ApiPermissionOrAnonymous(Permissions.AppImageUpload)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> UploadImage(string app, IFormFile file) public async Task<IActionResult> UploadImage(string app, IFormFile file)
{ {
@ -272,7 +318,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpDelete] [HttpDelete]
[Route("apps/{app}/image")] [Route("apps/{app}/image")]
[ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppUpdateImage)] [ApiPermissionOrAnonymous(Permissions.AppImageDelete)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> DeleteImage(string app) public async Task<IActionResult> DeleteImage(string app)
{ {
@ -300,16 +346,24 @@ namespace Squidex.Areas.Api.Controllers.Apps
return NoContent(); return NoContent();
} }
private async Task<AppDto> InvokeCommandAsync(ICommand command) private Task<AppDto> InvokeCommandAsync(ICommand command)
{ {
var context = await CommandBus.PublishAsync(command); return InvokeCommandAsync(command, x =>
{
var userOrClientId = HttpContext.User.UserOrClientId()!;
var userOrClientId = HttpContext.User.UserOrClientId()!; var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend);
return AppDto.FromApp(x, userOrClientId, isFrontend, appPlansProvider, Resources);
});
}
var isFrontend = HttpContext.User.IsInClient(DefaultClients.Frontend); private async Task<T> InvokeCommandAsync<T>(ICommand command, Func<IAppEntity, T> converter)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>(); var result = context.Result<IAppEntity>();
var response = AppDto.FromApp(result, userOrClientId, isFrontend, appPlansProvider, Resources); var response = converter(result);
return response; return response;
} }

47
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -22,7 +22,6 @@ using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Web; using Squidex.Web;
using P = Squidex.Shared.Permissions;
#pragma warning disable RECS0033 // Convert 'if' to '||' expression #pragma warning disable RECS0033 // Convert 'if' to '||' expression
@ -122,12 +121,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
permissions = role.Permissions; permissions = role.Permissions;
} }
if (resources.Includes(P.ForApp(P.AppContents, app.Name), permissions)) if (resources.Includes(Shared.Permissions.ForApp(Shared.Permissions.AppContents, app.Name), permissions))
{ {
result.CanAccessContent = true; result.CanAccessContent = true;
} }
if (resources.IsAllowed(P.AppPlansChange, app.Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppPlansChange, app.Name, additional: permissions))
{ {
result.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name; result.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
} }
@ -151,88 +150,88 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
AddDeleteLink("leave", resources.Url<AppContributorsController>(x => nameof(x.DeleteMyself), values)); AddDeleteLink("leave", resources.Url<AppContributorsController>(x => nameof(x.DeleteMyself), values));
} }
if (resources.IsAllowed(P.AppDelete, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppDelete, Name, additional: permissions))
{ {
AddDeleteLink("delete", resources.Url<AppsController>(x => nameof(x.DeleteApp), values)); AddDeleteLink("delete", resources.Url<AppsController>(x => nameof(x.DeleteApp), values));
} }
if (resources.IsAllowed(P.AppUpdate, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppUpdate, Name, additional: permissions))
{ {
AddPutLink("update", resources.Url<AppsController>(x => nameof(x.UpdateApp), values)); AddPutLink("update", resources.Url<AppsController>(x => nameof(x.PutApp), values));
} }
if (resources.IsAllowed(P.AppAssetsRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppAssetsRead, Name, additional: permissions))
{ {
AddGetLink("assets", resources.Url<AssetsController>(x => nameof(x.GetAssets), values)); AddGetLink("assets", resources.Url<AssetsController>(x => nameof(x.GetAssets), values));
} }
if (resources.IsAllowed(P.AppBackupsRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppBackupsRead, Name, additional: permissions))
{ {
AddGetLink("backups", resources.Url<BackupsController>(x => nameof(x.GetBackups), values)); AddGetLink("backups", resources.Url<BackupsController>(x => nameof(x.GetBackups), values));
} }
if (resources.IsAllowed(P.AppClientsRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppClientsRead, Name, additional: permissions))
{ {
AddGetLink("clients", resources.Url<AppClientsController>(x => nameof(x.GetClients), values)); AddGetLink("clients", resources.Url<AppClientsController>(x => nameof(x.GetClients), values));
} }
if (resources.IsAllowed(P.AppContributorsRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppContributorsRead, Name, additional: permissions))
{ {
AddGetLink("contributors", resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values)); AddGetLink("contributors", resources.Url<AppContributorsController>(x => nameof(x.GetContributors), values));
} }
if (resources.IsAllowed(P.AppLanguagesRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppLanguagesRead, Name, additional: permissions))
{ {
AddGetLink("languages", resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values)); AddGetLink("languages", resources.Url<AppLanguagesController>(x => nameof(x.GetLanguages), values));
} }
if (resources.IsAllowed(P.AppPatternsRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppPlansRead, Name, additional: permissions))
{
AddGetLink("patterns", resources.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
}
if (resources.IsAllowed(P.AppPlansRead, Name, additional: permissions))
{ {
AddGetLink("plans", resources.Url<AppPlansController>(x => nameof(x.GetPlans), values)); AddGetLink("plans", resources.Url<AppPlansController>(x => nameof(x.GetPlans), values));
} }
if (resources.IsAllowed(P.AppRolesRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppRolesRead, Name, additional: permissions))
{ {
AddGetLink("roles", resources.Url<AppRolesController>(x => nameof(x.GetRoles), values)); AddGetLink("roles", resources.Url<AppRolesController>(x => nameof(x.GetRoles), values));
} }
if (resources.IsAllowed(P.AppRulesRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppRulesRead, Name, additional: permissions))
{ {
AddGetLink("rules", resources.Url<RulesController>(x => nameof(x.GetRules), values)); AddGetLink("rules", resources.Url<RulesController>(x => nameof(x.GetRules), values));
} }
if (resources.IsAllowed(P.AppSchemasRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppSchemasRead, Name, additional: permissions))
{ {
AddGetLink("schemas", resources.Url<SchemasController>(x => nameof(x.GetSchemas), values)); AddGetLink("schemas", resources.Url<SchemasController>(x => nameof(x.GetSchemas), values));
} }
if (resources.IsAllowed(P.AppWorkflowsRead, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppWorkflowsRead, Name, additional: permissions))
{ {
AddGetLink("workflows", resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values)); AddGetLink("workflows", resources.Url<AppWorkflowsController>(x => nameof(x.GetWorkflows), values));
} }
if (resources.IsAllowed(P.AppSchemasCreate, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppSchemasCreate, Name, additional: permissions))
{ {
AddPostLink("schemas/create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values)); AddPostLink("schemas/create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
} }
if (resources.IsAllowed(P.AppAssetsCreate, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppAssetsCreate, Name, additional: permissions))
{ {
AddPostLink("assets/create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values)); AddPostLink("assets/create", resources.Url<SchemasController>(x => nameof(x.PostSchema), values));
} }
if (resources.IsAllowed(P.AppUpdateImage, Name, additional: permissions)) if (resources.IsAllowed(Shared.Permissions.AppImageUpload, Name, additional: permissions))
{ {
AddPostLink("image/upload", resources.Url<AppsController>(x => nameof(x.UploadImage), values)); AddPostLink("image/upload", resources.Url<AppsController>(x => nameof(x.UploadImage), values));
}
if (resources.IsAllowed(Shared.Permissions.AppImageDelete, Name, additional: permissions))
{
AddDeleteLink("image/delete", resources.Url<AppsController>(x => nameof(x.DeleteImage), values)); AddDeleteLink("image/delete", resources.Url<AppsController>(x => nameof(x.DeleteImage), values));
} }
AddGetLink("settings", resources.Url<AppsController>(x => nameof(x.GetAppSettings), values));
return this; return this;
} }
} }

74
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs

@ -0,0 +1,74 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Validation;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppSettingsDto : Resource
{
/// <summary>
/// The configured app patterns.
/// </summary>
[LocalizedRequired]
public List<PatternDto> Patterns { get; set; }
/// <summary>
/// The configured UI editors.
/// </summary>
[LocalizedRequired]
public List<EditorDto> Editors { get; set; }
/// <summary>
/// Hide the scheduler for content items.
/// </summary>
public bool HideScheduler { get; set; }
/// <summary>
/// The version of the app.
/// </summary>
public long Version { get; set; }
public static AppSettingsDto FromApp(IAppEntity app, Resources resources)
{
var settings = app.Settings;
var result = new AppSettingsDto
{
HideScheduler = settings.HideScheduler,
Patterns =
settings.Patterns
.Select(x => SimpleMapper.Map(x, new PatternDto())).ToList(),
Editors =
settings.Editors
.Select(x => SimpleMapper.Map(x, new EditorDto())).ToList(),
Version = app.Version
};
return result.CreateLinks(resources);
}
private AppSettingsDto CreateLinks(Resources resources)
{
var values = new { app = resources.App };
AddSelfLink(resources.Url<AppsController>(x => nameof(x.GetAppSettings), values));
if (resources.CanUpdateSettings)
{
AddPutLink("update", resources.Url<AppsController>(x => nameof(x.PutAppSettings), values));
}
return this;
}
}
}

26
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/EditorDto.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class EditorDto
{
/// <summary>
/// The name of the editor.
/// </summary>
[LocalizedRequired]
public string Name { get; set; }
/// <summary>
/// The url to the editor.
/// </summary>
[LocalizedRequired]
public string Url { get; set; }
}
}

31
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternDto.cs

@ -5,15 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
public sealed class PatternDto : Resource public sealed class PatternDto
{ {
/// <summary> /// <summary>
/// Unique id of the pattern. /// Unique id of the pattern.
@ -30,35 +27,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The regex pattern. /// The regex pattern.
/// </summary> /// </summary>
[LocalizedRequired] [LocalizedRequired]
public string Pattern { get; set; } public string Regex { get; set; }
/// <summary> /// <summary>
/// The regex message. /// The regex message.
/// </summary> /// </summary>
public string? Message { get; set; } public string? Message { get; set; }
public static PatternDto FromPattern(DomainId id, AppPattern pattern, Resources resources)
{
var result = SimpleMapper.Map(pattern, new PatternDto { Id = id });
return result.CreateLinks(resources);
}
private PatternDto CreateLinks(Resources resources)
{
var values = new { app = resources.App, id = Id };
if (resources.CanUpdatePattern)
{
AddPutLink("update", resources.Url<AppPatternsController>(x => nameof(x.PutPattern), values));
}
if (resources.CanDeletePattern)
{
AddDeleteLink("delete", resources.Url<AppPatternsController>(x => nameof(x.DeletePattern), values));
}
return this;
}
} }
} }

47
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/PatternsDto.cs

@ -1,47 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Validation;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class PatternsDto : Resource
{
/// <summary>
/// The patterns.
/// </summary>
[LocalizedRequired]
public PatternDto[] Items { get; set; }
public static PatternsDto FromApp(IAppEntity app, Resources resources)
{
var result = new PatternsDto
{
Items = app.Patterns.Select(x => PatternDto.FromPattern(x.Key, x.Value, resources)).ToArray()
};
return result.CreateLinks(resources);
}
private PatternsDto CreateLinks(Resources resources)
{
var values = new { app = resources.App };
AddSelfLink(resources.Url<AppPatternsController>(x => nameof(x.GetPatterns), values));
if (resources.CanCreatePattern)
{
AddPostLink("create", resources.Url<AppPatternsController>(x => nameof(x.PostPattern), values));
}
return this;
}
}
}

54
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppSettingsDto.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Collections;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class UpdateAppSettingsDto
{
/// <summary>
/// The configured app patterns.
/// </summary>
[Required]
public List<PatternDto> Patterns { get; set; }
/// <summary>
/// The configured UI editors.
/// </summary>
[Required]
public List<EditorDto> Editors { get; set; }
/// <summary>
/// Hide the scheduler for content items.
/// </summary>
public bool HideScheduler { get; set; }
public UpdateAppSettings ToCommand()
{
return new UpdateAppSettings
{
Settings = new AppSettings
{
HideScheduler = HideScheduler,
Patterns =
Patterns?.Select(x => new Pattern(x.Name, x.Regex)
{
Message = x.Message
}).ToReadOnlyCollection()!,
Editors =
Editors?.Select(x => new Editor(x.Name, x.Url)).ToReadOnlyCollection()!
}
};
}
}
}

44
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs

@ -1,44 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Validation;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public class UpdatePatternDto
{
/// <summary>
/// The name of the suggestion.
/// </summary>
[LocalizedRequired]
public string Name { get; set; }
/// <summary>
/// The regex pattern.
/// </summary>
[LocalizedRequired]
public string Pattern { get; set; }
/// <summary>
/// The regex message.
/// </summary>
public string? Message { get; set; }
public AddPattern ToAddCommand()
{
return SimpleMapper.Map(this, new AddPattern());
}
public UpdatePattern ToUpdateCommand(DomainId id)
{
return SimpleMapper.Map(this, new UpdatePattern { PatternId = id });
}
}
}

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

@ -68,6 +68,8 @@ namespace Squidex.Areas.Api.Controllers.News.Service
} }
} }
result.Features ??= new List<FeatureDto>();
return result; return result;
} }
} }

1
backend/src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Areas.Api.Controllers.UI namespace Squidex.Areas.Api.Controllers.UI
{ {

15
backend/src/Squidex/Config/Domain/AppsServices.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Areas.Api.Controllers.UI; using Squidex.Areas.Api.Controllers.UI;
@ -14,7 +15,7 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.DomainObject;
using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Search; using Squidex.Domain.Apps.Entities.Search;
using Squidex.Infrastructure; using Squidex.Infrastructure.Collections;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {
@ -47,7 +48,7 @@ namespace Squidex.Config.Domain
{ {
var uiOptions = c.GetRequiredService<IOptions<MyUIOptions>>().Value; var uiOptions = c.GetRequiredService<IOptions<MyUIOptions>>().Value;
var result = new InitialPatterns(); var patterns = new List<Pattern>();
if (uiOptions.RegexSuggestions != null) if (uiOptions.RegexSuggestions != null)
{ {
@ -56,12 +57,18 @@ namespace Squidex.Config.Domain
if (!string.IsNullOrWhiteSpace(key) && if (!string.IsNullOrWhiteSpace(key) &&
!string.IsNullOrWhiteSpace(value)) !string.IsNullOrWhiteSpace(value))
{ {
result[DomainId.NewGuid()] = new AppPattern(key, value); patterns.Add(new Pattern(key, value));
} }
} }
} }
return result; return new InitialSettings
{
Settings = new AppSettings
{
Patterns = patterns.ToReadOnlyCollection()
}
};
}); });
} }
} }

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

@ -35,6 +35,9 @@ namespace Squidex.Config.Domain
services.AddTransientAs<ConvertEventStoreAppId>() services.AddTransientAs<ConvertEventStoreAppId>()
.As<IMigration>(); .As<IMigration>();
services.AddTransientAs<CreateAppSettings>()
.As<IMigration>();
services.AddTransientAs<ClearRules>() services.AddTransientAs<ClearRules>()
.As<IMigration>(); .As<IMigration>();

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

@ -48,7 +48,6 @@ namespace Squidex.Config.Domain
new StringEnumConverter(), new StringEnumConverter(),
new SurrogateConverter<AppClients, AppClientsSurrogate>(), new SurrogateConverter<AppClients, AppClientsSurrogate>(),
new SurrogateConverter<AppContributors, AppContributorsSurrogate>(), new SurrogateConverter<AppContributors, AppContributorsSurrogate>(),
new SurrogateConverter<AppPatterns, AppPatternsSurrogate>(),
new SurrogateConverter<ClaimsPrincipal, ClaimsPrinicpalSurrogate>(), new SurrogateConverter<ClaimsPrincipal, ClaimsPrinicpalSurrogate>(),
new SurrogateConverter<FilterNode<IJsonValue>, JsonFilterSurrogate>(), new SurrogateConverter<FilterNode<IJsonValue>, JsonFilterSurrogate>(),
new SurrogateConverter<LanguageConfig, LanguageConfigSurrogate>(), new SurrogateConverter<LanguageConfig, LanguageConfigSurrogate>(),

4
backend/src/Squidex/appsettings.json

@ -163,10 +163,6 @@
* Hide the Local/UTC button * Hide the Local/UTC button
*/ */
"hideDateTimeModeButton": false, "hideDateTimeModeButton": false,
/*
* True to disable scheduled content status changed, for example when you have your own scheduling system.
*/
"disableScheduledChanges": false,
/* /*
* Show the exposed values as information on the apps overview page. * Show the exposed values as information on the apps overview page.

39
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternJsonTests.cs

@ -1,39 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Core.Model.Apps
{
public class AppPatternJsonTests
{
[Fact]
public void Should_serialize_and_deserialize()
{
var patterns = AppPatterns.Empty;
var guid1 = DomainId.NewGuid();
var guid2 = DomainId.NewGuid();
var guid3 = DomainId.NewGuid();
patterns = patterns.Add(guid1, "Name1", "Pattern1", "Default");
patterns = patterns.Add(guid2, "Name2", "Pattern2", "Default");
patterns = patterns.Add(guid3, "Name3", "Pattern3", "Default");
patterns = patterns.Update(guid2, "Name2 Update", "Pattern2 Update", "Default2");
patterns = patterns.Update(guid3, "Name3 Update", "Pattern3 Update", "Default3");
patterns = patterns.Remove(guid1);
var serialized = patterns.SerializeAndDeserialize();
serialized.Should().BeEquivalentTo(patterns);
}
}
}

86
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs

@ -1,86 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure;
using Xunit;
#pragma warning disable SA1310 // Field names must not contain underscore
namespace Squidex.Domain.Apps.Core.Model.Apps
{
public class AppPatternsTests
{
private readonly AppPatterns patterns_0;
private readonly DomainId firstId = DomainId.NewGuid();
private readonly DomainId id = DomainId.NewGuid();
public AppPatternsTests()
{
patterns_0 = AppPatterns.Empty.Add(firstId, "Default", "Default Pattern", "Message");
}
[Fact]
public void Should_add_pattern()
{
var patterns_1 = patterns_0.Add(id, "NewPattern", "New Pattern", "Message");
patterns_1[id].Should().BeEquivalentTo(new AppPattern("NewPattern", "New Pattern") with { Message = "Message" });
}
[Fact]
public void Should_do_nothing_if_adding_pattern_with_same_id()
{
var patterns_1 = patterns_0.Add(firstId, "Default", "Default Pattern", "Message");
Assert.Same(patterns_1, patterns_0);
}
[Fact]
public void Should_update_pattern()
{
var patterns_1 = patterns_0.Update(firstId, "UpdatePattern", "Update Pattern");
patterns_1[firstId].Should().BeEquivalentTo(new AppPattern("UpdatePattern", "Update Pattern"));
}
[Fact]
public void Should_return_same_patterns_if_pattern_is_updated_with_same_values()
{
var patterns_1 = patterns_0.Update(firstId, "Default", "Default Pattern", "Message");
Assert.Same(patterns_0, patterns_1);
}
[Fact]
public void Should_return_same_patterns_if_pattern_to_update_not_found()
{
var patterns_1 = patterns_0.Update(id, "NewPattern", "NewPattern", "Message");
Assert.Same(patterns_0, patterns_1);
}
[Fact]
public void Should_remove_pattern()
{
var patterns_1 = patterns_0.Add(id, "Name1", "Pattern1");
var patterns_2 = patterns_1.Remove(firstId);
Assert.Equal(id, patterns_2.Keys.Single());
}
[Fact]
public void Should_do_nothing_if_remove_pattern_not_found()
{
var patterns_1 = patterns_0.Remove(id);
Assert.Same(patterns_0, patterns_1);
}
}
}

8
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs

@ -151,7 +151,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
Assert.False(Roles.IsDefault(firstRole)); Assert.False(Roles.IsDefault(firstRole));
} }
[InlineData("Developer", 7)] [InlineData("Developer", 6)]
[InlineData("Editor", 4)] [InlineData("Editor", 4)]
[InlineData("Reader", 2)] [InlineData("Reader", 2)]
[InlineData("Owner", 1)] [InlineData("Owner", 1)]
@ -173,9 +173,9 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
Assert.Equal(permissionCount, result!.Permissions.Count); Assert.Equal(permissionCount, result!.Permissions.Count);
} }
[InlineData("Developer", 17)] [InlineData("Developer", 15)]
[InlineData("Editor", 14)] [InlineData("Editor", 13)]
[InlineData("Reader", 13)] [InlineData("Reader", 12)]
[InlineData("Owner", 1)] [InlineData("Owner", 1)]
[Theory] [Theory]
public void Should_add_extra_permissions_for_frontend_client(string name, int permissionCount) public void Should_add_extra_permissions_for_frontend_client(string name, int permissionCount)

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

@ -59,7 +59,6 @@ namespace Squidex.Domain.Apps.Core.TestHelpers
new StringEnumConverter(), new StringEnumConverter(),
new SurrogateConverter<AppClients, AppClientsSurrogate>(), new SurrogateConverter<AppClients, AppClientsSurrogate>(),
new SurrogateConverter<AppContributors, AppContributorsSurrogate>(), new SurrogateConverter<AppContributors, AppContributorsSurrogate>(),
new SurrogateConverter<AppPatterns, AppPatternsSurrogate>(),
new SurrogateConverter<ClaimsPrincipal, ClaimsPrinicpalSurrogate>(), new SurrogateConverter<ClaimsPrincipal, ClaimsPrinicpalSurrogate>(),
new SurrogateConverter<FilterNode<IJsonValue>, JsonFilterSurrogate>(), new SurrogateConverter<FilterNode<IJsonValue>, JsonFilterSurrogate>(),
new SurrogateConverter<LanguageConfig, LanguageConfigSurrogate>(), new SurrogateConverter<LanguageConfig, LanguageConfigSurrogate>(),

17
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs

@ -94,23 +94,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
Assert.Empty(result); Assert.Empty(result);
} }
[Fact]
public async Task Should_return_patterns_result_if_matching_and_permission_given()
{
var permission = Permissions.ForApp(Permissions.AppPatternsRead, appId.Name);
var ctx = ContextWithPermission(permission.Id);
A.CallTo(() => urlGenerator.PatternsUI(appId))
.Returns("patterns-url");
var result = await sut.SearchAsync("patterns", ctx);
result.Should().BeEquivalentTo(
new SearchResults()
.Add("Patterns", SearchResultType.Setting, "patterns-url"));
}
[Fact] [Fact]
public async Task Should_not_return_patterns_result_if_user_has_no_permission() public async Task Should_not_return_patterns_result_if_user_has_no_permission()
{ {

112
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs

@ -37,10 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
private readonly string planIdFree = "free"; private readonly string planIdFree = "free";
private readonly AppDomainObject sut; private readonly AppDomainObject sut;
private readonly DomainId workflowId = DomainId.NewGuid(); private readonly DomainId workflowId = DomainId.NewGuid();
private readonly DomainId patternId1 = DomainId.NewGuid(); private readonly InitialSettings initialSettings;
private readonly DomainId patternId2 = DomainId.NewGuid();
private readonly DomainId patternId3 = DomainId.NewGuid();
private readonly InitialPatterns initialPatterns;
protected override DomainId Id protected override DomainId Id
{ {
@ -63,13 +60,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
A.CallTo(() => appPlansProvider.GetPlan(planIdPaid)) A.CallTo(() => appPlansProvider.GetPlan(planIdPaid))
.Returns(new ConfigAppLimitsPlan { Id = planIdPaid, MaxContributors = 30 }); .Returns(new ConfigAppLimitsPlan { Id = planIdPaid, MaxContributors = 30 });
initialPatterns = new InitialPatterns // Create a non-empty setting, otherwise the event is not raised as it does not change the domain object.
initialSettings = new InitialSettings
{ {
{ patternId1, new AppPattern("Number", "[0-9]") }, Settings = new AppSettings
{ patternId2, new AppPattern("Numbers", "[0-9]*") } {
HideScheduler = true
}
}; };
sut = new AppDomainObject(PersistenceFactory, A.Dummy<ISemanticLog>(), initialPatterns, appPlansProvider, appPlansBillingManager, userResolver); sut = new AppDomainObject(PersistenceFactory, A.Dummy<ISemanticLog>(), initialSettings, appPlansProvider, appPlansBillingManager, userResolver);
sut.Setup(Id); sut.Setup(Id);
} }
@ -97,8 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppCreated { Name = AppName }), CreateEvent(new AppCreated { Name = AppName }),
CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }), CreateEvent(new AppContributorAssigned { ContributorId = Actor.Identifier, Role = Role.Owner }),
CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }), CreateEvent(new AppSettingsUpdated { Settings = initialSettings.Settings })
CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" })
); );
} }
@ -115,9 +114,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppCreated { Name = AppName }, true), CreateEvent(new AppCreated { Name = AppName }, true), // Must be with client actor.
CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }, true), CreateEvent(new AppSettingsUpdated { Settings = initialSettings.Settings }, true)
CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" }, true)
); );
} }
@ -137,7 +135,31 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppUpdated { Label = "my-label", Description = "my-description" }) CreateEvent(new AppUpdated { Label = command.Label, Description = command.Description })
);
}
[Fact]
public async Task UpdateSettings_should_create_event_and_update_settings()
{
var settings = new AppSettings
{
HideDateTimeQuickButtons = true
};
var command = new UpdateAppSettings { Settings = settings };
await ExecuteCreateAsync();
var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(settings, sut.Snapshot.Settings);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppSettingsUpdated { Settings = settings })
); );
} }
@ -601,63 +623,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
); );
} }
[Fact]
public async Task AddPattern_should_create_events_and_add_pattern()
{
var command = new AddPattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" };
await ExecuteCreateAsync();
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppPatternAdded { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" })
);
}
[Fact]
public async Task DeletePattern_should_create_events_and_update_pattern()
{
var command = new DeletePattern { PatternId = patternId3 };
await ExecuteCreateAsync();
await ExecuteAddPatternAsync();
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppPatternDeleted { PatternId = patternId3 })
);
}
[Fact]
public async Task UpdatePattern_should_create_events_and_remove_pattern()
{
var command = new UpdatePattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" };
await ExecuteCreateAsync();
await ExecuteAddPatternAsync();
var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppPatternUpdated { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" })
);
}
[Fact] [Fact]
public async Task ArchiveApp_should_create_events_and_update_archived_flag() public async Task ArchiveApp_should_create_events_and_update_archived_flag()
{ {
@ -690,11 +655,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return PublishAsync(new UploadAppImage { File = new NoopAssetFile() }); return PublishAsync(new UploadAppImage { File = new NoopAssetFile() });
} }
private Task ExecuteAddPatternAsync()
{
return PublishAsync(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" });
}
private Task ExecuteAssignContributorAsync() private Task ExecuteAssignContributorAsync()
{ {
return PublishAsync(new AssignContributor { ContributorId = contributorId, Role = Role.Editor }); return PublishAsync(new AssignContributor { ContributorId = contributorId, Role = Role.Editor });

192
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppPatternsTests.cs

@ -1,192 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using FakeItEasy;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
using Xunit;
#pragma warning disable SA1310 // Field names must not contain underscore
namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
{
public class GuardAppPatternsTests : IClassFixture<TranslationsFixture>
{
private readonly DomainId patternId = DomainId.NewGuid();
private readonly AppPatterns patterns_0 = AppPatterns.Empty;
[Fact]
public void CanAdd_should_throw_exception_if_name_empty()
{
var command = new AddPattern { PatternId = patternId, Name = string.Empty, Pattern = ".*" };
ValidationAssert.Throws(() => GuardAppPatterns.CanAdd(command, App(patterns_0)),
new ValidationError("Name is required.", "Name"));
}
[Fact]
public void CanAdd_should_throw_exception_if_pattern_empty()
{
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = string.Empty };
ValidationAssert.Throws(() => GuardAppPatterns.CanAdd(command, App(patterns_0)),
new ValidationError("Pattern is required.", "Pattern"));
}
[Fact]
public void CanAdd_should_throw_exception_if_pattern_not_valid()
{
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = "[0-9{1}" };
ValidationAssert.Throws(() => GuardAppPatterns.CanAdd(command, App(patterns_0)),
new ValidationError("Pattern is not a valid value.", "Pattern"));
}
[Fact]
public void CanAdd_should_throw_exception_if_name_exists()
{
var patterns_1 = patterns_0.Add(DomainId.NewGuid(), "any", "[a-z]", "Message");
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = ".*" };
ValidationAssert.Throws(() => GuardAppPatterns.CanAdd(command, App(patterns_1)),
new ValidationError("A pattern with the same name already exists."));
}
[Fact]
public void CanAdd_should_throw_exception_if_pattern_exists()
{
var patterns_1 = patterns_0.Add(DomainId.NewGuid(), "any", "[a-z]", "Message");
var command = new AddPattern { PatternId = patternId, Name = "other", Pattern = "[a-z]" };
ValidationAssert.Throws(() => GuardAppPatterns.CanAdd(command, App(patterns_1)),
new ValidationError("This pattern already exists but with another name."));
}
[Fact]
public void CanAdd_should_not_throw_exception_if_command_is_valid()
{
var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = ".*" };
GuardAppPatterns.CanAdd(command, App(patterns_0));
}
[Fact]
public void CanDelete_should_throw_exception_if_pattern_not_found()
{
var command = new DeletePattern { PatternId = patternId };
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppPatterns.CanDelete(command, App(patterns_0)));
}
[Fact]
public void CanDelete_should_not_throw_exception_if_command_is_valid()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new DeletePattern { PatternId = patternId };
GuardAppPatterns.CanDelete(command, App(patterns_1));
}
[Fact]
public void CanUpdate_should_throw_exception_if_name_empty()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new UpdatePattern { PatternId = patternId, Name = string.Empty, Pattern = ".*" };
ValidationAssert.Throws(() => GuardAppPatterns.CanUpdate(command, App(patterns_1)),
new ValidationError("Name is required.", "Name"));
}
[Fact]
public void CanUpdate_should_throw_exception_if_pattern_empty()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new UpdatePattern { PatternId = patternId, Name = "any", Pattern = string.Empty };
ValidationAssert.Throws(() => GuardAppPatterns.CanUpdate(command, App(patterns_1)),
new ValidationError("Pattern is required.", "Pattern"));
}
[Fact]
public void CanUpdate_should_throw_exception_if_pattern_not_valid()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new UpdatePattern { PatternId = patternId, Name = "any", Pattern = "[0-9{1}" };
ValidationAssert.Throws(() => GuardAppPatterns.CanUpdate(command, App(patterns_1)),
new ValidationError("Pattern is not a valid value.", "Pattern"));
}
[Fact]
public void CanUpdate_should_throw_exception_if_name_exists()
{
var id1 = DomainId.NewGuid();
var id2 = DomainId.NewGuid();
var patterns_1 = patterns_0.Add(id1, "Pattern1", "[0-5]", "Message");
var patterns_2 = patterns_1.Add(id2, "Pattern2", "[0-4]", "Message");
var command = new UpdatePattern { PatternId = id2, Name = "Pattern1", Pattern = "[0-4]" };
ValidationAssert.Throws(() => GuardAppPatterns.CanUpdate(command, App(patterns_2)),
new ValidationError("A pattern with the same name already exists."));
}
[Fact]
public void CanUpdate_should_throw_exception_if_pattern_exists()
{
var id1 = DomainId.NewGuid();
var id2 = DomainId.NewGuid();
var patterns_1 = patterns_0.Add(id1, "Pattern1", "[0-5]", "Message");
var patterns_2 = patterns_1.Add(id2, "Pattern2", "[0-4]", "Message");
var command = new UpdatePattern { PatternId = id2, Name = "Pattern2", Pattern = "[0-5]" };
ValidationAssert.Throws(() => GuardAppPatterns.CanUpdate(command, App(patterns_2)),
new ValidationError("This pattern already exists but with another name."));
}
[Fact]
public void CanUpdate_should_throw_exception_if_pattern_does_not_exists()
{
var command = new UpdatePattern { PatternId = patternId, Name = "Pattern1", Pattern = ".*" };
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppPatterns.CanUpdate(command, App(patterns_0)));
}
[Fact]
public void CanUpdate_should_not_throw_exception_if_pattern_exist_with_valid_command()
{
var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message");
var command = new UpdatePattern { PatternId = patternId, Name = "Pattern1", Pattern = ".*" };
GuardAppPatterns.CanUpdate(command, App(patterns_1));
}
private static IAppEntity App(AppPatterns patterns)
{
var app = A.Fake<IAppEntity>();
A.CallTo(() => app.Patterns)
.Returns(patterns);
return app;
}
}
}

121
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs

@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Squidex.Shared.Users; using Squidex.Shared.Users;
using Xunit; using Xunit;
@ -127,6 +128,126 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
GuardApp.CanChangePlan(command, App(plan), appPlans); GuardApp.CanChangePlan(command, App(plan), appPlans);
} }
[Fact]
public void CanUpdateSettings_should_throw_exception_if_settings_is_null()
{
var command = new UpdateAppSettings();
ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command),
new ValidationError("Settings is required.", "Settings"));
}
[Fact]
public void CanUpdateSettings_should_throw_exception_if_patterns_is_null()
{
var command = new UpdateAppSettings
{
Settings = new AppSettings
{
Patterns = null!
}
};
ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command),
new ValidationError("Patterns is required.", "Settings.Patterns"));
}
[Fact]
public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_name()
{
var command = new UpdateAppSettings
{
Settings = new AppSettings
{
Patterns = ReadOnlyCollection.Create(
new Pattern(null!, "[a-z]"))
}
};
ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command),
new ValidationError("Name is required.", "Settings.Patterns[0].Name"));
}
[Fact]
public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_regex()
{
var command = new UpdateAppSettings
{
Settings = new AppSettings
{
Patterns = ReadOnlyCollection.Create(
new Pattern("name", null!))
}
};
ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command),
new ValidationError("Regex is required.", "Settings.Patterns[0].Regex"));
}
[Fact]
public void CanUpdateSettings_should_throw_exception_if_editors_is_null()
{
var command = new UpdateAppSettings
{
Settings = new AppSettings
{
Editors = null!
}
};
ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command),
new ValidationError("Editors is required.", "Settings.Editors"));
}
[Fact]
public void CanUpdateSettings_should_throw_exception_if_editors_has_null_name()
{
var command = new UpdateAppSettings
{
Settings = new AppSettings
{
Editors = ReadOnlyCollection.Create(
new Editor(null!, "[a-z]"))
}
};
ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command),
new ValidationError("Name is required.", "Settings.Editors[0].Name"));
}
[Fact]
public void CanUpdateSettings_should_throw_exception_if_patterns_has_null_url()
{
var command = new UpdateAppSettings
{
Settings = new AppSettings
{
Editors = ReadOnlyCollection.Create(
new Editor("name", null!))
}
};
ValidationAssert.Throws(() => GuardApp.CanUpdateSettings(command),
new ValidationError("Url is required.", "Settings.Editors[0].Url"));
}
[Fact]
public void CanUpdateSettings_should_not_throw_exception_if_setting_is_valid()
{
var command = new UpdateAppSettings
{
Settings = new AppSettings
{
Patterns = ReadOnlyCollection.Create(
new Pattern("name", "[a-z]")),
Editors = ReadOnlyCollection.Create(
new Editor("name", "url/to/editor"))
}
};
GuardApp.CanUpdateSettings(command);
}
private static IAppEntity App(AppPlan? plan) private static IAppEntity App(AppPlan? plan)
{ {
var app = A.Fake<IAppEntity>(); var app = A.Fake<IAppEntity>();

10
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs

@ -60,8 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
var workflows = Workflows.Empty var workflows = Workflows.Empty
.Add(id1, "workflow1") .Add(id1, "workflow1")
.Add(id2, "workflow2") .Add(id2, "workflow2")
.Update(id1, new Workflow(default, Workflow.EmptySteps, new List<DomainId> { schemaId.Id })) .Update(id1, new Workflow(default, null, new List<DomainId> { schemaId.Id }))
.Update(id2, new Workflow(default, Workflow.EmptySteps, new List<DomainId> { schemaId.Id })); .Update(id2, new Workflow(default, null, new List<DomainId> { schemaId.Id }));
var errors = await sut.ValidateAsync(appId.Id, workflows); var errors = await sut.ValidateAsync(appId.Id, workflows);
@ -79,8 +79,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
var workflows = Workflows.Empty var workflows = Workflows.Empty
.Add(id1, "workflow1") .Add(id1, "workflow1")
.Add(id2, "workflow2") .Add(id2, "workflow2")
.Update(id1, new Workflow(default, Workflow.EmptySteps, new List<DomainId> { oldSchemaId })) .Update(id1, new Workflow(default, null, new List<DomainId> { oldSchemaId }))
.Update(id2, new Workflow(default, Workflow.EmptySteps, new List<DomainId> { oldSchemaId })); .Update(id2, new Workflow(default, null, new List<DomainId> { oldSchemaId }));
var errors = await sut.ValidateAsync(appId.Id, workflows); var errors = await sut.ValidateAsync(appId.Id, workflows);
@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var workflows = Workflows.Empty var workflows = Workflows.Empty
.Add(id1, "workflow1") .Add(id1, "workflow1")
.Add(id2, "workflow2") .Add(id2, "workflow2")
.Update(id1, new Workflow(default, Workflow.EmptySteps, new List<DomainId> { schemaId.Id })); .Update(id1, new Workflow(default, null, new List<DomainId> { schemaId.Id }));
var errors = await sut.ValidateAsync(appId.Id, workflows); var errors = await sut.ValidateAsync(appId.Id, workflows);

72
backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs

@ -40,16 +40,16 @@ namespace TestSuite.ApiTests
[Fact] [Fact]
public async Task Should_manage_app_properties() public async Task Should_manage_app_properties()
{ {
var appLabel = Guid.NewGuid().ToString(); var newLabel = Guid.NewGuid().ToString();
var appDescription = Guid.NewGuid().ToString(); var newDescription = Guid.NewGuid().ToString();
// STEP 1: Update app // STEP 1: Update app
var updateRequest = new UpdateAppDto { Label = appLabel, Description = appDescription }; var updateRequest = new UpdateAppDto { Label = newLabel, Description = newDescription };
var app_1 = await _.Apps.UpdateAppAsync(_.AppName, updateRequest); var app_1 = await _.Apps.PutAppAsync(_.AppName, updateRequest);
Assert.Equal(appLabel, app_1.Label); Assert.Equal(newLabel, app_1.Label);
Assert.Equal(appDescription, app_1.Description); Assert.Equal(newDescription, app_1.Description);
} }
[Fact] [Fact]
@ -252,40 +252,6 @@ namespace TestSuite.ApiTests
Assert.DoesNotContain(roles_4.Items, x => x.Name == roleName); Assert.DoesNotContain(roles_4.Items, x => x.Name == roleName);
} }
[Fact]
public async Task Should_manage_patterns()
{
var patternName = Guid.NewGuid().ToString();
var patternRegex1 = Guid.NewGuid().ToString();
var patternRegex2 = Guid.NewGuid().ToString();
// STEP 1: Add pattern.
var createRequest = new UpdatePatternDto { Name = patternName, Pattern = patternRegex1 };
var patterns_1 = await _.Apps.PostPatternAsync(_.AppName, createRequest);
var pattern_1 = patterns_1.Items.Single(x => x.Name == patternName);
// Should return pattern with correct regex.
Assert.Equal(patternRegex1, pattern_1.Pattern);
// STEP 2: Update pattern.
var updateRequest = new UpdatePatternDto { Name = patternName, Pattern = patternRegex2 };
var patterns_2 = await _.Apps.PutPatternAsync(_.AppName, pattern_1.Id, updateRequest);
var pattern_2 = patterns_2.Items.Single(x => x.Name == patternName);
// Should return pattern with correct regex.
Assert.Equal(patternRegex2, pattern_2.Pattern);
// STEP 3: Remove pattern.
var patterns_3 = await _.Apps.DeletePatternAsync(_.AppName, pattern_2.Id);
// Should not return deleted pattern.
Assert.DoesNotContain(patterns_3.Items, x => x.Id == pattern_2.Id);
}
[Fact] [Fact]
public async Task Should_manage_languages() public async Task Should_manage_languages()
{ {
@ -365,5 +331,31 @@ namespace TestSuite.ApiTests
Assert.Equal(new string[] { "it" }, languageDE_5.Fallback.ToArray()); Assert.Equal(new string[] { "it" }, languageDE_5.Fallback.ToArray());
Assert.Equal(new string[] { "it", "de", "en" }, languages_5.Items.Select(x => x.Iso2Code).ToArray()); Assert.Equal(new string[] { "it", "de", "en" }, languages_5.Items.Select(x => x.Iso2Code).ToArray());
} }
[Fact]
public async Task Should_manage_settings()
{
// STEP 1: Get initial settings.
var settings_0 = await _.Apps.GetAppSettingsAsync(_.AppName);
Assert.NotEmpty(settings_0.Patterns);
Assert.Empty(settings_0.Editors);
// STEP 2: Update settings with new state.
var updateRequest = new UpdateAppSettingsDto
{
Patterns = settings_0.Patterns,
Editors = new List<EditorDto>
{
new EditorDto { Name = "editor", Url = "http://squidex.io/path/to/editor" }
}
};
var settings_1 = await _.Apps.PutAppSettingsAsync(_.AppName, updateRequest);
Assert.NotEmpty(settings_1.Patterns);
Assert.NotEmpty(settings_1.Editors);
}
} }
} }

2
backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

@ -4,7 +4,7 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />

2
backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj

@ -4,7 +4,7 @@
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />

4
backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -5,13 +5,13 @@
<RootNamespace>TestSuite</RootNamespace> <RootNamespace>TestSuite</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fody" Version="6.4.0"> <PackageReference Include="Fody" Version="6.5.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Lazy.Fody" Version="1.9.0" PrivateAssets="all" /> <PackageReference Include="Lazy.Fody" Version="1.9.0" PrivateAssets="all" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="6.19.0" /> <PackageReference Include="Squidex.ClientLibrary" Version="7.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup> </ItemGroup>

7
frontend/app-config/webpack.config.js

@ -6,7 +6,7 @@ function root() {
var newArgs = Array.prototype.slice.call(arguments, 0); var newArgs = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [appRoot].concat(newArgs)); return path.join.apply(path, [appRoot].concat(newArgs));
}; }
const plugins = { const plugins = {
// https://github.com/webpack-contrib/mini-css-extract-plugin // https://github.com/webpack-contrib/mini-css-extract-plugin
@ -440,7 +440,10 @@ module.exports = function (env) {
}, { }, {
loader: 'postcss-loader' loader: 'postcss-loader'
}, { }, {
loader: 'sass-loader?sourceMap' loader: 'sass-loader',
options: {
sourceMap: true
}
}], }],
/* /*
* Do not include component styles. * Do not include component styles.

2
frontend/app/app.component.ts

@ -13,5 +13,5 @@ import { Component } from '@angular/core';
templateUrl: './app.component.html' templateUrl: './app.component.html'
}) })
export class AppComponent { export class AppComponent {
public isLoaded = false; public isLoaded?: boolean | null;
} }

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

@ -1,6 +1,6 @@
<sqx-title message="i18n:common.clusterPageTitle"></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>
<iframe src="./orleans"></iframe> <iframe src="./orleans"></iframe>
</div> </div>

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

@ -1,4 +1,4 @@
<tr [class.faulted]="eventConsumer.error && eventConsumer.error?.length > 0"> <tr [class.faulted]="eventConsumer.error && eventConsumer.error && eventConsumer.error.length > 0">
<td class="cell-auto"> <td class="cell-auto">
<span class="truncate"> <span class="truncate">
<i class="faulted-icon icon icon-bug" (click)="error.emit()" [class.hidden]="!eventConsumer.error || eventConsumer.error?.length === 0"></i> <i class="faulted-icon icon icon-bug" (click)="error.emit()" [class.hidden]="!eventConsumer.error || eventConsumer.error?.length === 0"></i>

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

@ -1,6 +1,6 @@
<sqx-title message="i18n:eventConsumers.pageTitle"></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>
{{ 'common.consumers' | sqxTranslate }} {{ 'common.consumers' | sqxTranslate }}
</ng-container> </ng-container>
@ -14,7 +14,7 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<sqx-list-view [isLoading]="eventConsumersState.isLoading | async" table="true"> <sqx-list-view [isLoading]="eventConsumersState.isLoading | async" [table]="true">
<ng-container header> <ng-container header>
<table class="table table-items table-fixed" #header> <table class="table table-items table-fixed" #header>
<thead> <thead>

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

@ -2,7 +2,7 @@
<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="i18n:users.editPageTitle"></sqx-title> <sqx-title message="i18n:users.editPageTitle"></sqx-title>

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

@ -17,7 +17,7 @@ import { ResourceOwner } from '@app/shared';
templateUrl: './user-page.component.html' templateUrl: './user-page.component.html'
}) })
export class UserPageComponent extends ResourceOwner implements OnInit { export class UserPageComponent extends ResourceOwner implements OnInit {
public isEditable = true; public isEditable = false;
public user?: UserDto | null; public user?: UserDto | null;
public userForm = new UserForm(this.formBuilder); public userForm = new UserForm(this.formBuilder);

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

@ -1,6 +1,6 @@
<sqx-title message="i18n:users.listPageTitle"></sqx-title> <sqx-title message="i18n:users.listPageTitle"></sqx-title>
<sqx-panel desiredWidth="50rem" grid="true" closeQueryParamsHandling="none"> <sqx-panel desiredWidth="50rem" [grid]="true" closeQueryParamsHandling="">
<ng-container title> <ng-container title>
{{ 'users.listTitle' | sqxTranslate }} {{ 'users.listTitle' | sqxTranslate }}
</ng-container> </ng-container>
@ -27,7 +27,7 @@
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<sqx-list-view [isLoading]="usersState.isLoading | async" table="true"> <sqx-list-view [isLoading]="usersState.isLoading | async" [table]="true">
<ng-container header> <ng-container header>
<table class="table table-items table-fixed" #header> <table class="table table-items table-fixed" #header>
<thead> <thead>

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

@ -130,14 +130,16 @@ describe('EventConsumersService', () => {
expect(eventConsumer!).toEqual(createEventConsumer(12)); expect(eventConsumer!).toEqual(createEventConsumer(12));
})); }));
function eventConsumerResponse(id: number) { function eventConsumerResponse(id: number, suffix = '') {
const key = `${id}${suffix}`;
return { return {
name: `event-consumer${id}`, name: `event-consumer${id}`,
position: `position${id}`, position: `position${key}`,
count: id, count: id,
isStopped: true, isStopped: true,
isResetting: true, isResetting: true,
error: `failure${id}`, error: `failure${key}`,
_links: { _links: {
reset: { method: 'PUT', href: `/event-consumers/${id}/reset` } reset: { method: 'PUT', href: `/event-consumers/${id}/reset` }
} }
@ -150,11 +152,13 @@ export function createEventConsumer(id: number, suffix = '') {
reset: { method: 'PUT', href: `/event-consumers/${id}/reset` } reset: { method: 'PUT', href: `/event-consumers/${id}/reset` }
}; };
const key = `${id}${suffix}`;
return new EventConsumerDto(links, return new EventConsumerDto(links,
`event-consumer${id}`, `event-consumer${id}`,
id, id,
true, true,
true, true,
`failure${id}${suffix}`, `failure${key}`,
`position${id}${suffix}`); `position${key}`);
} }

18
frontend/app/features/administration/services/users.service.spec.ts

@ -221,13 +221,15 @@ describe('UsersService', () => {
req.flush({}); req.flush({});
})); }));
function userResponse(id: number) { function userResponse(id: number, suffix = '') {
const key = `${id}${suffix}`;
return { return {
id: `${id}`, id: `${id}`,
email: `user${id}@domain.com`, email: `user${key}@domain.com`,
displayName: `user${id}`, displayName: `user${key}`,
permissions: [ permissions: [
`Permission${id}` `Permission${key}`
], ],
isLocked: true, isLocked: true,
_links: { _links: {
@ -244,12 +246,14 @@ export function createUser(id: number, suffix = '') {
update: { method: 'PUT', href: `/users/${id}` } update: { method: 'PUT', href: `/users/${id}` }
}; };
const key = `${id}${suffix}`;
return new UserDto(links, return new UserDto(links,
`${id}`, `${id}`,
`user${id}${suffix}@domain.com`, `user${key}@domain.com`,
`user${id}${suffix}`, `user${key}`,
[ [
`Permission${id}${suffix}` `Permission${key}`
], ],
true); true);
} }

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

@ -41,19 +41,13 @@ export class UserDto {
} }
} }
export interface CreateUserDto { type Permissions = readonly string[];
readonly email: string;
readonly displayName: string;
readonly permissions: ReadonlyArray<string>;
readonly password: string;
}
export interface UpdateUserDto { export type CreateUserDto =
readonly email: string; Readonly<{ email: string, displayName: string, permissions: Permissions, password: string }>;
readonly displayName: string;
readonly permissions: ReadonlyArray<string>; export type UpdateUserDto =
readonly password?: string; Partial<CreateUserDto>;
}
@Injectable() @Injectable()
export class UsersService { export class UsersService {

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

@ -166,7 +166,7 @@ export class UsersState extends State<Snapshot> {
public delete(user: UserDto) { public delete(user: UserDto) {
return this.usersService.deleteUser(user).pipe( return this.usersService.deleteUser(user).pipe(
tap(updated => { tap(() => {
this.next(s => { this.next(s => {
const users = s.users.filter(x => x.id !== user.id); const users = s.users.filter(x => x.id !== user.id);

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

@ -1,5 +1,5 @@
<sqx-title message="i18n:api.graphqlPageTitle"></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>
</sqx-panel> </sqx-panel>

2
frontend/app/features/apps/pages/onboarding-dialog.component.ts

@ -23,6 +23,6 @@ export class OnboardingDialogComponent {
public close = new EventEmitter(); public close = new EventEmitter();
public next() { public next() {
this.step = this.step + 1; this.step += 1;
} }
} }

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

@ -1,4 +1,4 @@
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="20rem" [isBlank]="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
{{ 'common.filters' | sqxTranslate }} {{ 'common.filters' | sqxTranslate }}
</ng-container> </ng-container>
@ -7,8 +7,8 @@
<h3>{{ 'common.tags' | sqxTranslate }}</h3> <h3>{{ 'common.tags' | sqxTranslate }}</h3>
<sqx-asset-tags (reset)="resetTags()" <sqx-asset-tags (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>

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

@ -1,6 +1,6 @@
<sqx-title message="i18n:assets.listPageTitle"></sqx-title> <sqx-title message="i18n:assets.listPageTitle"></sqx-title>
<sqx-panel desiredWidth="*" minWidth="50rem" showSidebar="true" grid="true" closeQueryParamsHandling="none"> <sqx-panel desiredWidth="*" minWidth="50rem" [showSidebar]="true" [grid]="true" closeQueryParamsHandling="">
<ng-container title> <ng-container title>
{{ 'common.assets' | sqxTranslate }} {{ 'common.assets' | sqxTranslate }}
</ng-container> </ng-container>
@ -17,7 +17,7 @@
<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="{{ 'assets.searchByTags' | sqxTranslate }}" <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)"
@ -30,7 +30,7 @@
[queries]="queries" [queries]="queries"
[queriesTypes]="'common.assets' | sqxTranslate" [queriesTypes]="'common.assets' | sqxTranslate"
(queryChange)="search($event)" (queryChange)="search($event)"
enableShortcut="true"> [enableShortcut]="true">
</sqx-search-form> </sqx-search-form>
</div> </div>
</div> </div>

2
frontend/app/features/content/pages/comments/comments-page.component.html

@ -1,4 +1,4 @@
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false" grid="true"> <sqx-panel desiredWidth="20rem" [isBlank]="true" [isLazyLoaded]="false" [grid]="true">
<ng-container title> <ng-container title>
{{ 'comments.title' | sqxTranslate }} {{ 'comments.title' | sqxTranslate }}
</ng-container> </ng-container>

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

@ -1,4 +1,4 @@
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="20rem" [isBlank]="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
{{ 'common.workflow' | sqxTranslate }} {{ 'common.workflow' | sqxTranslate }}
</ng-container> </ng-container>
@ -32,8 +32,8 @@
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" [class.active]="dropdownNew.isOpenChanges | async" #buttonOptions> <button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" [class.active]="dropdownNew.isOpenChanges | async" #buttonOptions>
<sqx-content-status <sqx-content-status
layout="multiline" layout="multiline"
[status]="content.newStatus" [status]="content.newStatus!"
[statusColor]="content.newStatusColor" [statusColor]="content.newStatusColor!"
[scheduled]="content.scheduleJob"> [scheduled]="content.scheduleJob">
</sqx-content-status> </sqx-content-status>
</button> </button>
@ -75,7 +75,7 @@
<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.isOpenChanges | async" #buttonOptions> <button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" [class.active]="dropdown.isOpenChanges | async" #buttonOptions>
<sqx-content-status small="true" <sqx-content-status [small]="true"
layout="multiline" layout="multiline"
[status]="content.status" [status]="content.status"
[statusColor]="content.statusColor" [statusColor]="content.statusColor"
@ -89,7 +89,7 @@
<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)">
{{ 'common.statusChangeTo' | sqxTranslate }} {{ 'common.statusChangeTo' | sqxTranslate }}
<sqx-content-status small="true" <sqx-content-status [small]="true"
layout="text" layout="text"
[status]="info.status" [status]="info.status"
[statusColor]="info.color"> [statusColor]="info.color">
@ -117,13 +117,13 @@
</button> </button>
</ng-template> </ng-template>
<sqx-form-hint marginTop="1"> <sqx-form-hint [marginTop="1"]>
{{ 'contents.lastUpdatedLabel' | sqxTranslate }}: {{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">{{ 'common.history' | sqxTranslate }}</h3> <h3 class="bordered">{{ 'common.history2' | 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"
@ -135,4 +135,4 @@
</ng-container> </ng-container>
</sqx-panel> </sqx-panel>
<sqx-due-time-selector #dueTimeSelector></sqx-due-time-selector> <sqx-due-time-selector [disabled]="disableScheduler" #dueTimeSelector></sqx-due-time-selector>

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

@ -32,6 +32,10 @@ export class ContentHistoryPageComponent extends ResourceOwner implements OnInit
public dropdown = new ModalModel(); public dropdown = new ModalModel();
public dropdownNew = new ModalModel(); public dropdownNew = new ModalModel();
public get disableScheduler() {
return this.appsState.snapshot.selectedSettings?.hideScheduler === true;
}
constructor( constructor(
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly contentPage: ContentPageComponent, private readonly contentPage: ContentPageComponent,

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

@ -1,7 +1,7 @@
<sqx-title [message]="schema.displayName"></sqx-title> <sqx-title [message]="schema.displayName"></sqx-title>
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()"> <form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-panel desiredWidth="*" minWidth="60rem" [showSidebar]="!!content" grid="true" (close)="back()"> <sqx-panel desiredWidth="*" minWidth="60rem" [showSidebar]="!!content" [grid]="true" (close)="back()">
<ng-container title> <ng-container title>
<a class="btn btn-text" (click)="back()" *ngIf="!schema.isSingleton"> <a class="btn btn-text" (click)="back()" *ngIf="!schema.isSingleton">
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
@ -102,7 +102,7 @@
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut> <sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
</ng-template> </ng-template>
<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>
</ng-container> </ng-container>
<ng-container content> <ng-container content>
@ -164,7 +164,7 @@
<i class="icon-plugin"></i> <i class="icon-plugin"></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">
{{ 'common.sidebarTour' | sqxTranslate }} {{ 'common.sidebarTour' | sqxTranslate }}
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</div> </div>

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

@ -282,6 +282,6 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
} }
} }
function isOtherContent(lhs: ContentDto | null | undefined, rhs: ContentDto | null | undefined) { function isOtherContent(lhs: ContentDto | undefined | null, rhs: ContentDto | undefined | null) {
return !lhs || !rhs || lhs.id !== rhs.id; return !lhs || !rhs || lhs.id !== rhs.id;
} }

2
frontend/app/features/content/pages/content/editor/content-editor.component.ts

@ -27,7 +27,7 @@ export class ContentEditorComponent {
public contentVersion: Version | null; public contentVersion: Version | null;
@Input() @Input()
public contentFormCompare?: EditContentForm; public contentFormCompare?: EditContentForm | null;
@Input() @Input()
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;

2
frontend/app/features/content/pages/content/editor/content-field.component.html

@ -79,7 +79,7 @@
<ng-template #singleControlCompare> <ng-template #singleControlCompare>
<sqx-field-editor <sqx-field-editor
[formModel]="getControlCompare()" [formModel]="getControlCompare()!"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages">
</sqx-field-editor> </sqx-field-editor>

4
frontend/app/features/content/pages/content/editor/content-field.component.ts

@ -20,13 +20,13 @@ export class ContentFieldComponent implements OnChanges {
public languageChange = new EventEmitter<AppLanguageDto>(); public languageChange = new EventEmitter<AppLanguageDto>();
@Input() @Input()
public isCompact = false; public isCompact?: boolean | null;
@Input() @Input()
public form: EditContentForm; public form: EditContentForm;
@Input() @Input()
public formCompare?: EditContentForm; public formCompare?: EditContentForm | null;
@Input() @Input()
public formContext: any; public formContext: any;

8
frontend/app/features/content/pages/content/editor/content-section.component.html

@ -1,4 +1,4 @@
<ng-container *ngIf="!(formSection.hiddenChanges | async) || formCompare"> <ng-container *ngIf="(formSection.visibleChanges | async) || formCompare">
<div class="header" *ngIf="formSection.separator; let separator"> <div class="header" *ngIf="formSection.separator; let separator">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-auto"> <div class="col-auto">
@ -7,10 +7,10 @@
</button> </button>
</div> </div>
<div class="col"> <div class="col">
<h3>{{separator!.displayName}}</h3> <h3>{{separator.displayName}}</h3>
<sqx-form-hint *ngIf="separator!.properties.hints?.length > 0"> <sqx-form-hint *ngIf="separator.properties.hints && separator.properties.hints.length > 0">
{{separator!.properties.hints}} {{separator.properties.hints}}
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>

4
frontend/app/features/content/pages/content/editor/content-section.component.ts

@ -24,13 +24,13 @@ export class ContentSectionComponent extends StatefulComponent<State> implements
public languageChange = new EventEmitter<AppLanguageDto>(); public languageChange = new EventEmitter<AppLanguageDto>();
@Input() @Input()
public isCompact = false; public isCompact?: boolean | null;
@Input() @Input()
public form: EditContentForm; public form: EditContentForm;
@Input() @Input()
public formCompare?: EditContentForm; public formCompare?: EditContentForm | null;
@Input() @Input()
public formContext: any; public formContext: any;

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

@ -18,7 +18,7 @@
</sqx-language-selector> </sqx-language-selector>
</div> </div>
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="top-right" after="120000"> <sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="top-right" [after]="120000">
{{ 'contents.validationHint' | sqxTranslate }} {{ 'contents.validationHint' | sqxTranslate }}
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</ng-container> </ng-container>

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

Loading…
Cancel
Save