Browse Source

Index UI (#1145)

* Temp

* Temp

* Indexes UI.

* UI for indexes finalized.

* Revert text

* More formatting.
pull/1146/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
b379c97142
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 26
      backend/i18n/frontend_en.json
  2. 26
      backend/i18n/frontend_fr.json
  3. 26
      backend/i18n/frontend_it.json
  4. 26
      backend/i18n/frontend_nl.json
  5. 26
      backend/i18n/frontend_pt.json
  6. 26
      backend/i18n/frontend_zh.json
  7. 5
      backend/i18n/source/backend_en.json
  8. 26
      backend/i18n/source/frontend_en.json
  9. 3
      backend/i18n/translator/Squidex.Translator/Processes/CheckFrontend.cs
  10. 7
      backend/i18n/translator/Squidex.Translator/Processes/TranslateBackend.cs
  11. 4
      backend/i18n/translator/Squidex.Translator/Processes/TranslateTemplates.cs
  12. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  13. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCache.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsOptions.cs
  15. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  16. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  17. 15
      backend/src/Squidex.Shared/Texts.fr.resx
  18. 15
      backend/src/Squidex.Shared/Texts.it.resx
  19. 15
      backend/src/Squidex.Shared/Texts.nl.resx
  20. 15
      backend/src/Squidex.Shared/Texts.pt.resx
  21. 15
      backend/src/Squidex.Shared/Texts.resx
  22. 15
      backend/src/Squidex.Shared/Texts.zh.resx
  23. 4
      backend/src/Squidex.Web/Services/UrlGenerator.cs
  24. 2
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaIndexesController.cs
  25. 1
      backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs
  26. 2
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  27. 8
      backend/src/Squidex/Config/Domain/FontendServices.cs
  28. 3
      backend/src/Squidex/appsettings.json
  29. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  30. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs
  31. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs
  32. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  33. 1
      frontend/.prettierrc
  34. 8
      frontend/package-lock.json
  35. 2
      frontend/package.json
  36. 2694
      frontend/src/app/_theme.html
  37. 2
      frontend/src/app/features/apps/pages/apps-page.component.html
  38. 6
      frontend/src/app/features/apps/pages/onboarding-dialog.component.html
  39. 5
      frontend/src/app/features/apps/pages/onboarding-dialog.component.ts
  40. 2
      frontend/src/app/features/assets/pages/assets-page.component.html
  41. 10
      frontend/src/app/features/content/pages/content/editor/content-editor.component.html
  42. 5
      frontend/src/app/features/content/pages/content/editor/content-editor.component.ts
  43. 11
      frontend/src/app/features/content/shared/forms/assets-editor.component.html
  44. 4
      frontend/src/app/features/dashboard/pages/dashboard-page.component.html
  45. 5
      frontend/src/app/features/dashboard/pages/dashboard-page.component.ts
  46. 2
      frontend/src/app/features/rules/pages/rules/rule.component.html
  47. 1
      frontend/src/app/features/schemas/pages/schema/fields/schema-fields.component.html
  48. 4
      frontend/src/app/features/schemas/pages/schema/fields/types/assets-ui.component.html
  49. 5
      frontend/src/app/features/schemas/pages/schema/fields/types/assets-ui.component.ts
  50. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html
  51. 5
      frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts
  52. 73
      frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.html
  53. 2
      frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.scss
  54. 100
      frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.ts
  55. 57
      frontend/src/app/features/schemas/pages/schema/indexes/index.component.html
  56. 12
      frontend/src/app/features/schemas/pages/schema/indexes/index.component.scss
  57. 39
      frontend/src/app/features/schemas/pages/schema/indexes/index.component.ts
  58. 55
      frontend/src/app/features/schemas/pages/schema/indexes/schema-indexes.component.html
  59. 22
      frontend/src/app/features/schemas/pages/schema/indexes/schema-indexes.component.scss
  60. 57
      frontend/src/app/features/schemas/pages/schema/indexes/schema-indexes.component.ts
  61. 10
      frontend/src/app/features/schemas/pages/schema/schema-page.component.html
  62. 8
      frontend/src/app/features/schemas/pages/schema/schema-page.component.ts
  63. 8
      frontend/src/app/features/settings/pages/clients/client-connect-form.component.html
  64. 5
      frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts
  65. 4
      frontend/src/app/features/settings/pages/clients/client.component.html
  66. 4
      frontend/src/app/features/settings/pages/jobs/job.component.html
  67. 8
      frontend/src/app/features/settings/pages/jobs/job.component.ts
  68. 6
      frontend/src/app/features/settings/pages/plans/plans-page.component.html
  69. 5
      frontend/src/app/features/settings/pages/plans/plans-page.component.ts
  70. 2
      frontend/src/app/features/settings/pages/templates/template.component.html
  71. 5
      frontend/src/app/features/settings/pages/templates/template.component.ts
  72. 2
      frontend/src/app/features/settings/pages/templates/templates-page.component.html
  73. 5
      frontend/src/app/features/settings/pages/templates/templates-page.component.ts
  74. 4
      frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.html
  75. 5
      frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.ts
  76. 2
      frontend/src/app/features/teams/pages/more/more-page.component.html
  77. 6
      frontend/src/app/features/teams/pages/plans/plans-page.component.html
  78. 5
      frontend/src/app/features/teams/pages/plans/plans-page.component.ts
  79. 7
      frontend/src/app/framework/angular/forms/editors/autocomplete.component.scss
  80. 2
      frontend/src/app/framework/angular/forms/form-error.component.html
  81. 6
      frontend/src/app/framework/angular/forms/form-error.component.ts
  82. 4
      frontend/src/app/framework/angular/modals/dialog-renderer.component.html
  83. 6
      frontend/src/app/framework/angular/modals/dialog-renderer.component.ts
  84. 6
      frontend/src/app/framework/angular/modals/modal-dialog.component.html
  85. 4
      frontend/src/app/framework/angular/modals/tour-template.component.html
  86. 7
      frontend/src/app/framework/angular/modals/tour-template.component.ts
  87. 1
      frontend/src/app/framework/index.ts
  88. 3
      frontend/src/app/shared/components/chat-dialog.component.ts
  89. 6
      frontend/src/app/shared/components/chat-item.component.html
  90. 6
      frontend/src/app/shared/components/comments/comment.component.html
  91. 5
      frontend/src/app/shared/components/comments/comment.component.ts
  92. 2
      frontend/src/app/shared/components/search/search-form.component.html
  93. 5
      frontend/src/app/shared/components/search/search-form.component.ts
  94. 7
      frontend/src/app/shared/internal.ts
  95. 116
      frontend/src/app/shared/services/indexes.service.spec.ts
  96. 93
      frontend/src/app/shared/services/indexes.service.ts
  97. 4
      frontend/src/app/shared/services/jobs.service.spec.ts
  98. 36
      frontend/src/app/shared/state/indexes.forms.ts
  99. 132
      frontend/src/app/shared/state/indexes.state.spec.ts
  100. 114
      frontend/src/app/shared/state/indexes.state.ts

26
backend/i18n/frontend_en.json

@ -136,16 +136,11 @@
"assets.uploadHint": "Drop file on existing item to replace the asset with a newer version.", "assets.uploadHint": "Drop file on existing item to replace the asset with a newer version.",
"assets.viewReferences": "View all content items referencing this asset.", "assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.", "assetScripts.reloaded": "Asset Scripts reloaded.",
"chat.answer": "Here is my answer:",
"chat.answersEmpty": "The ChatBot does not provide an answer or has not been configured yet.",
"chat.ask": "Ask", "chat.ask": "Ask",
"chat.describeFormat": "Also add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe the content you want to generate.",
"chat.failed": "Failed to answer your request.", "chat.failed": "Failed to answer your request.",
"chat.prompt": "Describe the content you want to generate", "chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot", "chat.title": "Chat Bot",
"chat.use": "Use", "chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Add Client", "clients.add": "Add Client",
"clients.add.description": "Add a client to give other applications access to your content.", "clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client", "clients.add.title": "Add a new Client",
@ -322,6 +317,7 @@
"common.openAPI": "Open API", "common.openAPI": "Open API",
"common.options": "Options", "common.options": "Options",
"common.or": "or", "common.or": "or",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}", "common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?", "common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items", "common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References", "common.references": "References",
"common.refresh": "Refresh", "common.refresh": "Refresh",
"common.remember": "Don't ask again", "common.remember": "Don't ask again",
"common.remove": "remove",
"common.rename": "Rename", "common.rename": "Rename",
"common.renameTag": "Rename Tag", "common.renameTag": "Rename Tag",
"common.reply": "Reply", "common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Separator for editing UI.", "schemas.fieldTypes.ui.description": "Separator for editing UI.",
"schemas.hideFieldFailed": "Failed to hide field. Please reload.", "schemas.hideFieldFailed": "Failed to hide field. Please reload.",
"schemas.import": "Import schema", "schemas.import": "Import schema",
"schemas.indexes.addIndex": "Add Index",
"schemas.indexes.addIndexButton": "Add Index",
"schemas.indexes.addTitle": "Add Index",
"schemas.indexes.created": "Index has been scheduled for creation. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.createFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteConfirmText": "Delete index",
"schemas.indexes.deleteConfirmTitle": "Do you really want to remove the index?",
"schemas.indexes.deleted": "Index has been scheduled for deletion. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.deleteFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteFieldConfirmText": "Remove field",
"schemas.indexes.deleteFieldConfirmTitle": "Do you really want to remove the field?",
"schemas.indexes.empty": "No index created yet.",
"schemas.indexes.hint": "Indexes can be expensive. Analyze your queries and read the following documentation before creating an index: [Documentation](https://www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/)",
"schemas.indexes.loadFailed": "Failed to load indexes. Please reload.",
"schemas.indexes.notEnableHint1": "Indexes cannot be created. You have turn on a feature, which creates a dedicated collection per schema. Use the following environment variable:",
"schemas.indexes.notEnableHint2": "Please note, that this does not insert your existing content items to the new collections automatically. You have to run a migration by setting the following environment variable. Start Squidex with rebuild setting once and wait for completion. Then turn it off again.",
"schemas.indexes.reloaded": "Indexes reloaded.",
"schemas.listFields": "List Fields", "schemas.listFields": "List Fields",
"schemas.listFieldsEmpty": "Drop field here or reorder them to show the fields in the content list. When no list field is defined, the first field is used.", "schemas.listFieldsEmpty": "Drop field here or reorder them to show the fields in the content list. When no list field is defined, the first field is used.",
"schemas.loadFailed": "Failed to load schemas. Please reload.", "schemas.loadFailed": "Failed to load schemas. Please reload.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Field Rules", "schemas.rules.title": "Field Rules",
"schemas.rules.when": "when", "schemas.rules.when": "when",
"schemas.saved": "Schema saved successfully.", "schemas.saved": "Schema saved successfully.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Save and close", "schemas.saveFieldAndClose": "Save and close",
"schemas.saveFieldAndNew": "Save and add field", "schemas.saveFieldAndNew": "Save and add field",
"schemas.schemaHintsHint": "Describe this schema for documentation and user interfaces.", "schemas.schemaHintsHint": "Describe this schema for documentation and user interfaces.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schema synchronized successfully.", "schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.", "schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.",
"schemas.tabFields": "Fields", "schemas.tabFields": "Fields",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json", "schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Created", "schemas.tableHeaders.created": "Created",
"schemas.tableHeaders.created_title": "Created", "schemas.tableHeaders.created_title": "Created",

26
backend/i18n/frontend_fr.json

@ -136,16 +136,11 @@
"assets.uploadHint": "Déposez le fichier sur un élément existant pour remplacer l'actif par une version plus récente.", "assets.uploadHint": "Déposez le fichier sur un élément existant pour remplacer l'actif par une version plus récente.",
"assets.viewReferences": "Afficher tous les éléments de contenu faisant référence à cet élément.", "assets.viewReferences": "Afficher tous les éléments de contenu faisant référence à cet élément.",
"assetScripts.reloaded": "Scripts d'actif rechargés.", "assetScripts.reloaded": "Scripts d'actif rechargés.",
"chat.answer": "Here is my answer:",
"chat.answersEmpty": "The ChatBot does not provide an answer or has not been configured yet.",
"chat.ask": "Ask", "chat.ask": "Ask",
"chat.describeFormat": "Also add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe the content you want to generate.",
"chat.failed": "Failed to answer your request.", "chat.failed": "Failed to answer your request.",
"chat.prompt": "Describe the content you want to generate", "chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot", "chat.title": "Chat Bot",
"chat.use": "Use", "chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Ajouter un client", "clients.add": "Ajouter un client",
"clients.add.description": "Ajoutez un client pour permettre à d'autres applications d'accéder à votre contenu.", "clients.add.description": "Ajoutez un client pour permettre à d'autres applications d'accéder à votre contenu.",
"clients.add.title": "Ajouter un nouveau client", "clients.add.title": "Ajouter un nouveau client",
@ -322,6 +317,7 @@
"common.openAPI": "Ouvrir l'API", "common.openAPI": "Ouvrir l'API",
"common.options": "Options", "common.options": "Options",
"common.or": "ou", "common.or": "ou",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} de {numberOfItems}", "common.pagerInfo": "{itemFirst}-{itemLast} de {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} Du total?", "common.pagerInfoNoTotal": "{itemFirst}-{itemLast} Du total?",
"common.pagerReload": "Cliquez pour recharger la vue et obtenir le nombre total d'articles", "common.pagerReload": "Cliquez pour recharger la vue et obtenir le nombre total d'articles",
@ -353,6 +349,7 @@
"common.references": "Les références", "common.references": "Les références",
"common.refresh": "Rafraîchir", "common.refresh": "Rafraîchir",
"common.remember": "Ne demande plus", "common.remember": "Ne demande plus",
"common.remove": "remove",
"common.rename": "Renommer", "common.rename": "Renommer",
"common.renameTag": "Renommer la balise", "common.renameTag": "Renommer la balise",
"common.reply": "Reply", "common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Séparateur pour l'édition de l'interface utilisateur.", "schemas.fieldTypes.ui.description": "Séparateur pour l'édition de l'interface utilisateur.",
"schemas.hideFieldFailed": "Impossible de masquer le champ. Veuillez recharger.", "schemas.hideFieldFailed": "Impossible de masquer le champ. Veuillez recharger.",
"schemas.import": "Calendrier d'importation", "schemas.import": "Calendrier d'importation",
"schemas.indexes.addIndex": "Add Index",
"schemas.indexes.addIndexButton": "Add Index",
"schemas.indexes.addTitle": "Add Index",
"schemas.indexes.created": "Index has been scheduled for creation. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.createFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteConfirmText": "Delete index",
"schemas.indexes.deleteConfirmTitle": "Do you really want to remove the index?",
"schemas.indexes.deleted": "Index has been scheduled for deletion. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.deleteFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteFieldConfirmText": "Remove field",
"schemas.indexes.deleteFieldConfirmTitle": "Do you really want to remove the field?",
"schemas.indexes.empty": "No index created yet.",
"schemas.indexes.hint": "Indexes can be expensive. Analyze your queries and read the following documentation before creating an index: [Documentation](https://www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/)",
"schemas.indexes.loadFailed": "Failed to load indexes. Please reload.",
"schemas.indexes.notEnableHint1": "Indexes cannot be created. You have turn on a feature, which creates a dedicated collection per schema. Use the following environment variable:",
"schemas.indexes.notEnableHint2": "Please note, that this does not insert your existing content items to the new collections automatically. You have to run a migration by setting the following environment variable. Start Squidex with rebuild setting once and wait for completion. Then turn it off again.",
"schemas.indexes.reloaded": "Indexes reloaded.",
"schemas.listFields": "Champs de liste", "schemas.listFields": "Champs de liste",
"schemas.listFieldsEmpty": "Déposez le champ ici ou réorganisez-les pour afficher les champs dans la liste de contenu. Lorsqu'aucun champ de liste n'est défini, le premier champ est utilisé.", "schemas.listFieldsEmpty": "Déposez le champ ici ou réorganisez-les pour afficher les champs dans la liste de contenu. Lorsqu'aucun champ de liste n'est défini, le premier champ est utilisé.",
"schemas.loadFailed": "Échec du chargement des schémas. Veuillez recharger.", "schemas.loadFailed": "Échec du chargement des schémas. Veuillez recharger.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Règles de champ", "schemas.rules.title": "Règles de champ",
"schemas.rules.when": "quand", "schemas.rules.when": "quand",
"schemas.saved": "Schéma enregistré avec succès.", "schemas.saved": "Schéma enregistré avec succès.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Sauver et fermer", "schemas.saveFieldAndClose": "Sauver et fermer",
"schemas.saveFieldAndNew": "Enregistrer et ajouter un champ", "schemas.saveFieldAndNew": "Enregistrer et ajouter un champ",
"schemas.schemaHintsHint": "Décrivez ce schéma pour la documentation et les interfaces utilisateur.", "schemas.schemaHintsHint": "Décrivez ce schéma pour la documentation et les interfaces utilisateur.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schéma synchronisé avec succès.", "schemas.synchronized": "Schéma synchronisé avec succès.",
"schemas.synchronizeFailed": "Échec de la synchronisation du schéma. Veuillez recharger.", "schemas.synchronizeFailed": "Échec de la synchronisation du schéma. Veuillez recharger.",
"schemas.tabFields": "Des champs", "schemas.tabFields": "Des champs",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json", "schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Créé", "schemas.tableHeaders.created": "Créé",
"schemas.tableHeaders.created_title": "Created", "schemas.tableHeaders.created_title": "Created",

26
backend/i18n/frontend_it.json

@ -136,16 +136,11 @@
"assets.uploadHint": "Trascina il file sull'elemento esistente per poterlo sostituire con una versione più recente.", "assets.uploadHint": "Trascina il file sull'elemento esistente per poterlo sostituire con una versione più recente.",
"assets.viewReferences": "View all content items referencing this asset.", "assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.", "assetScripts.reloaded": "Asset Scripts reloaded.",
"chat.answer": "Here is my answer:",
"chat.answersEmpty": "The ChatBot does not provide an answer or has not been configured yet.",
"chat.ask": "Ask", "chat.ask": "Ask",
"chat.describeFormat": "Also add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe the content you want to generate.",
"chat.failed": "Failed to answer your request.", "chat.failed": "Failed to answer your request.",
"chat.prompt": "Describe the content you want to generate", "chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot", "chat.title": "Chat Bot",
"chat.use": "Use", "chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Aggiungi un Client", "clients.add": "Aggiungi un Client",
"clients.add.description": "Add a client to give other applications access to your content.", "clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client", "clients.add.title": "Add a new Client",
@ -322,6 +317,7 @@
"common.openAPI": "Open API", "common.openAPI": "Open API",
"common.options": "Options", "common.options": "Options",
"common.or": "o", "common.or": "o",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}", "common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?", "common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items", "common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References", "common.references": "References",
"common.refresh": "Aggiorna", "common.refresh": "Aggiorna",
"common.remember": "Ricorda la mia decisione", "common.remember": "Ricorda la mia decisione",
"common.remove": "remove",
"common.rename": "Rinomina", "common.rename": "Rinomina",
"common.renameTag": "Rename Tag", "common.renameTag": "Rename Tag",
"common.reply": "Reply", "common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Separatore per il pannello delle modifiche della UI.", "schemas.fieldTypes.ui.description": "Separatore per il pannello delle modifiche della UI.",
"schemas.hideFieldFailed": "Non è stato possibile nascondere il campo. Per favore ricarica.", "schemas.hideFieldFailed": "Non è stato possibile nascondere il campo. Per favore ricarica.",
"schemas.import": "Importa uno schema", "schemas.import": "Importa uno schema",
"schemas.indexes.addIndex": "Add Index",
"schemas.indexes.addIndexButton": "Add Index",
"schemas.indexes.addTitle": "Add Index",
"schemas.indexes.created": "Index has been scheduled for creation. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.createFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteConfirmText": "Delete index",
"schemas.indexes.deleteConfirmTitle": "Do you really want to remove the index?",
"schemas.indexes.deleted": "Index has been scheduled for deletion. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.deleteFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteFieldConfirmText": "Remove field",
"schemas.indexes.deleteFieldConfirmTitle": "Do you really want to remove the field?",
"schemas.indexes.empty": "No index created yet.",
"schemas.indexes.hint": "Indexes can be expensive. Analyze your queries and read the following documentation before creating an index: [Documentation](https://www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/)",
"schemas.indexes.loadFailed": "Failed to load indexes. Please reload.",
"schemas.indexes.notEnableHint1": "Indexes cannot be created. You have turn on a feature, which creates a dedicated collection per schema. Use the following environment variable:",
"schemas.indexes.notEnableHint2": "Please note, that this does not insert your existing content items to the new collections automatically. You have to run a migration by setting the following environment variable. Start Squidex with rebuild setting once and wait for completion. Then turn it off again.",
"schemas.indexes.reloaded": "Indexes reloaded.",
"schemas.listFields": "Lista dei Campi", "schemas.listFields": "Lista dei Campi",
"schemas.listFieldsEmpty": "Incolla qui il campo o riordina i campi da mostrare nella lista dei contenuti. Se non imposti una lista di campi da visualizzare, viene utilizzato il primo della lista.", "schemas.listFieldsEmpty": "Incolla qui il campo o riordina i campi da mostrare nella lista dei contenuti. Se non imposti una lista di campi da visualizzare, viene utilizzato il primo della lista.",
"schemas.loadFailed": "Non è stato possibile caricare gli schemi. Per favore ricarica.", "schemas.loadFailed": "Non è stato possibile caricare gli schemi. Per favore ricarica.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Regole per il campo", "schemas.rules.title": "Regole per il campo",
"schemas.rules.when": "quando", "schemas.rules.when": "quando",
"schemas.saved": "Lo Schema è stato salvato con successo.", "schemas.saved": "Lo Schema è stato salvato con successo.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Salva e chiudi", "schemas.saveFieldAndClose": "Salva e chiudi",
"schemas.saveFieldAndNew": "Salva e aggiungi un campo", "schemas.saveFieldAndNew": "Salva e aggiungi un campo",
"schemas.schemaHintsHint": "Descrivi questo schema per la documentazione e le interfacce utente.", "schemas.schemaHintsHint": "Descrivi questo schema per la documentazione e le interfacce utente.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Lo Schema è stato sincronizzato con successo.", "schemas.synchronized": "Lo Schema è stato sincronizzato con successo.",
"schemas.synchronizeFailed": "Non è stato possibile sincronizzare lo schema. Per favore ricarica.", "schemas.synchronizeFailed": "Non è stato possibile sincronizzare lo schema. Per favore ricarica.",
"schemas.tabFields": "Campi", "schemas.tabFields": "Campi",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json", "schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Creato", "schemas.tableHeaders.created": "Creato",
"schemas.tableHeaders.created_title": "Created", "schemas.tableHeaders.created_title": "Created",

26
backend/i18n/frontend_nl.json

@ -136,16 +136,11 @@
"assets.uploadHint": "Zet het bestand neer op bestaand item om het bestand te vervangen door een nieuwere versie.", "assets.uploadHint": "Zet het bestand neer op bestaand item om het bestand te vervangen door een nieuwere versie.",
"assets.viewReferences": "View all content items referencing this asset.", "assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.", "assetScripts.reloaded": "Asset Scripts reloaded.",
"chat.answer": "Here is my answer:",
"chat.answersEmpty": "The ChatBot does not provide an answer or has not been configured yet.",
"chat.ask": "Ask", "chat.ask": "Ask",
"chat.describeFormat": "Also add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe the content you want to generate.",
"chat.failed": "Failed to answer your request.", "chat.failed": "Failed to answer your request.",
"chat.prompt": "Describe the content you want to generate", "chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot", "chat.title": "Chat Bot",
"chat.use": "Use", "chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Client toevoegen", "clients.add": "Client toevoegen",
"clients.add.description": "Voeg een client toe om andere applicaties toegang te geven tot uw inhoud.", "clients.add.description": "Voeg een client toe om andere applicaties toegang te geven tot uw inhoud.",
"clients.add.title": "Client toevoegen", "clients.add.title": "Client toevoegen",
@ -322,6 +317,7 @@
"common.openAPI": "Open API", "common.openAPI": "Open API",
"common.options": "Options", "common.options": "Options",
"common.or": "of", "common.or": "of",
"common.order": "Order",
"common.pagerInfo": "{itemFirst} - {itemLast} van {numberOfItems}", "common.pagerInfo": "{itemFirst} - {itemLast} van {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?", "common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items", "common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References", "common.references": "References",
"common.refresh": "Vernieuwen", "common.refresh": "Vernieuwen",
"common.remember": "Onthoud mijn keuze", "common.remember": "Onthoud mijn keuze",
"common.remove": "remove",
"common.rename": "Hernoemen", "common.rename": "Hernoemen",
"common.renameTag": "Hernoem Tag", "common.renameTag": "Hernoem Tag",
"common.reply": "Reply", "common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Scheidingsteken voor het bewerken van gebruikersinterface.", "schemas.fieldTypes.ui.description": "Scheidingsteken voor het bewerken van gebruikersinterface.",
"schemas.hideFieldFailed": "Kan veld niet verbergen. Laad opnieuw.", "schemas.hideFieldFailed": "Kan veld niet verbergen. Laad opnieuw.",
"schemas.import": "Importeer schema", "schemas.import": "Importeer schema",
"schemas.indexes.addIndex": "Add Index",
"schemas.indexes.addIndexButton": "Add Index",
"schemas.indexes.addTitle": "Add Index",
"schemas.indexes.created": "Index has been scheduled for creation. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.createFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteConfirmText": "Delete index",
"schemas.indexes.deleteConfirmTitle": "Do you really want to remove the index?",
"schemas.indexes.deleted": "Index has been scheduled for deletion. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.deleteFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteFieldConfirmText": "Remove field",
"schemas.indexes.deleteFieldConfirmTitle": "Do you really want to remove the field?",
"schemas.indexes.empty": "No index created yet.",
"schemas.indexes.hint": "Indexes can be expensive. Analyze your queries and read the following documentation before creating an index: [Documentation](https://www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/)",
"schemas.indexes.loadFailed": "Failed to load indexes. Please reload.",
"schemas.indexes.notEnableHint1": "Indexes cannot be created. You have turn on a feature, which creates a dedicated collection per schema. Use the following environment variable:",
"schemas.indexes.notEnableHint2": "Please note, that this does not insert your existing content items to the new collections automatically. You have to run a migration by setting the following environment variable. Start Squidex with rebuild setting once and wait for completion. Then turn it off again.",
"schemas.indexes.reloaded": "Indexes reloaded.",
"schemas.listFields": "Lijstvelden", "schemas.listFields": "Lijstvelden",
"schemas.listFieldsEmpty": "Zet het veld hier neer of rangschik ze opnieuw om de velden in de inhoudslijst weer te geven. Als er geen lijstveld is gedefinieerd, wordt het eerste veld gebruikt.", "schemas.listFieldsEmpty": "Zet het veld hier neer of rangschik ze opnieuw om de velden in de inhoudslijst weer te geven. Als er geen lijstveld is gedefinieerd, wordt het eerste veld gebruikt.",
"schemas.loadFailed": "Kan schema's niet laden. Laad opnieuw.", "schemas.loadFailed": "Kan schema's niet laden. Laad opnieuw.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Veldregels", "schemas.rules.title": "Veldregels",
"schemas.rules.when": "Wanneer", "schemas.rules.when": "Wanneer",
"schemas.saved": "Schema succesvol opgeslagen.", "schemas.saved": "Schema succesvol opgeslagen.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Opslaan en sluiten", "schemas.saveFieldAndClose": "Opslaan en sluiten",
"schemas.saveFieldAndNew": "Opslaan en veld toevoegen", "schemas.saveFieldAndNew": "Opslaan en veld toevoegen",
"schemas.schemaHintsHint": "Beschrijf dit schema voor documentatie en gebruikersinterfaces.", "schemas.schemaHintsHint": "Beschrijf dit schema voor documentatie en gebruikersinterfaces.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schema is succesvol gesynchroniseerd.", "schemas.synchronized": "Schema is succesvol gesynchroniseerd.",
"schemas.synchronizeFailed": "Synchroniseren van schema is mislukt. Laad opnieuw.", "schemas.synchronizeFailed": "Synchroniseren van schema is mislukt. Laad opnieuw.",
"schemas.tabFields": "Velden", "schemas.tabFields": "Velden",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json", "schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Gemaakt", "schemas.tableHeaders.created": "Gemaakt",
"schemas.tableHeaders.created_title": "Created", "schemas.tableHeaders.created_title": "Created",

26
backend/i18n/frontend_pt.json

@ -136,16 +136,11 @@
"assets.uploadHint": "Deixe cair o ficheiro no item existente para substituir o ficheiro por uma versão mais recente.", "assets.uploadHint": "Deixe cair o ficheiro no item existente para substituir o ficheiro por uma versão mais recente.",
"assets.viewReferences": "View all content items referencing this asset.", "assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Scripts de ficheiros recarregados.", "assetScripts.reloaded": "Scripts de ficheiros recarregados.",
"chat.answer": "Here is my answer:",
"chat.answersEmpty": "The ChatBot does not provide an answer or has not been configured yet.",
"chat.ask": "Ask", "chat.ask": "Ask",
"chat.describeFormat": "Also add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe the content you want to generate.",
"chat.failed": "Failed to answer your request.", "chat.failed": "Failed to answer your request.",
"chat.prompt": "Describe the content you want to generate", "chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot", "chat.title": "Chat Bot",
"chat.use": "Use", "chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Adicionar Cliente", "clients.add": "Adicionar Cliente",
"clients.add.description": "Adicione um cliente para dar a outras aplicações acesso ao seu conteúdo.", "clients.add.description": "Adicione um cliente para dar a outras aplicações acesso ao seu conteúdo.",
"clients.add.title": "Adicionar um novo Cliente", "clients.add.title": "Adicionar um novo Cliente",
@ -322,6 +317,7 @@
"common.openAPI": "Open API", "common.openAPI": "Open API",
"common.options": "Options", "common.options": "Options",
"common.or": "ou", "common.or": "ou",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} de {numberOfItems}", "common.pagerInfo": "{itemFirst}-{itemLast} de {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} do total?", "common.pagerInfoNoTotal": "{itemFirst}-{itemLast} do total?",
"common.pagerReload": "Clique para recarregar a vista e obter o número total de itens", "common.pagerReload": "Clique para recarregar a vista e obter o número total de itens",
@ -353,6 +349,7 @@
"common.references": "References", "common.references": "References",
"common.refresh": "Refrescar", "common.refresh": "Refrescar",
"common.remember": "Não pergunte de novo.", "common.remember": "Não pergunte de novo.",
"common.remove": "remove",
"common.rename": "Renomear", "common.rename": "Renomear",
"common.renameTag": "Renomear Etiqueta", "common.renameTag": "Renomear Etiqueta",
"common.reply": "Reply", "common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Separador para edição de UI.", "schemas.fieldTypes.ui.description": "Separador para edição de UI.",
"schemas.hideFieldFailed": "Falhou em esconder o campo. Por favor, recarregue.", "schemas.hideFieldFailed": "Falhou em esconder o campo. Por favor, recarregue.",
"schemas.import": "Esquema de importação", "schemas.import": "Esquema de importação",
"schemas.indexes.addIndex": "Add Index",
"schemas.indexes.addIndexButton": "Add Index",
"schemas.indexes.addTitle": "Add Index",
"schemas.indexes.created": "Index has been scheduled for creation. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.createFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteConfirmText": "Delete index",
"schemas.indexes.deleteConfirmTitle": "Do you really want to remove the index?",
"schemas.indexes.deleted": "Index has been scheduled for deletion. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.deleteFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteFieldConfirmText": "Remove field",
"schemas.indexes.deleteFieldConfirmTitle": "Do you really want to remove the field?",
"schemas.indexes.empty": "No index created yet.",
"schemas.indexes.hint": "Indexes can be expensive. Analyze your queries and read the following documentation before creating an index: [Documentation](https://www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/)",
"schemas.indexes.loadFailed": "Failed to load indexes. Please reload.",
"schemas.indexes.notEnableHint1": "Indexes cannot be created. You have turn on a feature, which creates a dedicated collection per schema. Use the following environment variable:",
"schemas.indexes.notEnableHint2": "Please note, that this does not insert your existing content items to the new collections automatically. You have to run a migration by setting the following environment variable. Start Squidex with rebuild setting once and wait for completion. Then turn it off again.",
"schemas.indexes.reloaded": "Indexes reloaded.",
"schemas.listFields": "Campos de Lista", "schemas.listFields": "Campos de Lista",
"schemas.listFieldsEmpty": "Deixe cair aqui ou reencomenda-os para mostrar os campos na lista de conteúdos. Quando não é definido nenhum campo de lista, o primeiro campo é utilizado.", "schemas.listFieldsEmpty": "Deixe cair aqui ou reencomenda-os para mostrar os campos na lista de conteúdos. Quando não é definido nenhum campo de lista, o primeiro campo é utilizado.",
"schemas.loadFailed": "Falhou em carregar esquemas. Por favor, recarregue.", "schemas.loadFailed": "Falhou em carregar esquemas. Por favor, recarregue.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Regras de Campo", "schemas.rules.title": "Regras de Campo",
"schemas.rules.when": "quando", "schemas.rules.when": "quando",
"schemas.saved": "Esqquema salvo com sucesso.", "schemas.saved": "Esqquema salvo com sucesso.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Salvar e fechar", "schemas.saveFieldAndClose": "Salvar e fechar",
"schemas.saveFieldAndNew": "Guardar e adicionar campo", "schemas.saveFieldAndNew": "Guardar e adicionar campo",
"schemas.schemaHintsHint": "Descreva este esquema para documentação e interfaces de utilizador.", "schemas.schemaHintsHint": "Descreva este esquema para documentação e interfaces de utilizador.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Esquema sincronizado com sucesso.", "schemas.synchronized": "Esquema sincronizado com sucesso.",
"schemas.synchronizeFailed": "Falhou em sincronizar o esquema. Por favor, recarregue.", "schemas.synchronizeFailed": "Falhou em sincronizar o esquema. Por favor, recarregue.",
"schemas.tabFields": "Campos", "schemas.tabFields": "Campos",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json", "schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Criado", "schemas.tableHeaders.created": "Criado",
"schemas.tableHeaders.created_title": "Created", "schemas.tableHeaders.created_title": "Created",

26
backend/i18n/frontend_zh.json

@ -136,16 +136,11 @@
"assets.uploadHint": "在现有项目上放置文件以使用更新版本替换资源。", "assets.uploadHint": "在现有项目上放置文件以使用更新版本替换资源。",
"assets.viewReferences": "View all content items referencing this asset.", "assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.", "assetScripts.reloaded": "Asset Scripts reloaded.",
"chat.answer": "Here is my answer:",
"chat.answersEmpty": "The ChatBot does not provide an answer or has not been configured yet.",
"chat.ask": "Ask", "chat.ask": "Ask",
"chat.describeFormat": "Also add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe the content you want to generate.",
"chat.failed": "Failed to answer your request.", "chat.failed": "Failed to answer your request.",
"chat.prompt": "Describe the content you want to generate", "chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot", "chat.title": "Chat Bot",
"chat.use": "Use", "chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "添加客户端", "clients.add": "添加客户端",
"clients.add.description": "Add a client to give other applications access to your content.", "clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client", "clients.add.title": "Add a new Client",
@ -322,6 +317,7 @@
"common.openAPI": "Open API", "common.openAPI": "Open API",
"common.options": "Options", "common.options": "Options",
"common.or": "或", "common.or": "或",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} 的 {numberOfItems}", "common.pagerInfo": "{itemFirst}-{itemLast} 的 {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?", "common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items", "common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References", "common.references": "References",
"common.refresh": "刷新", "common.refresh": "刷新",
"common.remember": "不要再问了", "common.remember": "不要再问了",
"common.remove": "remove",
"common.rename": "重命名", "common.rename": "重命名",
"common.renameTag": "Rename Tag", "common.renameTag": "Rename Tag",
"common.reply": "Reply", "common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "编辑 UI 的分隔符。", "schemas.fieldTypes.ui.description": "编辑 UI 的分隔符。",
"schemas.hideFieldFailed": "隐藏字段失败。请重新加载。", "schemas.hideFieldFailed": "隐藏字段失败。请重新加载。",
"schemas.import": "导入Schemas", "schemas.import": "导入Schemas",
"schemas.indexes.addIndex": "Add Index",
"schemas.indexes.addIndexButton": "Add Index",
"schemas.indexes.addTitle": "Add Index",
"schemas.indexes.created": "Index has been scheduled for creation. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.createFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteConfirmText": "Delete index",
"schemas.indexes.deleteConfirmTitle": "Do you really want to remove the index?",
"schemas.indexes.deleted": "Index has been scheduled for deletion. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.deleteFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteFieldConfirmText": "Remove field",
"schemas.indexes.deleteFieldConfirmTitle": "Do you really want to remove the field?",
"schemas.indexes.empty": "No index created yet.",
"schemas.indexes.hint": "Indexes can be expensive. Analyze your queries and read the following documentation before creating an index: [Documentation](https://www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/)",
"schemas.indexes.loadFailed": "Failed to load indexes. Please reload.",
"schemas.indexes.notEnableHint1": "Indexes cannot be created. You have turn on a feature, which creates a dedicated collection per schema. Use the following environment variable:",
"schemas.indexes.notEnableHint2": "Please note, that this does not insert your existing content items to the new collections automatically. You have to run a migration by setting the following environment variable. Start Squidex with rebuild setting once and wait for completion. Then turn it off again.",
"schemas.indexes.reloaded": "Indexes reloaded.",
"schemas.listFields": "列表字段", "schemas.listFields": "列表字段",
"schemas.listFieldsEmpty": "将字段拖放到此处或重新排序以显示内容列表中的字段。当未定义列表字段时,使用第一个字段。", "schemas.listFieldsEmpty": "将字段拖放到此处或重新排序以显示内容列表中的字段。当未定义列表字段时,使用第一个字段。",
"schemas.loadFailed": "加载Schemas失败。请重新加载。", "schemas.loadFailed": "加载Schemas失败。请重新加载。",
@ -986,6 +1000,7 @@
"schemas.rules.title": "字段规则", "schemas.rules.title": "字段规则",
"schemas.rules.when": "何时", "schemas.rules.when": "何时",
"schemas.saved": "Schema saved successfully.", "schemas.saved": "Schema saved successfully.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "保存并关闭", "schemas.saveFieldAndClose": "保存并关闭",
"schemas.saveFieldAndNew": "保存并添加字段", "schemas.saveFieldAndNew": "保存并添加字段",
"schemas.schemaHintsHint": "为文档和用户界面描述这个Schemas。", "schemas.schemaHintsHint": "为文档和用户界面描述这个Schemas。",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schema synchronized successfully.", "schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "同步Schemas失败。请重新加载。", "schemas.synchronizeFailed": "同步Schemas失败。请重新加载。",
"schemas.tabFields": "字段", "schemas.tabFields": "字段",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json", "schemas.tabJson": "Json",
"schemas.tableHeaders.created": "创建", "schemas.tableHeaders.created": "创建",
"schemas.tableHeaders.created_title": "Created", "schemas.tableHeaders.created_title": "Created",

5
backend/i18n/source/backend_en.json

@ -349,16 +349,11 @@
"users.lockedOutText": "Your account is locked, please contact the administrator.", "users.lockedOutText": "Your account is locked, please contact the administrator.",
"users.lockedOutTitle": "Account locked", "users.lockedOutTitle": "Account locked",
"users.lockYourselfError": "You cannot lock yourself.", "users.lockYourselfError": "You cannot lock yourself.",
"users.login.askAdmin": "",
"users.login.custom": "Enter your E-Mail Address to login with your Company Account and single sign on (SSO).", "users.login.custom": "Enter your E-Mail Address to login with your Company Account and single sign on (SSO).",
"users.login.emailBusinessPlaceholder": "Enter Business Email", "users.login.emailBusinessPlaceholder": "Enter Business Email",
"users.login.emailPlaceholder": "Enter Email", "users.login.emailPlaceholder": "Enter Email",
"users.login.error": "Email or password not correct", "users.login.error": "Email or password not correct",
"users.login.loginWith": "{action} with <strong>{provider}</strong>", "users.login.loginWith": "{action} with <strong>{provider}</strong>",
"users.login.noAccountLoginAction": "Click here to login",
"users.login.noAccountLoginQuestion": "Already registered?",
"users.login.noAccountSignupAction": "Click here to signup",
"users.login.noAccountSignupQuestion": "No account yet?",
"users.login.passwordPlaceholder": "Enter Password", "users.login.passwordPlaceholder": "Enter Password",
"users.login.separator": "OR", "users.login.separator": "OR",
"users.logout.headline": "Logged out!", "users.logout.headline": "Logged out!",

26
backend/i18n/source/frontend_en.json

@ -136,16 +136,11 @@
"assets.uploadHint": "Drop file on existing item to replace the asset with a newer version.", "assets.uploadHint": "Drop file on existing item to replace the asset with a newer version.",
"assets.viewReferences": "View all content items referencing this asset.", "assets.viewReferences": "View all content items referencing this asset.",
"assetScripts.reloaded": "Asset Scripts reloaded.", "assetScripts.reloaded": "Asset Scripts reloaded.",
"chat.answer": "Here is my answer:",
"chat.answersEmpty": "The ChatBot does not provide an answer or has not been configured yet.",
"chat.ask": "Ask", "chat.ask": "Ask",
"chat.describeFormat": "Also add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.",
"chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe the content you want to generate.",
"chat.failed": "Failed to answer your request.", "chat.failed": "Failed to answer your request.",
"chat.prompt": "Describe the content you want to generate", "chat.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot", "chat.title": "Chat Bot",
"chat.use": "Use", "chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Add Client", "clients.add": "Add Client",
"clients.add.description": "Add a client to give other applications access to your content.", "clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client", "clients.add.title": "Add a new Client",
@ -322,6 +317,7 @@
"common.openAPI": "Open API", "common.openAPI": "Open API",
"common.options": "Options", "common.options": "Options",
"common.or": "or", "common.or": "or",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}", "common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?", "common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items", "common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References", "common.references": "References",
"common.refresh": "Refresh", "common.refresh": "Refresh",
"common.remember": "Don't ask again", "common.remember": "Don't ask again",
"common.remove": "remove",
"common.rename": "Rename", "common.rename": "Rename",
"common.renameTag": "Rename Tag", "common.renameTag": "Rename Tag",
"common.reply": "Reply", "common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Separator for editing UI.", "schemas.fieldTypes.ui.description": "Separator for editing UI.",
"schemas.hideFieldFailed": "Failed to hide field. Please reload.", "schemas.hideFieldFailed": "Failed to hide field. Please reload.",
"schemas.import": "Import schema", "schemas.import": "Import schema",
"schemas.indexes.addIndex": "Add Index",
"schemas.indexes.addIndexButton": "Add Index",
"schemas.indexes.addTitle": "Add Index",
"schemas.indexes.created": "Index has been scheduled for creation. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.createFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteConfirmText": "Delete index",
"schemas.indexes.deleteConfirmTitle": "Do you really want to remove the index?",
"schemas.indexes.deleted": "Index has been scheduled for deletion. It might take a while, depending on the number of existing content items. This page is updated automatically.",
"schemas.indexes.deleteFailed": "Failed to create index. Please reload.",
"schemas.indexes.deleteFieldConfirmText": "Remove field",
"schemas.indexes.deleteFieldConfirmTitle": "Do you really want to remove the field?",
"schemas.indexes.empty": "No index created yet.",
"schemas.indexes.hint": "Indexes can be expensive. Analyze your queries and read the following documentation before creating an index: [Documentation](https://www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/)",
"schemas.indexes.loadFailed": "Failed to load indexes. Please reload.",
"schemas.indexes.notEnableHint1": "Indexes cannot be created. You have turn on a feature, which creates a dedicated collection per schema. Use the following environment variable:",
"schemas.indexes.notEnableHint2": "Please note, that this does not insert your existing content items to the new collections automatically. You have to run a migration by setting the following environment variable. Start Squidex with rebuild setting once and wait for completion. Then turn it off again.",
"schemas.indexes.reloaded": "Indexes reloaded.",
"schemas.listFields": "List Fields", "schemas.listFields": "List Fields",
"schemas.listFieldsEmpty": "Drop field here or reorder them to show the fields in the content list. When no list field is defined, the first field is used.", "schemas.listFieldsEmpty": "Drop field here or reorder them to show the fields in the content list. When no list field is defined, the first field is used.",
"schemas.loadFailed": "Failed to load schemas. Please reload.", "schemas.loadFailed": "Failed to load schemas. Please reload.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Field Rules", "schemas.rules.title": "Field Rules",
"schemas.rules.when": "when", "schemas.rules.when": "when",
"schemas.saved": "Schema saved successfully.", "schemas.saved": "Schema saved successfully.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Save and close", "schemas.saveFieldAndClose": "Save and close",
"schemas.saveFieldAndNew": "Save and add field", "schemas.saveFieldAndNew": "Save and add field",
"schemas.schemaHintsHint": "Describe this schema for documentation and user interfaces.", "schemas.schemaHintsHint": "Describe this schema for documentation and user interfaces.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schema synchronized successfully.", "schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.", "schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.",
"schemas.tabFields": "Fields", "schemas.tabFields": "Fields",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json", "schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Created", "schemas.tableHeaders.created": "Created",
"schemas.tableHeaders.created_title": "Created", "schemas.tableHeaders.created_title": "Created",

3
backend/i18n/translator/Squidex.Translator/Processes/CheckFrontend.cs

@ -92,7 +92,8 @@ public class CheckFrontend(DirectoryInfo folder, TranslationService service)
AddTranslations("\"i18n\\:(?<Key>[^\"]+)\""); AddTranslations("\"i18n\\:(?<Key>[^\"]+)\"");
AddTranslations("\'i18n\\:(?<Key>[^\']+)\'"); AddTranslations("\'i18n\\:(?<Key>[^\']+)\'");
AddTranslations("'(?<Key>[^\']+)' \\| sqxTranslate"); AddTranslations("\"(?<Key>[^\"]+)\"[\\s]*\\|[\\s]*sqxTranslate");
AddTranslations("\'(?<Key>[^\']+)\'[\\s]*\\|[\\s]*sqxTranslate");
return translations; return translations;
} }

7
backend/i18n/translator/Squidex.Translator/Processes/TranslateBackend.cs

@ -10,7 +10,7 @@ using Squidex.Translator.State;
namespace Squidex.Translator.Processes; namespace Squidex.Translator.Processes;
public class TranslateBackend(DirectoryInfo folder, TranslationService service) public partial class TranslateBackend(DirectoryInfo folder, TranslationService service)
{ {
private readonly DirectoryInfo folder = Backend.GetFolder(folder); private readonly DirectoryInfo folder = Backend.GetFolder(folder);
@ -22,7 +22,7 @@ public class TranslateBackend(DirectoryInfo folder, TranslationService service)
var isReplaced = false; var isReplaced = false;
content = Regex.Replace(content, "\"[^\"]*\"", match => content = VariableRegex().Replace(content, match =>
{ {
var value = match.Value[1..^1]; var value = match.Value[1..^1];
@ -50,4 +50,7 @@ public class TranslateBackend(DirectoryInfo folder, TranslationService service)
} }
} }
} }
[GeneratedRegex("\"[^\"]*\"")]
private static partial Regex VariableRegex();
} }

4
backend/i18n/translator/Squidex.Translator/Processes/TranslateTemplates.cs

@ -114,7 +114,7 @@ public partial class TranslateTemplates(DirectoryInfo folder, TranslationService
else else
{ {
// Keep our original indentation. // Keep our original indentation.
textNode.Text = originalPrefix + $"{{{{ '{key}' | sqxTranslate }}}}" + originalSuffix; textNode.Text = originalPrefix + $"{{{{ \"{key}\" | sqxTranslate }}}}" + originalSuffix;
isReplaced = true; isReplaced = true;
} }
@ -140,7 +140,7 @@ public partial class TranslateTemplates(DirectoryInfo folder, TranslationService
{ {
if (attribute.Name.Contains('[', StringComparison.Ordinal)) if (attribute.Name.Contains('[', StringComparison.Ordinal))
{ {
node.SetAttributeValue(attribute.Name, $"{{{{ '{key}' | sqxTranslate }}}}"); node.SetAttributeValue(attribute.Name, $"{{{{ \"{key}\" | sqxTranslate }}}}");
} }
else else
{ {

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -28,7 +28,7 @@ public partial class MongoContentRepository(
IMongoDatabase database, IMongoDatabase database,
IAppProvider appProvider, IAppProvider appProvider,
string shardKey, string shardKey,
IOptions<ContentOptions> options, IOptions<ContentsOptions> options,
ILogger<MongoContentRepository> log) ILogger<MongoContentRepository> log)
: MongoBase<MongoContentEntity>, IContentRepository, IInitializable : MongoBase<MongoContentEntity>, IContentRepository, IInitializable
{ {
@ -38,7 +38,7 @@ public partial class MongoContentRepository(
private readonly MongoContentCollection collectionPublished = private readonly MongoContentCollection collectionPublished =
new MongoContentCollection($"States_Contents_Published3{shardKey}", database, log, new MongoContentCollection($"States_Contents_Published3{shardKey}", database, log,
ReadPreference.Secondary, options.Value.OptimizeForSelfHosting); ReadPreference.Secondary, options.Value.OptimizeForSelfHosting);
private readonly ContentOptions options = options.Value; private readonly ContentsOptions options = options.Value;
public bool CanUseTransactions { get; private set; } public bool CanUseTransactions { get; private set; }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentCache.cs

@ -12,6 +12,6 @@ using Squidex.Infrastructure.Caching;
namespace Squidex.Domain.Apps.Entities.Contents; namespace Squidex.Domain.Apps.Entities.Contents;
public sealed class ContentCache(IMemoryCache? memoryCache, IOptions<ContentOptions> options) : QueryCache<DomainId, EnrichedContent>(options.Value.CanCache ? memoryCache : null), IContentCache public sealed class ContentCache(IMemoryCache? memoryCache, IOptions<ContentsOptions> options) : QueryCache<DomainId, EnrichedContent>(options.Value.CanCache ? memoryCache : null), IContentCache
{ {
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsOptions.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Contents; namespace Squidex.Domain.Apps.Entities.Contents;
public sealed class ContentOptions public sealed class ContentsOptions
{ {
public bool CanCache { get; set; } public bool CanCache { get; set; }

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

@ -27,12 +27,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries;
public class ContentQueryParser( public class ContentQueryParser(
IAppProvider appprovider, IAppProvider appprovider,
ITextIndex textIndex, ITextIndex textIndex,
IOptions<ContentOptions> options, IOptions<ContentsOptions> options,
IMemoryCache cache, IMemoryCache cache,
IJsonSerializer serializer) IJsonSerializer serializer)
{ {
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly ContentOptions options = options.Value; private readonly ContentsOptions options = options.Value;
public virtual async Task<Q> ParseAsync(Context context, Q q, Schema? schema = null, public virtual async Task<Q> ParseAsync(Context context, Q q, Schema? schema = null,
CancellationToken ct = default) CancellationToken ct = default)

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -22,12 +22,12 @@ public sealed class ContentQueryService(
IContentEnricher contentEnricher, IContentEnricher contentEnricher,
IContentRepository contentRepository, IContentRepository contentRepository,
IContentLoader contentLoader, IContentLoader contentLoader,
IOptions<ContentOptions> options, IOptions<ContentsOptions> options,
ContentQueryParser queryParser) ContentQueryParser queryParser)
: IContentQueryService : IContentQueryService
{ {
private const string SingletonId = "_schemaId_"; private const string SingletonId = "_schemaId_";
private readonly ContentOptions options = options.Value; private readonly ContentsOptions options = options.Value;
public async IAsyncEnumerable<EnrichedContent> StreamAsync(Context context, string schemaIdOrName, int skip, public async IAsyncEnumerable<EnrichedContent> StreamAsync(Context context, string schemaIdOrName, int skip,
[EnumeratorCancellation] CancellationToken ct = default) [EnumeratorCancellation] CancellationToken ct = default)

15
backend/src/Squidex.Shared/Texts.fr.resx

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve"> <data name="users.lockYourselfError" xml:space="preserve">
<value>Vous ne pouvez pas vous verrouiller.</value> <value>Vous ne pouvez pas vous verrouiller.</value>
</data> </data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<data name="users.login.custom" xml:space="preserve"> <data name="users.login.custom" xml:space="preserve">
<value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value> <value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value>
</data> </data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve"> <data name="users.login.loginWith" xml:space="preserve">
<value>{action} avec &lt;strong&gt;{provider}&lt;/strong&gt;</value> <value>{action} avec &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</data> </data>
<data name="users.login.noAccountLoginAction" xml:space="preserve">
<value>Cliquez ici pour vous identifier</value>
</data>
<data name="users.login.noAccountLoginQuestion" xml:space="preserve">
<value>Déjà enregistré?</value>
</data>
<data name="users.login.noAccountSignupAction" xml:space="preserve">
<value>Cliquez ici pour vous inscrire</value>
</data>
<data name="users.login.noAccountSignupQuestion" xml:space="preserve">
<value>Pas encore de compte ?</value>
</data>
<data name="users.login.passwordPlaceholder" xml:space="preserve"> <data name="users.login.passwordPlaceholder" xml:space="preserve">
<value>Entrer le mot de passe</value> <value>Entrer le mot de passe</value>
</data> </data>

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve"> <data name="users.lockYourselfError" xml:space="preserve">
<value>Non puoi bloccare te stesso.</value> <value>Non puoi bloccare te stesso.</value>
</data> </data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<data name="users.login.custom" xml:space="preserve"> <data name="users.login.custom" xml:space="preserve">
<value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value> <value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value>
</data> </data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve"> <data name="users.login.loginWith" xml:space="preserve">
<value>{action} con &lt;strong&gt;{provider}&lt;/strong&gt;</value> <value>{action} con &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</data> </data>
<data name="users.login.noAccountLoginAction" xml:space="preserve">
<value>Clicca qui per accedere</value>
</data>
<data name="users.login.noAccountLoginQuestion" xml:space="preserve">
<value>Sei già registrato?</value>
</data>
<data name="users.login.noAccountSignupAction" xml:space="preserve">
<value>Clicca qui per registrarti</value>
</data>
<data name="users.login.noAccountSignupQuestion" xml:space="preserve">
<value>Non hai ancora un account?</value>
</data>
<data name="users.login.passwordPlaceholder" xml:space="preserve"> <data name="users.login.passwordPlaceholder" xml:space="preserve">
<value>Inserisci la password</value> <value>Inserisci la password</value>
</data> </data>

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve"> <data name="users.lockYourselfError" xml:space="preserve">
<value>Je kunt jezelf niet vergrendelen.</value> <value>Je kunt jezelf niet vergrendelen.</value>
</data> </data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<data name="users.login.custom" xml:space="preserve"> <data name="users.login.custom" xml:space="preserve">
<value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value> <value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value>
</data> </data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve"> <data name="users.login.loginWith" xml:space="preserve">
<value>{action} met &lt;strong&gt; {provider} &lt;/strong&gt;</value> <value>{action} met &lt;strong&gt; {provider} &lt;/strong&gt;</value>
</data> </data>
<data name="users.login.noAccountLoginAction" xml:space="preserve">
<value>Klik hier om in te loggen</value>
</data>
<data name="users.login.noAccountLoginQuestion" xml:space="preserve">
<value>Al geregistreerd?</value>
</data>
<data name="users.login.noAccountSignupAction" xml:space="preserve">
<value>Klik hier om in te schrijven</value>
</data>
<data name="users.login.noAccountSignupQuestion" xml:space="preserve">
<value>Nog geen account?</value>
</data>
<data name="users.login.passwordPlaceholder" xml:space="preserve"> <data name="users.login.passwordPlaceholder" xml:space="preserve">
<value>Voer wachtwoord in</value> <value>Voer wachtwoord in</value>
</data> </data>

15
backend/src/Squidex.Shared/Texts.pt.resx

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve"> <data name="users.lockYourselfError" xml:space="preserve">
<value>Você não pode se trancar.</value> <value>Você não pode se trancar.</value>
</data> </data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<data name="users.login.custom" xml:space="preserve"> <data name="users.login.custom" xml:space="preserve">
<value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value> <value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value>
</data> </data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve"> <data name="users.login.loginWith" xml:space="preserve">
<value>{action} com &lt;strong&gt;{provider}&lt;/strong&gt;</value> <value>{action} com &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</data> </data>
<data name="users.login.noAccountLoginAction" xml:space="preserve">
<value>Carregue aqui para entrar</value>
</data>
<data name="users.login.noAccountLoginQuestion" xml:space="preserve">
<value>Já registado?</value>
</data>
<data name="users.login.noAccountSignupAction" xml:space="preserve">
<value>Carregue aqui para registar</value>
</data>
<data name="users.login.noAccountSignupQuestion" xml:space="preserve">
<value>Sem conta?</value>
</data>
<data name="users.login.passwordPlaceholder" xml:space="preserve"> <data name="users.login.passwordPlaceholder" xml:space="preserve">
<value>Introduzir Password</value> <value>Introduzir Password</value>
</data> </data>

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve"> <data name="users.lockYourselfError" xml:space="preserve">
<value>You cannot lock yourself.</value> <value>You cannot lock yourself.</value>
</data> </data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<data name="users.login.custom" xml:space="preserve"> <data name="users.login.custom" xml:space="preserve">
<value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value> <value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value>
</data> </data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve"> <data name="users.login.loginWith" xml:space="preserve">
<value>{action} with &lt;strong&gt;{provider}&lt;/strong&gt;</value> <value>{action} with &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</data> </data>
<data name="users.login.noAccountLoginAction" xml:space="preserve">
<value>Click here to login</value>
</data>
<data name="users.login.noAccountLoginQuestion" xml:space="preserve">
<value>Already registered?</value>
</data>
<data name="users.login.noAccountSignupAction" xml:space="preserve">
<value>Click here to signup</value>
</data>
<data name="users.login.noAccountSignupQuestion" xml:space="preserve">
<value>No account yet?</value>
</data>
<data name="users.login.passwordPlaceholder" xml:space="preserve"> <data name="users.login.passwordPlaceholder" xml:space="preserve">
<value>Enter Password</value> <value>Enter Password</value>
</data> </data>

15
backend/src/Squidex.Shared/Texts.zh.resx

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve"> <data name="users.lockYourselfError" xml:space="preserve">
<value>你不能锁定自己。</value> <value>你不能锁定自己。</value>
</data> </data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<data name="users.login.custom" xml:space="preserve"> <data name="users.login.custom" xml:space="preserve">
<value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value> <value>Enter your E-Mail Address to login with your Company Account and single sign on (SSO).</value>
</data> </data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve"> <data name="users.login.loginWith" xml:space="preserve">
<value>{action} with &lt;strong&gt;{provider}&lt;/strong&gt;</value> <value>{action} with &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</data> </data>
<data name="users.login.noAccountLoginAction" xml:space="preserve">
<value>点击此处登录</value>
</data>
<data name="users.login.noAccountLoginQuestion" xml:space="preserve">
<value>已经注册?</value>
</data>
<data name="users.login.noAccountSignupAction" xml:space="preserve">
<value>点击此处注册</value>
</data>
<data name="users.login.noAccountSignupQuestion" xml:space="preserve">
<value>还没有账号?</value>
</data>
<data name="users.login.passwordPlaceholder" xml:space="preserve"> <data name="users.login.passwordPlaceholder" xml:space="preserve">
<value>输入密码</value> <value>输入密码</value>
</data> </data>

4
backend/src/Squidex.Web/Services/UrlGenerator.cs

@ -20,11 +20,11 @@ public sealed class UrlGenerator(
IGenericUrlGenerator urlGenerator, IGenericUrlGenerator urlGenerator,
IAssetFileStore assetFileStore, IAssetFileStore assetFileStore,
IOptions<AssetOptions> assetOptions, IOptions<AssetOptions> assetOptions,
IOptions<ContentOptions> contentOptions) IOptions<ContentsOptions> contentOptions)
: IUrlGenerator, IHttpImageEndpoint : IUrlGenerator, IHttpImageEndpoint
{ {
private readonly AssetOptions assetOptions = assetOptions.Value; private readonly AssetOptions assetOptions = assetOptions.Value;
private readonly ContentOptions contentOptions = contentOptions.Value; private readonly ContentsOptions contentOptions = contentOptions.Value;
public string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType) public string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType)
{ {

2
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaIndexesController.cs

@ -79,7 +79,7 @@ public class SchemaIndexesController(ICommandBus commandBus, IJobService jobServ
/// <param name="name">The name of the index.</param> /// <param name="name">The name of the index.</param>
/// <response code="204">Schema index deletion added to job queue.</response> /// <response code="204">Schema index deletion added to job queue.</response>
/// <response code="404">Schema or app not found.</response> /// <response code="404">Schema or app not found.</response>
[HttpPost] [HttpDelete]
[Route("apps/{app}/schemas/{schema}/indexes/{name}")] [Route("apps/{app}/schemas/{schema}/indexes/{name}")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermissionOrAnonymous(PermissionIds.AppSchemasIndexes)] [ApiPermissionOrAnonymous(PermissionIds.AppSchemasIndexes)]

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

@ -32,7 +32,6 @@ public static class IndexExtensions
}; };
var uiOptions = httpContext.RequestServices.GetService<IOptions<MyUIOptions>>()?.Value; var uiOptions = httpContext.RequestServices.GetService<IOptions<MyUIOptions>>()?.Value;
if (uiOptions != null) if (uiOptions != null)
{ {
var clonedOptions = uiOptions with var clonedOptions = uiOptions with

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

@ -24,7 +24,7 @@ public static class ContentsServices
{ {
public static void AddSquidexContents(this IServiceCollection services, IConfiguration config) public static void AddSquidexContents(this IServiceCollection services, IConfiguration config)
{ {
services.Configure<ContentOptions>(config, services.Configure<ContentsOptions>(config,
"contents"); "contents");
services.Configure<TemplatesOptions>(config, services.Configure<TemplatesOptions>(config,

8
backend/src/Squidex/Config/Domain/FontendServices.cs

@ -9,6 +9,7 @@ using System.Text.Json;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.AI; using Squidex.AI;
using Squidex.Areas.Api.Controllers.UI; using Squidex.Areas.Api.Controllers.UI;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History;
using Squidex.Hosting; using Squidex.Hosting;
using Squidex.Text.Translations; using Squidex.Text.Translations;
@ -56,6 +57,13 @@ public static class FontendServices
options.More["canUseChatBot"] = chatAgent.IsConfigured; options.More["canUseChatBot"] = chatAgent.IsConfigured;
}); });
services.Configure<MyUIOptions>((services, options) =>
{
var contentsOptions = services.GetRequiredService<IOptions<ContentsOptions>>();
options.More["canCreateIndexes"] = contentsOptions.Value.OptimizeForSelfHosting;
});
services.Configure<MyUIOptions>((services, options) => services.Configure<MyUIOptions>((services, options) =>
{ {
if (string.IsNullOrWhiteSpace(options.CollaborationService)) if (string.IsNullOrWhiteSpace(options.CollaborationService))

3
backend/src/Squidex/appsettings.json

@ -161,6 +161,9 @@
// Hide all onboarding tooltips and dialogs. // Hide all onboarding tooltips and dialogs.
"hideOnboarding": false, "hideOnboarding": false,
// Hide the indexes UI.
"hideIndexes": false,
// Hide the today and now button. // Hide the today and now button.
"hideDateButtons": false, "hideDateButtons": false,

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -126,7 +126,7 @@ public abstract class GraphQLTestBase : IClassFixture<TranslationsFixture>
{ {
x.CanCache = true; x.CanCache = true;
}) })
.Configure<ContentOptions>(x => .Configure<ContentsOptions>(x =>
{ {
x.CanCache = true; x.CanCache = true;
}) })

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs

@ -75,7 +75,7 @@ public abstract class ContentsQueryFixture : GivenContext, IAsyncLifetime
var services = var services =
new ServiceCollection() new ServiceCollection()
.AddSingleton(Options.Create(new ContentOptions { OptimizeForSelfHosting = selfHosting })) .AddSingleton(Options.Create(new ContentsOptions { OptimizeForSelfHosting = selfHosting }))
.AddSingleton(CreateAppProvider()) .AddSingleton(CreateAppProvider())
.AddSingleton(mongoClient) .AddSingleton(mongoClient)
.AddSingleton(mongoDatabase) .AddSingleton(mongoDatabase)

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs

@ -26,7 +26,7 @@ public class ContentQueryParserTests : GivenContext
public ContentQueryParserTests() public ContentQueryParserTests()
{ {
var options = Options.Create(new ContentOptions { DefaultPageSize = 30 }); var options = Options.Create(new ContentsOptions { DefaultPageSize = 30 });
Schema = Schema Schema = Schema
.AddString(1, "firstName", Partitioning.Invariant) .AddString(1, "firstName", Partitioning.Invariant)

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -45,7 +45,7 @@ public class ContentQueryServiceTests : GivenContext
A.CallTo(() => queryParser.ParseAsync(A<Context>._, A<Q>._, A<Schema?>._, CancellationToken)) A.CallTo(() => queryParser.ParseAsync(A<Context>._, A<Q>._, A<Schema?>._, CancellationToken))
.ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!)); .ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!));
var options = Options.Create(new ContentOptions()); var options = Options.Create(new ContentsOptions());
sut = new ContentQueryService( sut = new ContentQueryService(
AppProvider, AppProvider,

1
frontend/.prettierrc

@ -1,5 +1,6 @@
{ {
"plugins": ["squidex-prettier-plugin-organize-attributes"], "plugins": ["squidex-prettier-plugin-organize-attributes"],
"endOfLine": "crlf",
"overrides": [ "overrides": [
{ {
"files": "*.html", "files": "*.html",

8
frontend/package-lock.json

@ -107,7 +107,7 @@
"karma-coverage": "~2.2.1", "karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.2.5", "prettier": "3.3.3",
"squidex-prettier-plugin-organize-attributes": "^1.0.1", "squidex-prettier-plugin-organize-attributes": "^1.0.1",
"storybook": "^8.1.5", "storybook": "^8.1.5",
"stylelint": "16.6.1", "stylelint": "16.6.1",
@ -22682,9 +22682,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.2.5", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"

2
frontend/package.json

@ -114,7 +114,7 @@
"karma-coverage": "~2.2.1", "karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.2.5", "prettier": "3.3.3",
"squidex-prettier-plugin-organize-attributes": "^1.0.1", "squidex-prettier-plugin-organize-attributes": "^1.0.1",
"storybook": "^8.1.5", "storybook": "^8.1.5",
"stylelint": "16.6.1", "stylelint": "16.6.1",

2694
frontend/src/app/_theme.html

File diff suppressed because it is too large

2
frontend/src/app/features/apps/pages/apps-page.component.html

@ -62,7 +62,7 @@
} }
</div> </div>
} }
@if (info) { @if (info) {
<div class="apps-section"> <div class="apps-section">
<small class="info">{{ info }}</small> <small class="info">{{ info }}</small>

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

@ -16,7 +16,7 @@
{{ "tour.welcome" | sqxTranslate }} {{ "tour.welcome" | sqxTranslate }}
<span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span> <span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span>
</h1> </h1>
<div [innerHTML]="'tour.stepIntroText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="'tour.stepIntroText' | sqxTranslate"></div>
<div class="mt-4"> <div class="mt-4">
<button class="btn btn-success" (click)="next()"> <button class="btn btn-success" (click)="next()">
{{ "tour.stepIntroNext" | sqxTranslate }} {{ "tour.stepIntroNext" | sqxTranslate }}
@ -32,7 +32,7 @@
<div class="onboarding-enter-leave" @slide> <div class="onboarding-enter-leave" @slide>
<form [formGroup]="answersForm" (ngSubmit)="submitAnswers()"> <form [formGroup]="answersForm" (ngSubmit)="submitAnswers()">
<h2>{{ "tour.stepDataTitle" | sqxTranslate }}</h2> <h2>{{ "tour.stepDataTitle" | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepDataText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="'tour.stepDataText' | sqxTranslate"></div>
<div class="form-group mt-4"> <div class="form-group mt-4">
<label for="role">{{ "tour.stepDataCompanyRole" | sqxTranslate }}</label> <label for="role">{{ "tour.stepDataCompanyRole" | sqxTranslate }}</label>
<select class="form-select" id="companyRole" formControlName="companyRole"> <select class="form-select" id="companyRole" formControlName="companyRole">
@ -79,7 +79,7 @@
<img class="header-left" @fade src="./images/logo-white-small.png" /> <img class="header-left" @fade src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave text-center" @slide> <div class="onboarding-enter-leave text-center" @slide>
<h2>{{ "tour.stepTourTitle" | sqxTranslate }}</h2> <h2>{{ "tour.stepTourTitle" | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepTourText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="'tour.stepTourText' | sqxTranslate"></div>
<div class="mt-4"> <div class="mt-4">
<button class="btn btn-success" (click)="start()"> <button class="btn btn-success" (click)="start()">
{{ "tour.startYes" | sqxTranslate }} {{ "tour.startYes" | sqxTranslate }}

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

@ -8,7 +8,7 @@
import { Component, EventEmitter, Output } from '@angular/core'; import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { fadeAnimation, MarkdownPipe, ModalDialogComponent, SafeHtmlPipe, slideAnimation, TourState, TranslatePipe, UsersService } from '@app/shared'; import { fadeAnimation, MarkdownDirective, ModalDialogComponent, slideAnimation, TourState, TranslatePipe, UsersService } from '@app/shared';
@Component({ @Component({
standalone: true, standalone: true,
@ -20,10 +20,9 @@ import { fadeAnimation, MarkdownPipe, ModalDialogComponent, SafeHtmlPipe, slideA
], ],
imports: [ imports: [
FormsModule, FormsModule,
MarkdownPipe, MarkdownDirective,
ModalDialogComponent, ModalDialogComponent,
ReactiveFormsModule, ReactiveFormsModule,
SafeHtmlPipe,
TranslatePipe, TranslatePipe,
], ],
}) })

2
frontend/src/app/features/assets/pages/assets-page.component.html

@ -62,7 +62,7 @@
<div class="col-auto"> <div class="col-auto">
<button <button
class="btn btn-success" class="btn btn-success"
attr.aria-label="{{ 'i18n:assets.createFolder' | sqxTranslate }}" attr.aria-label="{{ 'assets.createFolder' | sqxTranslate }}"
(click)="addAssetFolderDialog.show()" (click)="addAssetFolderDialog.show()"
shortcut="CTRL + U" shortcut="CTRL + U"
title="i18n:assets.createFolderTooltip" title="i18n:assets.createFolderTooltip"

10
frontend/src/app/features/content/pages/content/editor/content-editor.component.html

@ -14,16 +14,10 @@
<a (click)="loadLatest.emit()">{{ "contents.viewLatest" | sqxTranslate }}</a> <a (click)="loadLatest.emit()">{{ "contents.viewLatest" | sqxTranslate }}</a>
</div> </div>
@if (isDeleted) { @if (isDeleted) {
<div <div inline="true" [sqxMarkdown]="'contents.versionViewingDeleted' | sqxTranslate: { version: contentVersion }"></div>
[innerHTML]="
'contents.versionViewingDeleted' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml
"></div>
} }
@if (!isDeleted) { @if (!isDeleted) {
<div <div inline="true" [sqxMarkdown]="'contents.versionViewing' | sqxTranslate: { version: contentVersion }"></div>
[innerHTML]="
'contents.versionViewing' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml
"></div>
} }
</div> </div>
} }

5
frontend/src/app/features/content/pages/content/editor/content-editor.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core'; import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { AppLanguageDto, CursorsComponent, CursorsDirective, EditContentForm, FormErrorComponent, ListViewComponent, MarkdownInlinePipe, SafeHtmlPipe, SchemaDto, TranslatePipe, Version } from '@app/shared'; import { AppLanguageDto, CursorsComponent, CursorsDirective, EditContentForm, FormErrorComponent, ListViewComponent, MarkdownDirective, SchemaDto, TranslatePipe, Version } from '@app/shared';
import { ContentSectionComponent } from '../../../shared/forms/content-section.component'; import { ContentSectionComponent } from '../../../shared/forms/content-section.component';
@Component({ @Component({
@ -24,8 +24,7 @@ import { ContentSectionComponent } from '../../../shared/forms/content-section.c
FormErrorComponent, FormErrorComponent,
FormsModule, FormsModule,
ListViewComponent, ListViewComponent,
MarkdownInlinePipe, MarkdownDirective,
SafeHtmlPipe,
TranslatePipe, TranslatePipe,
], ],
}) })

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

@ -1,8 +1,6 @@
<div class="row gx-1" [class.disabled]="snapshot.isDisabled"> <div class="row gx-1" [class.disabled]="snapshot.isDisabled">
<div class="col" [class.disabled]="snapshot.isDisabled"> <div class="col" [class.disabled]="snapshot.isDisabled">
<button type="button" <button class="btn btn-outline-secondary btn-dashed btn-block btn-add" (click)="assetsDialog.show()" type="button">
class="btn btn-outline-secondary btn-dashed btn-block btn-add"
(click)="assetsDialog.show()">
{{ "contents.assetsUpload" | sqxTranslate }} {{ "contents.assetsUpload" | sqxTranslate }}
</button> </button>
</div> </div>
@ -13,7 +11,12 @@
[sqxDropDisabled]="snapshot.isDisabled" [sqxDropDisabled]="snapshot.isDisabled"
(sqxDropFile)="addFiles($event)"> (sqxDropFile)="addFiles($event)">
{{ "contents.assetsSelect" | sqxTranslate }} {{ "contents.assetsSelect" | sqxTranslate }}
<input class="file-drop-button-input hidden" #fileInput (change)="addFiles($any($event.target!)['files'])" multiple type="file" /> <input
class="file-drop-button-input hidden"
#fileInput
(change)="addFiles($any($event.target!)['files'])"
multiple
type="file" />
</div> </div>
</div> </div>

4
frontend/src/app/features/dashboard/pages/dashboard-page.component.html

@ -5,9 +5,7 @@
@if (!isScrolled) { @if (!isScrolled) {
<div class="dashboard-summary" @fade sqxTourStep="appDashboard"> <div class="dashboard-summary" @fade sqxTourStep="appDashboard">
<h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1> <h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1>
<div <div class="subtext" [sqxMarkdown]="'dashboard.welcomeText' | sqxTranslate: { app: app.displayName }"></div>
class="subtext"
[innerHTML]="'dashboard.welcomeText' | sqxTranslate: { app: app.displayName } | sqxMarkdown | sqxSafeHtml"></div>
</div> </div>
} }
<gridster #grid [options]="gridOptions"> <gridster #grid [options]="gridOptions">

5
frontend/src/app/features/dashboard/pages/dashboard-page.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponent, GridType } from 'angular-gridster2'; import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponent, GridType } from 'angular-gridster2';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppsState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, fadeAnimation, IFrameCardComponent, LocalStoreService, MarkdownPipe, RandomCatCardComponent, RandomDogCardComponent, SafeHtmlPipe, Settings, StorageUsagePerDateDto, Subscriptions, SupportCardComponent, switchSafe, TitleComponent, TourStepDirective, TranslatePipe, UsagesService } from '@app/shared'; import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AppsState, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, fadeAnimation, IFrameCardComponent, LocalStoreService, MarkdownDirective, RandomCatCardComponent, RandomDogCardComponent, Settings, StorageUsagePerDateDto, Subscriptions, SupportCardComponent, switchSafe, TitleComponent, TourStepDirective, TranslatePipe, UsagesService } from '@app/shared';
import { ApiCardComponent } from './cards/api-card.component'; import { ApiCardComponent } from './cards/api-card.component';
import { ContentSummaryCardComponent } from './cards/content-summary-card.component'; import { ContentSummaryCardComponent } from './cards/content-summary-card.component';
import { GithubCardComponent } from './cards/github-card.component'; import { GithubCardComponent } from './cards/github-card.component';
@ -42,10 +42,9 @@ import { DashboardConfigComponent } from './dashboard-config.component';
GridsterItemComponent, GridsterItemComponent,
HistoryCardComponent, HistoryCardComponent,
IFrameCardComponent, IFrameCardComponent,
MarkdownPipe, MarkdownDirective,
RandomCatCardComponent, RandomCatCardComponent,
RandomDogCardComponent, RandomDogCardComponent,
SafeHtmlPipe,
SchemaCardComponent, SchemaCardComponent,
SupportCardComponent, SupportCardComponent,
TitleComponent, TitleComponent,

2
frontend/src/app/features/rules/pages/rules/rule.component.html

@ -14,8 +14,8 @@
<button <button
class="btn btn-text-secondary" class="btn btn-text-secondary"
#buttonOptions #buttonOptions
(click)="dropdown.toggle()"
attr.aria-label="{{ 'common.options' | sqxTranslate }}" attr.aria-label="{{ 'common.options' | sqxTranslate }}"
(click)="dropdown.toggle()"
type="button"> type="button">
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>

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

@ -27,5 +27,6 @@
</button> </button>
} }
} }
<sqx-field-wizard (dialogClose)="fieldWizard.hide()" [schema]="schema" [settings]="settings" *sqxModal="fieldWizard"></sqx-field-wizard> <sqx-field-wizard (dialogClose)="fieldWizard.hide()" [schema]="schema" [settings]="settings" *sqxModal="fieldWizard"></sqx-field-wizard>
} }

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

@ -26,7 +26,7 @@
<input class="form-control" id="{{ field.fieldId }}_previewFormat" formControlName="previewFormat" /> <input class="form-control" id="{{ field.fieldId }}_previewFormat" formControlName="previewFormat" />
<sqx-form-hint> <sqx-form-hint>
<span [innerHTML]="'schemas.fieldTypes.assets.previewFormatHint' | sqxTranslate | sqxMarkdownInline | sqxSafeHtml"></span> <span inline="true" [sqxMarkdown]="'schemas.fieldTypes.assets.previewFormatHint' | sqxTranslate"></span>
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>
@ -41,7 +41,7 @@
</div> </div>
<sqx-form-hint> <sqx-form-hint>
{{ "schemas.fieldTypes.assets.resolveHint" | sqxTranslate | sqxMarkdownInline }} <span inline="true" [sqxMarkdown]="'schemas.fieldTypes.assets.resolveHint' | sqxTranslate"></span>
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>

5
frontend/src/app/features/schemas/pages/schema/fields/types/assets-ui.component.ts

@ -7,7 +7,7 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { AssetFolderDropdownComponent, AssetsFieldPropertiesDto, FieldDto, FormHintComponent, MarkdownInlinePipe, SafeHtmlPipe, TranslatePipe } from '@app/shared'; import { AssetFolderDropdownComponent, AssetsFieldPropertiesDto, FieldDto, FormHintComponent, MarkdownDirective, TranslatePipe } from '@app/shared';
@Component({ @Component({
standalone: true, standalone: true,
@ -18,9 +18,8 @@ import { AssetFolderDropdownComponent, AssetsFieldPropertiesDto, FieldDto, FormH
AssetFolderDropdownComponent, AssetFolderDropdownComponent,
FormHintComponent, FormHintComponent,
FormsModule, FormsModule,
MarkdownInlinePipe, MarkdownDirective,
ReactiveFormsModule, ReactiveFormsModule,
SafeHtmlPipe,
TranslatePipe, TranslatePipe,
], ],
}) })

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

@ -21,7 +21,7 @@
<input class="form-control" id="{{ field.fieldId }}_fieldFormat" formControlName="format" maxlength="100" /> <input class="form-control" id="{{ field.fieldId }}_fieldFormat" formControlName="format" maxlength="100" />
<sqx-form-hint> <sqx-form-hint>
<span [innerHTML]="'schemas.fieldTypes.dateTime.formatHint' | sqxTranslate | sqxMarkdownInline | sqxSafeHtml"></span> <span inline="true" [sqxMarkdown]="'schemas.fieldTypes.dateTime.formatHint' | sqxTranslate"></span>
</sqx-form-hint> </sqx-form-hint>
</div> </div>
</div> </div>

5
frontend/src/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts

@ -8,7 +8,7 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { DATETIME_FIELD_EDITORS, DateTimeFieldPropertiesDto, FieldDto, FloatConverter, FormHintComponent, MarkdownInlinePipe, SafeHtmlPipe, TranslatePipe } from '@app/shared'; import { DATETIME_FIELD_EDITORS, DateTimeFieldPropertiesDto, FieldDto, FloatConverter, FormHintComponent, MarkdownDirective, TranslatePipe } from '@app/shared';
@Component({ @Component({
standalone: true, standalone: true,
@ -18,9 +18,8 @@ import { DATETIME_FIELD_EDITORS, DateTimeFieldPropertiesDto, FieldDto, FloatConv
imports: [ imports: [
FormHintComponent, FormHintComponent,
FormsModule, FormsModule,
MarkdownInlinePipe, MarkdownDirective,
ReactiveFormsModule, ReactiveFormsModule,
SafeHtmlPipe,
TranslatePipe, TranslatePipe,
], ],
}) })

73
frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.html

@ -0,0 +1,73 @@
<form (ngSubmit)="createSchema()">
<sqx-modal-dialog (dialogClose)="dialogClose.emit()" size="md">
<ng-container title>
{{ "schemas.indexes.addTitle" | sqxTranslate }}
</ng-container>
<ng-container content>
<sqx-form-hint>
<span inline="true" [sqxMarkdown]="'schemas.indexes.hint' | sqxTranslate" trusted="true"></span>
</sqx-form-hint>
@for (form of createForm.controls; track form; let i = $index) {
<div class="form-group row gx-2" attr.data-testid="pattern_{{ form.get('name')?.value }}" [formGroup]="form">
<div class="col">
<sqx-control-errors for="name"></sqx-control-errors>
<select class="form-select" formControlName="name">
@for (fieldName of fieldNames; track fieldName) {
<option [ngValue]="fieldName">{{ fieldName }}</option>
}
</select>
</div>
<div class="col-4">
<sqx-control-errors for="order"></sqx-control-errors>
<select class="form-select" formControlName="order">
<option [ngValue]="'Ascending'">Ascending</option>
<option [ngValue]="'Descending'">Descending</option>
</select>
</div>
<div class="col-auto">
<button
class="btn btn-text-danger"
attr.aria-label="{{ 'common.delete' | sqxTranslate }}"
confirmRememberKey="deleteIndexField"
confirmText="i18n:schemas.indexes.deleteFieldConfirmText"
confirmTitle="i18n:schemas.indexes.deleteFieldConfirmTitle"
(sqxConfirmClick)="createForm.form.removeAt(i)"
type="button">
<i class="icon-bin2"></i>
</button>
</div>
</div>
}
<div class="form-group row gx-2">
<div class="col">
<div class="form-control preview">{{ "common.name" | sqxTranslate }}</div>
</div>
<div class="col-4">
<div class="form-control preview">{{ "common.order" | sqxTranslate }}</div>
</div>
<div class="col-auto">
<button
class="btn btn-success"
attr.aria-label="{{ 'common.add' | sqxTranslate }}"
(click)="createForm.form.add()"
type="button">
<i class="icon-add"></i>
</button>
</div>
</div>
</ng-container>
<ng-container footer>
<button class="btn btn-text-secondary" (click)="dialogClose.emit()" type="button">
{{ "common.cancel" | sqxTranslate }}
</button>
<button class="btn btn-success" type="submit">
{{ "common.create" | sqxTranslate }}
</button>
</ng-container>
</sqx-modal-dialog>
</form>

2
frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.scss

@ -0,0 +1,2 @@
@import 'mixins';
@import 'vars';

100
frontend/src/app/features/schemas/pages/schema/indexes/index-form.component.ts

@ -0,0 +1,100 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppLanguageDto, ConfirmClickDirective, ControlErrorsComponent, CreateIndexForm, FormHintComponent, IndexesState, MarkdownDirective, ModalDialogComponent, SchemaDto, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
selector: 'sqx-index-form',
styleUrls: ['./index-form.component.scss'],
templateUrl: './index-form.component.html',
imports: [
ConfirmClickDirective,
ControlErrorsComponent,
FormHintComponent,
FormsModule,
MarkdownDirective,
ModalDialogComponent,
ReactiveFormsModule,
TranslatePipe,
],
})
export class IndexFormComponent {
@Output()
public create = new EventEmitter();
@Output()
public dialogClose = new EventEmitter();
@Input({ required: true })
public schema!: SchemaDto;
@Input({ required: true })
public languages!: AppLanguageDto[];
public createForm = new CreateIndexForm();
public fieldNames: string[] = [];
constructor(
private readonly indexesState: IndexesState,
) {
}
public ngOnInit() {
const metaFields: string[] = [
'created',
'createdBy',
'lastModified',
'lastModifiedBy',
'newStatus',
'status',
'version',
];
const dataFields: string[] = [];
for (const field of this.schema.fields) {
if (field.properties.isContentField) {
if (field.isLocalizable) {
for (const language of this.languages) {
dataFields.push(`data.${field.name}.${language.iso2Code}`);
}
} else {
dataFields.push(`data.${field.name}.iv`);
}
}
}
this.fieldNames = [...metaFields, ...dataFields.sort()];
}
public emitCreate() {
this.create.emit();
}
public emitClose() {
this.dialogClose.emit();
}
public createSchema() {
const fields = this.createForm.submit();
if (fields) {
this.indexesState.create({ fields })
.subscribe({
next: () => {
this.emitCreate();
},
error: error => {
this.createForm.submitFailed(error);
},
});
}
}
}

57
frontend/src/app/features/schemas/pages/schema/indexes/index.component.html

@ -0,0 +1,57 @@
<div class="table-items-row table-items-row-expandable">
<div class="table-items-row-summary row gx-2 align-items-center">
<div class="col">
<span class="truncate">{{ index.name }}</span>
</div>
<div class="col-auto">
<div class="float-end">
<button
class="btn btn-outline-secondary btn-expand me-1"
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
[class.expanded]="isExpanded"
(click)="toggleExpanded()"
type="button">
<span class="hidden">{{ "common.settings" | sqxTranslate }}</span>
<i class="icon-settings"></i>
</button>
<button
class="btn btn-text-danger"
attr.aria-label="{{ 'common.delete' | sqxTranslate }}"
confirmRememberKey="deleteIndex"
confirmText="i18n:schemas.indexes.deleteConfirmText"
confirmTitle="i18n:schemas.indexes.deleteConfirmTitle"
[disabled]="!index.canDelete"
(sqxConfirmClick)="delete()"
type="button">
<i class="icon-bin2"></i>
</button>
</div>
</div>
</div>
@if (isExpanded) {
<div class="table-items-row-details">
<table class="table table-sm table-fixed mb-0">
<colgroup>
<col />
<col style="width: 300px" />
</colgroup>
<thead>
<tr>
<th>{{ "common.field" | sqxTranslate }}</th>
<th>{{ "common.order" | sqxTranslate }}</th>
</tr>
</thead>
<tbody>
@for (field of index.fields; track field.name) {
<tr>
<td>{{ field.name }}</td>
<td>{{ field.order }}</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>

12
frontend/src/app/features/schemas/pages/schema/indexes/index.component.scss

@ -0,0 +1,12 @@
@import 'mixins';
@import 'vars';
th {
padding-left: 1rem !important;
padding-right: 1rem;
background: $color-border-light !important;
}
td {
padding-left: 1rem;
}

39
frontend/src/app/features/schemas/pages/schema/indexes/index.component.ts

@ -0,0 +1,39 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input } from '@angular/core';
import { ConfirmClickDirective, IndexDto, IndexesState, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
selector: 'sqx-index',
styleUrls: ['./index.component.scss'],
templateUrl: './index.component.html',
imports: [
ConfirmClickDirective,
TranslatePipe,
],
})
export class IndexComponent {
@Input({ required: true })
public index!: IndexDto;
public isExpanded?: boolean | null;
constructor(
private readonly indexesState: IndexesState,
) {
}
public toggleExpanded() {
this.isExpanded = !this.isExpanded;
}
public delete() {
this.indexesState.delete(this.index);
}
}

55
frontend/src/app/features/schemas/pages/schema/indexes/schema-indexes.component.html

@ -0,0 +1,55 @@
<sqx-list-view innerWidth="50rem">
@if (canCreateIndexes) {
<div class="mt-2">
@if ((indexesState.isLoaded | async) && (indexesState.indexes | async); as indexes) {
@if (indexes.length === 0) {
<div class="table-items-row table-items-row-summary table-items-row-empty">
{{ "schemas.indexes.empty" | sqxTranslate }}
<button class="btn btn-success btn-sm ms-2" (click)="addIndexDialog.show()" sqxTourStep="addField" type="button">
<i class="icon icon-plus"></i>
{{ "schemas.indexes.addIndex" | sqxTranslate }}
</button>
</div>
}
@for (index of indexes; track index.name) {
<sqx-index [index]="index"></sqx-index>
}
}
</div>
} @else {
<div class="table-items-row table-items-row-summary table-items-row-empty text-sm">
{{ "schemas.indexes.empty" | sqxTranslate }}
<div class="section">
<span [sqxMarkdown]="'schemas.indexes.notEnableHint1' | sqxTranslate" trusted="true"></span>
<sqx-code>CONTENTS__OPTIMIZEFORSELFHOSTING=true</sqx-code>
</div>
<div class="section">
<span [sqxMarkdown]="'schemas.indexes.notEnableHint2' | sqxTranslate" trusted="true"></span>
<sqx-code>REBUILD__CONTENTS=true</sqx-code>
</div>
</div>
}
</sqx-list-view>
@if (canCreateIndexes) {
@if (indexesState.canCreate | async) {
<button class="btn btn-success index-button" (click)="addIndexDialog.show()" type="button">
<i class="icon icon-plus index-button-icon"></i>
<div class="index-button-text">{{ "schemas.indexes.addIndexButton" | sqxTranslate }}</div>
</button>
}
@if (languagesState.isoLanguages | async; as languages) {
<sqx-index-form
(create)="addIndexDialog.hide()"
(dialogClose)="addIndexDialog.hide()"
[languages]="languages"
[schema]="schema"
*sqxModal="addIndexDialog"></sqx-index-form>
}
}

22
frontend/src/app/features/schemas/pages/schema/indexes/schema-indexes.component.scss

@ -0,0 +1,22 @@
@import 'mixins';
@import 'vars';
.index-button {
@include circle(5.25rem);
@include box-shadow-outer(0, 8px, 16px, .3);
@include absolute(auto, 6rem, 1rem, auto);
&-icon {
font-weight: bold;
}
&-text {
font-size: $font-small;
}
}
.section {
font-size: $font-small;
font-weight: normal;
margin-top: 1rem;
}

57
frontend/src/app/features/schemas/pages/schema/indexes/schema-indexes.component.ts

@ -0,0 +1,57 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AsyncPipe } from '@angular/common';
import { Component, inject, Input, OnInit } from '@angular/core';
import { switchMap, timer } from 'rxjs';
import { CodeComponent, DialogModel, IndexesState, LanguagesState, ListViewComponent, MarkdownDirective, ModalDirective, SchemaDto, Subscriptions, TranslatePipe, UIOptions } from '@app/shared';
import { IndexFormComponent } from './index-form.component';
import { IndexComponent } from './index.component';
@Component({
standalone: true,
selector: 'sqx-schema-indexes',
styleUrls: ['./schema-indexes.component.scss'],
templateUrl: './schema-indexes.component.html',
imports: [
AsyncPipe,
CodeComponent,
IndexFormComponent,
IndexComponent,
MarkdownDirective,
ModalDirective,
ListViewComponent,
TranslatePipe,
],
})
export class SchemaIndexesComponent implements OnInit {
private readonly subscriptions = new Subscriptions();
public readonly canCreateIndexes = inject(UIOptions).value.canCreateIndexes;
@Input({ required: true })
public schema!: SchemaDto;
public addIndexDialog = new DialogModel();
constructor(
public readonly indexesState: IndexesState,
public readonly languagesState: LanguagesState,
) {
}
public ngOnInit() {
this.indexesState.load();
this.subscriptions.add(
timer(3000, 3000).pipe(
switchMap(() => this.indexesState.load(false, true))));
this.languagesState.load();
}
}

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

@ -28,6 +28,13 @@
{{ "schemas.tabJson" | sqxTranslate }} {{ "schemas.tabJson" | sqxTranslate }}
</a> </a>
</li> </li>
@if (!hideIndexes) {
<li>
<a class="nav-link" [class.active]="tab === 'indexes'" [queryParams]="{ tab: 'indexes' }" [routerLink]="[]">
{{ "schemas.tabIndexes" | sqxTranslate }}
</a>
</li>
}
<li> <li>
<a class="nav-link" [class.active]="tab === 'more'" [queryParams]="{ tab: 'more' }" [routerLink]="[]"> <a class="nav-link" [class.active]="tab === 'more'" [queryParams]="{ tab: 'more' }" [routerLink]="[]">
{{ "schemas.tabMore" | sqxTranslate }} {{ "schemas.tabMore" | sqxTranslate }}
@ -94,6 +101,9 @@
@case ("json") { @case ("json") {
<sqx-schema-export-form [schema]="schema"></sqx-schema-export-form> <sqx-schema-export-form [schema]="schema"></sqx-schema-export-form>
} }
@case ("indexes") {
<sqx-schema-indexes [schema]="schema"></sqx-schema-indexes>
}
@case ("more") { @case ("more") {
<sqx-list-view innerWidth="50rem"> <sqx-list-view innerWidth="50rem">
<div> <div>

8
frontend/src/app/features/schemas/pages/schema/schema-page.component.ts

@ -6,14 +6,15 @@
*/ */
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ConfirmClickDirective, defined, DropdownMenuComponent, LayoutComponent, ListViewComponent, MessageBus, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, SchemasState, SidebarMenuDirective, Subscriptions, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared'; import { ConfirmClickDirective, defined, DropdownMenuComponent, LayoutComponent, ListViewComponent, MessageBus, ModalDirective, ModalModel, ModalPlacementDirective, SchemaDto, SchemasState, SidebarMenuDirective, Subscriptions, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, UIOptions } from '@app/shared';
import { SchemaCloning } from '../messages'; import { SchemaCloning } from '../messages';
import { SchemaEditFormComponent } from './common/schema-edit-form.component'; import { SchemaEditFormComponent } from './common/schema-edit-form.component';
import { SchemaExportFormComponent } from './export/schema-export-form.component'; import { SchemaExportFormComponent } from './export/schema-export-form.component';
import { SchemaFieldsComponent } from './fields/schema-fields.component'; import { SchemaFieldsComponent } from './fields/schema-fields.component';
import { SchemaIndexesComponent } from './indexes/schema-indexes.component';
import { SchemaPreviewUrlsFormComponent } from './preview/schema-preview-urls-form.component'; import { SchemaPreviewUrlsFormComponent } from './preview/schema-preview-urls-form.component';
import { SchemaFieldRulesFormComponent } from './rules/schema-field-rules-form.component'; import { SchemaFieldRulesFormComponent } from './rules/schema-field-rules-form.component';
import { SchemaScriptsFormComponent } from './scripts/schema-scripts-form.component'; import { SchemaScriptsFormComponent } from './scripts/schema-scripts-form.component';
@ -39,6 +40,7 @@ import { SchemaUIFormComponent } from './ui/schema-ui-form.component';
SchemaExportFormComponent, SchemaExportFormComponent,
SchemaFieldRulesFormComponent, SchemaFieldRulesFormComponent,
SchemaFieldsComponent, SchemaFieldsComponent,
SchemaIndexesComponent,
SchemaPreviewUrlsFormComponent, SchemaPreviewUrlsFormComponent,
SchemaScriptsFormComponent, SchemaScriptsFormComponent,
SchemaUIFormComponent, SchemaUIFormComponent,
@ -52,7 +54,7 @@ import { SchemaUIFormComponent } from './ui/schema-ui-form.component';
export class SchemaPageComponent implements OnInit { export class SchemaPageComponent implements OnInit {
private readonly subscriptions = new Subscriptions(); private readonly subscriptions = new Subscriptions();
public readonly exact = { exact: true }; public readonly hideIndexes = inject(UIOptions).value.hideIndexes;
public schema!: SchemaDto; public schema!: SchemaDto;
public schemaTab = this.route.queryParams.pipe(map(x => x['tab'] || 'fields')); public schemaTab = this.route.queryParams.pipe(map(x => x['tab'] || 'fields'));

8
frontend/src/app/features/settings/pages/clients/client-connect-form.component.html

@ -36,7 +36,7 @@
@case ("Start") { @case ("Start") {
<h3>{{ "clients.connectWizard.step1Title" | sqxTranslate }}</h3> <h3>{{ "clients.connectWizard.step1Title" | sqxTranslate }}</h3>
<sqx-form-hint> <sqx-form-hint>
<span [innerHTML]="'clients.connectWizard.postManDocs' | sqxTranslate | sqxMarkdownInline: true | sqxSafeHtml"></span> <span inline="true" [sqxMarkdown]="'clients.connectWizard.postManDocs' | sqxTranslate" trusted="true"></span>
</sqx-form-hint> </sqx-form-hint>
<div class="section"> <div class="section">
<div class="option" (click)="go('HTTP')"> <div class="option" (click)="go('HTTP')">
@ -108,7 +108,7 @@
<span class="badge rounded-pill bg-dark">1</span> <span class="badge rounded-pill bg-dark">1</span>
{{ "clients.connectWizard.cliStep1" | sqxTranslate }} {{ "clients.connectWizard.cliStep1" | sqxTranslate }}
</h5> </h5>
<div [innerHTML]="'clients.connectWizard.cliStep1Download' | sqxTranslate | sqxMarkdown: true | sqxSafeHtml"></div> <div [sqxMarkdown]="'clients.connectWizard.cliStep1Download' | sqxTranslate" trusted="true"></div>
<sqx-form-hint> <sqx-form-hint>
{{ "clients.connectWizard.cliStep1Hint" | sqxTranslate }} {{ "clients.connectWizard.cliStep1Hint" | sqxTranslate }}
</sqx-form-hint> </sqx-form-hint>
@ -116,7 +116,7 @@
<div class="section step"> <div class="section step">
<h5> <h5>
<span class="badge rounded-pill bg-dark">2</span> <span class="badge rounded-pill bg-dark">2</span>
<span [innerHTML]="'clients.connectWizard.cliStep2' | sqxTranslate | sqxMarkdownInline: true | sqxSafeHtml"></span> <span [sqxMarkdown]="'clients.connectWizard.cliStep2' | sqxTranslate" trusted="true"></span>
</h5> </h5>
</div> </div>
<div class="section step"> <div class="section step">
@ -150,7 +150,7 @@
<div class="sdk-header" [class.active]="sdk === availableSDK.value" (click)="select(availableSDK.value)"> <div class="sdk-header" [class.active]="sdk === availableSDK.value" (click)="select(availableSDK.value)">
<div <div
class="logo d-flex align-items-center justify-content-center" class="logo d-flex align-items-center justify-content-center"
[innerHTML]="availableSDK.value.logo | sqxSafeHtml"></div> [sqxMarkdown]="availableSDK.value.logo"></div>
{{ availableSDK.value.name }} {{ availableSDK.value.name }}
</div> </div>
} }

5
frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts

@ -7,7 +7,7 @@
import { AsyncPipe, KeyValuePipe } from '@angular/common'; import { AsyncPipe, KeyValuePipe } from '@angular/common';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AccessTokenDto, ApiUrlConfig, AppsState, ClientDto, ClientsService, ClientTourStated, CodeComponent, DialogService, ExternalLinkDirective, FormHintComponent, HelpService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, SafeHtmlPipe, SDKEntry, TooltipDirective, TranslatePipe } from '@app/shared'; import { AccessTokenDto, ApiUrlConfig, AppsState, ClientDto, ClientsService, ClientTourStated, CodeComponent, DialogService, ExternalLinkDirective, FormHintComponent, HelpService, MarkdownDirective, MessageBus, ModalDialogComponent, SDKEntry, TooltipDirective, TranslatePipe } from '@app/shared';
@Component({ @Component({
standalone: true, standalone: true,
@ -21,10 +21,7 @@ import { AccessTokenDto, ApiUrlConfig, AppsState, ClientDto, ClientsService, Cli
FormHintComponent, FormHintComponent,
KeyValuePipe, KeyValuePipe,
MarkdownDirective, MarkdownDirective,
MarkdownInlinePipe,
MarkdownPipe,
ModalDialogComponent, ModalDialogComponent,
SafeHtmlPipe,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,
], ],

4
frontend/src/app/features/settings/pages/clients/client.component.html

@ -44,7 +44,7 @@
<button <button
class="btn btn-outline-secondary" class="btn btn-outline-secondary"
attr.aria-label="{{ 'i18n:clients.copyClientId' | sqxTranslate }}" attr.aria-label="{{ 'clients.copyClientId' | sqxTranslate }}"
[sqxCopy]="clientId" [sqxCopy]="clientId"
title="i18n:clients.copyClientId" title="i18n:clients.copyClientId"
type="button"> type="button">
@ -64,7 +64,7 @@
<button <button
class="btn btn-outline-secondary" class="btn btn-outline-secondary"
attr.aria-label="{{ 'i18n:clients.copyClientSecret' | sqxTranslate }}" attr.aria-label="{{ 'clients.copyClientSecret' | sqxTranslate }}"
[sqxCopy]="inputSecret" [sqxCopy]="inputSecret"
title="i18n:clients.copyClientSecret" title="i18n:clients.copyClientSecret"
type="button"> type="button">

4
frontend/src/app/features/settings/pages/jobs/job.component.html

@ -35,7 +35,7 @@
<button <button
class="btn btn-outline-secondary btn-expand ms-1" class="btn btn-outline-secondary btn-expand ms-1"
[class.expanded]="expanded" [class.expanded]="isExpanded"
(click)="toggleExpanded()" (click)="toggleExpanded()"
type="button"> type="button">
<span class="hidden">{{ "common.settings" | sqxTranslate }}</span> <span class="hidden">{{ "common.settings" | sqxTranslate }}</span>
@ -56,7 +56,7 @@
</div> </div>
</div> </div>
@if (expanded) { @if (isExpanded) {
<div class="table-items-row-details"> <div class="table-items-row-details">
<div class="job-header"> <div class="job-header">
<h4>{{ "common.details" | sqxTranslate }}</h4> <h4>{{ "common.details" | sqxTranslate }}</h4>

8
frontend/src/app/features/settings/pages/jobs/job.component.ts

@ -7,7 +7,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ApiUrlConfig, CodeEditorComponent, ConfirmClickDirective, Duration, ExternalLinkDirective, FromNowPipe, JobDto, JobsState, KNumberPipe, StatusIconComponent, TooltipDirective, TranslatePipe, TypedSimpleChanges } from '@app/shared'; import { ApiUrlConfig, CodeEditorComponent, ConfirmClickDirective, Duration, ExternalLinkDirective, FromNowPipe, JobDto, JobsState, StatusIconComponent, TranslatePipe, TypedSimpleChanges } from '@app/shared';
@Component({ @Component({
standalone: true, standalone: true,
@ -21,9 +21,7 @@ import { ApiUrlConfig, CodeEditorComponent, ConfirmClickDirective, Duration, Ext
ExternalLinkDirective, ExternalLinkDirective,
FormsModule, FormsModule,
FromNowPipe, FromNowPipe,
KNumberPipe,
StatusIconComponent, StatusIconComponent,
TooltipDirective,
TranslatePipe, TranslatePipe,
], ],
}) })
@ -34,7 +32,7 @@ export class JobComponent {
public duration = ''; public duration = '';
public details = ''; public details = '';
public expanded = false; public isExpanded = false;
constructor( constructor(
public readonly apiUrl: ApiUrlConfig, public readonly apiUrl: ApiUrlConfig,
@ -65,6 +63,6 @@ export class JobComponent {
} }
public toggleExpanded() { public toggleExpanded() {
this.expanded = !this.expanded; this.isExpanded = !this.isExpanded;
} }
} }

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

@ -28,9 +28,9 @@
<div class="card card-body mb-4"> <div class="card card-body mb-4">
<h5 class="card-title">Squidex Referal Program</h5> <h5 class="card-title">Squidex Referal Program</h5>
<sqx-form-hint> <sqx-form-hint>
<div [innerHTML]="'plans.referralHint' | sqxTranslate: referral | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="'plans.referralHint' | sqxTranslate: referral"></div>
<div [innerHTML]="referral.condition | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="referral.condition"></div>
<div [innerHTML]="'plans.referralEarned' | sqxTranslate: referral | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="'plans.referralEarned' | sqxTranslate: referral"></div>
</sqx-form-hint> </sqx-form-hint>
</div> </div>
} }

5
frontend/src/app/features/settings/pages/plans/plans-page.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { ActivatedRoute, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { ApiUrlConfig, ExternalLinkDirective, FormHintComponent, LayoutComponent, ListViewComponent, MarkdownPipe, NotifoComponent, PlansState, SafeHtmlPipe, ShortcutDirective, SidebarMenuDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, UserNamePipe } from '@app/shared'; import { ApiUrlConfig, ExternalLinkDirective, FormHintComponent, LayoutComponent, ListViewComponent, MarkdownDirective, NotifoComponent, PlansState, ShortcutDirective, SidebarMenuDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe, UserNamePipe } from '@app/shared';
import { PlanComponent } from './plan.component'; import { PlanComponent } from './plan.component';
@Component({ @Component({
@ -22,13 +22,12 @@ import { PlanComponent } from './plan.component';
FormHintComponent, FormHintComponent,
LayoutComponent, LayoutComponent,
ListViewComponent, ListViewComponent,
MarkdownPipe, MarkdownDirective,
NotifoComponent, NotifoComponent,
PlanComponent, PlanComponent,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,
RouterOutlet, RouterOutlet,
SafeHtmlPipe,
ShortcutDirective, ShortcutDirective,
SidebarMenuDirective, SidebarMenuDirective,
TitleComponent, TitleComponent,

2
frontend/src/app/features/settings/pages/templates/template.component.html

@ -25,7 +25,7 @@
</div> </div>
<div class="table-items-row-details-tab"> <div class="table-items-row-details-tab">
@if (details | async; as loadedDetails) { @if (details | async; as loadedDetails) {
<div class="help" [innerHTML]="loadedDetails | sqxMarkdown | sqxSafeHtml"></div> <div class="help" [sqxMarkdown]="loadedDetails"></div>
} @else { } @else {
<sqx-loader></sqx-loader> <sqx-loader></sqx-loader>
} }

5
frontend/src/app/features/settings/pages/templates/template.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { map, Observable, shareReplay } from 'rxjs'; import { map, Observable, shareReplay } from 'rxjs';
import { AppsState, ClientsState, FormHintComponent, LoaderComponent, MarkdownPipe, SafeHtmlPipe, TemplateDetailsDto, TemplateDto, TemplatesService, TranslatePipe } from '@app/shared'; import { AppsState, ClientsState, FormHintComponent, LoaderComponent, MarkdownDirective, TemplateDetailsDto, TemplateDto, TemplatesService, TranslatePipe } from '@app/shared';
@Component({ @Component({
standalone: true, standalone: true,
@ -20,8 +20,7 @@ import { AppsState, ClientsState, FormHintComponent, LoaderComponent, MarkdownPi
AsyncPipe, AsyncPipe,
FormHintComponent, FormHintComponent,
LoaderComponent, LoaderComponent,
MarkdownPipe, MarkdownDirective,
SafeHtmlPipe,
TranslatePipe, TranslatePipe,
], ],
}) })

2
frontend/src/app/features/settings/pages/templates/templates-page.component.html

@ -16,7 +16,7 @@
<ng-container> <ng-container>
<sqx-list-view innerWidth="50rem" [isLoading]="templatesState.isLoading | async"> <sqx-list-view innerWidth="50rem" [isLoading]="templatesState.isLoading | async">
<sqx-form-alert light="true"> <sqx-form-alert light="true">
<div [innerHTML]="'templates.cliHint' | sqxTranslate | sqxMarkdownInline | sqxSafeHtml"></div> <div inline="true" [sqxMarkdown]="'templates.cliHint' | sqxTranslate"></div>
</sqx-form-alert> </sqx-form-alert>
@if ((templatesState.isLoaded | async) && (templatesState.templates | async); as templates) { @if ((templatesState.isLoaded | async) && (templatesState.templates | async); as templates) {

5
frontend/src/app/features/settings/pages/templates/templates-page.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { ClientsState, FormAlertComponent, LayoutComponent, ListViewComponent, MarkdownInlinePipe, SafeHtmlPipe, ShortcutDirective, SidebarMenuDirective, TemplatesState, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared'; import { ClientsState, FormAlertComponent, LayoutComponent, ListViewComponent, MarkdownDirective, ShortcutDirective, SidebarMenuDirective, TemplatesState, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared';
import { TemplateComponent } from './template.component'; import { TemplateComponent } from './template.component';
@Component({ @Component({
@ -21,11 +21,10 @@ import { TemplateComponent } from './template.component';
FormAlertComponent, FormAlertComponent,
LayoutComponent, LayoutComponent,
ListViewComponent, ListViewComponent,
MarkdownInlinePipe, MarkdownDirective,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,
RouterOutlet, RouterOutlet,
SafeHtmlPipe,
ShortcutDirective, ShortcutDirective,
SidebarMenuDirective, SidebarMenuDirective,
TemplateComponent, TemplateComponent,

4
frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.html

@ -5,9 +5,7 @@
@if (!isScrolled) { @if (!isScrolled) {
<div class="dashboard-summary" @fade> <div class="dashboard-summary" @fade>
<h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1> <h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1>
<div <div class="subtext" [sqxMarkdown]="'dashboard.welcomeTextTeam' | sqxTranslate: { team: team.name }"></div>
class="subtext"
[innerHTML]="'dashboard.welcomeTextTeam' | sqxTranslate: { team: team.name } | sqxMarkdown | sqxSafeHtml"></div>
</div> </div>
} }
<gridster #grid [options]="gridOptions"> <gridster #grid [options]="gridOptions">

5
frontend/src/app/features/teams/pages/dashboard/dashboard-page.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponent, GridType } from 'angular-gridster2'; import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponent, GridType } from 'angular-gridster2';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, fadeAnimation, IFrameCardComponent, LocalStoreService, MarkdownPipe, RandomCatCardComponent, RandomDogCardComponent, SafeHtmlPipe, Settings, StorageUsagePerDateDto, Subscriptions, SupportCardComponent, switchSafe, TeamsState, TitleComponent, TranslatePipe, UsagesService } from '@app/shared'; import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, defined, fadeAnimation, IFrameCardComponent, LocalStoreService, MarkdownDirective, RandomCatCardComponent, RandomDogCardComponent, Settings, StorageUsagePerDateDto, Subscriptions, SupportCardComponent, switchSafe, TeamsState, TitleComponent, TranslatePipe, UsagesService } from '@app/shared';
import { AppsCardComponent } from './cards/apps-card.component'; import { AppsCardComponent } from './cards/apps-card.component';
@Component({ @Component({
@ -33,10 +33,9 @@ import { AppsCardComponent } from './cards/apps-card.component';
GridsterComponent, GridsterComponent,
GridsterItemComponent, GridsterItemComponent,
IFrameCardComponent, IFrameCardComponent,
MarkdownPipe, MarkdownDirective,
RandomCatCardComponent, RandomCatCardComponent,
RandomDogCardComponent, RandomDogCardComponent,
SafeHtmlPipe,
SupportCardComponent, SupportCardComponent,
TitleComponent, TitleComponent,
TranslatePipe, TranslatePipe,

2
frontend/src/app/features/teams/pages/more/more-page.component.html

@ -72,7 +72,7 @@
titlePosition="left"> titlePosition="left">
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<a <a
class="panel-link" class="panel-link"
attr.aria-label="{{ 'common.help' | sqxTranslate }}" attr.aria-label="{{ 'common.help' | sqxTranslate }}"

6
frontend/src/app/features/teams/pages/plans/plans-page.component.html

@ -20,9 +20,9 @@
<div class="card card-body mb-4"> <div class="card card-body mb-4">
<h5 class="card-title">Squidex Referal Program</h5> <h5 class="card-title">Squidex Referal Program</h5>
<sqx-form-hint> <sqx-form-hint>
<div [innerHTML]="'plans.referralHint' | sqxTranslate: referral | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="'plans.referralHint' | sqxTranslate: referral"></div>
<div [innerHTML]="referral.condition | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="referral.condition"></div>
<div [innerHTML]="'plans.referralEarned' | sqxTranslate: referral | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="'plans.referralEarned' | sqxTranslate: referral"></div>
</sqx-form-hint> </sqx-form-hint>
</div> </div>
} }

5
frontend/src/app/features/teams/pages/plans/plans-page.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { ActivatedRoute, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { ApiUrlConfig, ExternalLinkDirective, FormHintComponent, LayoutComponent, ListViewComponent, MarkdownPipe, NotifoComponent, SafeHtmlPipe, ShortcutDirective, SidebarMenuDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared'; import { ApiUrlConfig, ExternalLinkDirective, FormHintComponent, LayoutComponent, ListViewComponent, MarkdownDirective, NotifoComponent, ShortcutDirective, SidebarMenuDirective, TitleComponent, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/shared';
import { TeamPlansState } from '../../internal'; import { TeamPlansState } from '../../internal';
import { PlanComponent } from './plan.component'; import { PlanComponent } from './plan.component';
@ -23,13 +23,12 @@ import { PlanComponent } from './plan.component';
FormHintComponent, FormHintComponent,
LayoutComponent, LayoutComponent,
ListViewComponent, ListViewComponent,
MarkdownPipe, MarkdownDirective,
NotifoComponent, NotifoComponent,
PlanComponent, PlanComponent,
RouterLink, RouterLink,
RouterLinkActive, RouterLinkActive,
RouterOutlet, RouterOutlet,
SafeHtmlPipe,
ShortcutDirective, ShortcutDirective,
SidebarMenuDirective, SidebarMenuDirective,
TitleComponent, TitleComponent,

7
frontend/src/app/framework/angular/forms/editors/autocomplete.component.scss

@ -7,13 +7,6 @@
.anchor { .anchor {
@include absolute(0, null, null, 0); @include absolute(0, null, null, 0);
background-color: red !important;
z-index: 1000;
opacity: .1;
& * {
background-color: red !important;
}
&.full-size { &.full-size {
@include absolute(0, 0, 0, 0); @include absolute(0, 0, 0, 0);

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

@ -4,7 +4,7 @@
<span class="form-alert-close" (click)="close()"> <span class="form-alert-close" (click)="close()">
<i class="icon-close"></i> <i class="icon-close"></i>
</span> </span>
<div [innerHTML]="error | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="error | sqxTranslate"></div>
</div> </div>
</div> </div>
} }

6
frontend/src/app/framework/angular/forms/form-error.component.ts

@ -8,9 +8,8 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ErrorDto, TypedSimpleChanges } from '@app/framework/internal'; import { ErrorDto, TypedSimpleChanges } from '@app/framework/internal';
import { MarkdownPipe } from '../pipes/markdown.pipe'; import { MarkdownDirective } from '../markdown.directive';
import { TranslatePipe } from '../pipes/translate.pipe'; import { TranslatePipe } from '../pipes/translate.pipe';
import { SafeHtmlPipe } from '../safe-html.pipe';
@Component({ @Component({
standalone: true, standalone: true,
@ -19,8 +18,7 @@ import { SafeHtmlPipe } from '../safe-html.pipe';
templateUrl: './form-error.component.html', templateUrl: './form-error.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
MarkdownPipe, MarkdownDirective,
SafeHtmlPipe,
TranslatePipe, TranslatePipe,
], ],
}) })

4
frontend/src/app/framework/angular/modals/dialog-renderer.component.html

@ -7,7 +7,7 @@
{{ request.title | sqxTranslate }} {{ request.title | sqxTranslate }}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<span [innerHTML]="request.text | sqxTranslate | sqxMarkdown | sqxSafeHtml"></span> <span [sqxMarkdown]="request.text | sqxTranslate"></span>
@if (request.canRemember) { @if (request.canRemember) {
<div class="form-check mt-4"> <div class="form-check mt-4">
<input class="form-check-input" id="remember" [(ngModel)]="request.remember" type="checkbox" /> <input class="form-check-input" id="remember" [(ngModel)]="request.remember" type="checkbox" />
@ -34,7 +34,7 @@
<div class="alert alert-dismissible alert-{{ notification.messageType }} shadowed" @fade (click)="close(notification)" role="alert"> <div class="alert alert-dismissible alert-{{ notification.messageType }} shadowed" @fade (click)="close(notification)" role="alert">
<button class="btn-close" data-dismiss="alert" (dialogClose)="close(notification)" type="button"></button> <button class="btn-close" data-dismiss="alert" (dialogClose)="close(notification)" type="button"></button>
<div class="shadow"></div> <div class="shadow"></div>
<div class="shadowed" [innerHTML]="notification.message | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <div class="shadowed" [sqxMarkdown]="notification.message | sqxTranslate"></div>
</div> </div>
} }
</div> </div>

6
frontend/src/app/framework/angular/modals/dialog-renderer.component.ts

@ -11,9 +11,8 @@ import { FormsModule } from '@angular/forms';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
import { DialogModel, DialogRequest, DialogService, fadeAnimation, Notification, StatefulComponent, Subscriptions, Tooltip } from '@app/framework/internal'; import { DialogModel, DialogRequest, DialogService, fadeAnimation, Notification, StatefulComponent, Subscriptions, Tooltip } from '@app/framework/internal';
import { FocusOnInitDirective } from '../forms/focus-on-init.directive'; import { FocusOnInitDirective } from '../forms/focus-on-init.directive';
import { MarkdownPipe } from '../pipes/markdown.pipe'; import { MarkdownDirective } from '../markdown.directive';
import { TranslatePipe } from '../pipes/translate.pipe'; import { TranslatePipe } from '../pipes/translate.pipe';
import { SafeHtmlPipe } from '../safe-html.pipe';
import { ModalDialogComponent } from './modal-dialog.component'; import { ModalDialogComponent } from './modal-dialog.component';
import { ModalPlacementDirective } from './modal-placement.directive'; import { ModalPlacementDirective } from './modal-placement.directive';
import { ModalDirective } from './modal.directive'; import { ModalDirective } from './modal.directive';
@ -42,11 +41,10 @@ interface State {
imports: [ imports: [
FocusOnInitDirective, FocusOnInitDirective,
FormsModule, FormsModule,
MarkdownPipe, MarkdownDirective,
ModalDialogComponent, ModalDialogComponent,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
SafeHtmlPipe,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,
], ],

6
frontend/src/app/framework/angular/modals/modal-dialog.component.html

@ -7,7 +7,11 @@
<ng-content select="[title]"></ng-content> <ng-content select="[title]"></ng-content>
</h4> </h4>
<ng-content select="[plainTitle]"></ng-content> <ng-content select="[plainTitle]"></ng-content>
<button class="btn btn-close" attr.aria-label="{{ 'common.close' | sqxTranslate }}" (click)="dialogClose.emit()" type="button"></button> <button
class="btn btn-close"
attr.aria-label="{{ 'common.close' | sqxTranslate }}"
(click)="dialogClose.emit()"
type="button"></button>
</div> </div>
} }

4
frontend/src/app/framework/angular/modals/tour-template.component.html

@ -19,9 +19,9 @@
</div> </div>
<div class="col"> <div class="col">
<h5 [innerHTML]="currentStep.title | sqxTranslate | sqxMarkdownInline | sqxSafeHtml"></h5> <h5 inline="true" [sqxMarkdown]="currentStep.title | sqxTranslate"></h5>
<div [innerHTML]="currentStep.content | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="currentStep.content | sqxTranslate"></div>
</div> </div>
<div class="col-auto"> <div class="col-auto">

7
frontend/src/app/framework/angular/modals/tour-template.component.ts

@ -8,9 +8,8 @@
import { AfterContentInit, ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { AfterContentInit, ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { fadeAnimation, StatefulComponent } from '@app/framework/internal'; import { fadeAnimation, StatefulComponent } from '@app/framework/internal';
import { MarkdownInlinePipe, MarkdownPipe } from '../pipes/markdown.pipe'; import { MarkdownDirective } from '../markdown.directive';
import { TranslatePipe } from '../pipes/translate.pipe'; import { TranslatePipe } from '../pipes/translate.pipe';
import { SafeHtmlPipe } from '../safe-html.pipe';
import { ModalPlacementDirective } from './modal-placement.directive'; import { ModalPlacementDirective } from './modal-placement.directive';
import { ModalDirective } from './modal.directive'; import { ModalDirective } from './modal.directive';
import { StepDefinition, TourService } from './tour.service'; import { StepDefinition, TourService } from './tour.service';
@ -25,11 +24,9 @@ import { StepDefinition, TourService } from './tour.service';
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
MarkdownInlinePipe, MarkdownDirective,
MarkdownPipe,
ModalDirective, ModalDirective,
ModalPlacementDirective, ModalPlacementDirective,
SafeHtmlPipe,
TranslatePipe, TranslatePipe,
], ],
}) })

1
frontend/src/app/framework/index.ts

@ -73,7 +73,6 @@ export * from './angular/pipes/colors.pipes';
export * from './angular/pipes/date-time.pipes'; export * from './angular/pipes/date-time.pipes';
export * from './angular/pipes/highlight.pipe'; export * from './angular/pipes/highlight.pipe';
export * from './angular/pipes/keys.pipe'; export * from './angular/pipes/keys.pipe';
export * from './angular/pipes/markdown.pipe';
export * from './angular/pipes/name.pipe'; export * from './angular/pipes/name.pipe';
export * from './angular/pipes/numbers.pipes'; export * from './angular/pipes/numbers.pipes';
export * from './angular/pipes/strings.pipes'; export * from './angular/pipes/strings.pipes';

3
frontend/src/app/shared/components/chat-dialog.component.ts

@ -9,7 +9,7 @@
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HTTP, MathHelper, ModalDialogComponent, ResizedDirective, TooltipDirective, TranslatePipe } from '@app/framework'; import { HTTP, MathHelper, ModalDialogComponent, TooltipDirective, TranslatePipe } from '@app/framework';
import { AppsState, AuthService, ChatEventDto, StatefulComponent, TranslationsService } from '@app/shared/internal'; import { AppsState, AuthService, ChatEventDto, StatefulComponent, TranslationsService } from '@app/shared/internal';
import { ChatItemComponent } from './chat-item.component'; import { ChatItemComponent } from './chat-item.component';
@ -33,7 +33,6 @@ interface State {
ChatItemComponent, ChatItemComponent,
FormsModule, FormsModule,
ModalDialogComponent, ModalDialogComponent,
ResizedDirective,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,
], ],

6
frontend/src/app/shared/components/chat-item.component.html

@ -54,7 +54,11 @@
</span> </span>
} }
@if (!snapshot.isRunning && !isFirst && type === "Bot") { @if (!snapshot.isRunning && !isFirst && type === "Bot") {
<button class="btn btn-secondary btn-sm btn-text" (click)="selectContent()" [disabled]="snapshot.isCopying" type="button"> <button
class="btn btn-secondary btn-sm btn-text"
(click)="selectContent()"
[disabled]="snapshot.isCopying"
type="button">
{{ "chat.use" | sqxTranslate }} {{ "chat.use" | sqxTranslate }}
@if (snapshot.isCopying) { @if (snapshot.isCopying) {
<sqx-loader></sqx-loader> <sqx-loader></sqx-loader>

6
frontend/src/app/shared/components/comments/comment.component.html

@ -26,7 +26,7 @@
{{ commentItem.comment.user | sqxUserNameRef: null }} {{ commentItem.comment.user | sqxUserNameRef: null }}
</div> </div>
</div> </div>
<div [innerHTML]="commentItem.comment.text | sqxMarkdown | sqxSafeHtml"></div> <div [sqxMarkdown]="commentItem.comment.text"></div>
<div class="comment-created text-muted"> <div class="comment-created text-muted">
@if (canFollow && commentItem.comment.url) { @if (canFollow && commentItem.comment.url) {
<a [routerLink]="commentItem.comment.url">{{ "comments.follow" | sqxTranslate }}</a> <a [routerLink]="commentItem.comment.url">{{ "comments.follow" | sqxTranslate }}</a>
@ -69,8 +69,8 @@
autoFocus="true" autoFocus="true"
autoSelectFirst="false" autoSelectFirst="false"
(editorKeyPress)="onKeyPressUpdate($event)" (editorKeyPress)="onKeyPressUpdate($event)"
[itemsSource]="contributors"
formControlName="text" formControlName="text"
[itemsSource]="contributors"
startCharacter="@" startCharacter="@"
textArea="true"> textArea="true">
</sqx-autocomplete> </sqx-autocomplete>
@ -102,8 +102,8 @@
autoFocus="true" autoFocus="true"
autoSelectFirst="false" autoSelectFirst="false"
(editorKeyPress)="onKeyPressReply($event)" (editorKeyPress)="onKeyPressReply($event)"
[itemsSource]="contributors"
formControlName="text" formControlName="text"
[itemsSource]="contributors"
sqxFocusOnInit sqxFocusOnInit
startCharacter="@" startCharacter="@"
textArea="true"> textArea="true">

5
frontend/src/app/shared/components/comments/comment.component.ts

@ -9,7 +9,7 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { AutocompleteComponent, AutocompleteSource, bounceAnimation, ConfirmClickDirective, FromNowPipe, MarkdownPipe, NOOP_DATASOURCE, SafeHtmlPipe, ScrollActiveDirective, TooltipDirective, TranslatePipe } from '@app/framework'; import { AutocompleteComponent, AutocompleteSource, bounceAnimation, ConfirmClickDirective, FromNowPipe, MarkdownDirective, NOOP_DATASOURCE, ScrollActiveDirective, TooltipDirective, TranslatePipe } from '@app/framework';
import { CommentItem, CommentsState, DialogService, Keys, StatefulComponent, UpsertCommentForm } from '@app/shared/internal'; import { CommentItem, CommentsState, DialogService, Keys, StatefulComponent, UpsertCommentForm } from '@app/shared/internal';
import { UserNameRefPipe, UserPictureRefPipe } from '../pipes'; import { UserNameRefPipe, UserPictureRefPipe } from '../pipes';
@ -30,10 +30,9 @@ interface State {
AutocompleteComponent, AutocompleteComponent,
ConfirmClickDirective, ConfirmClickDirective,
FromNowPipe, FromNowPipe,
MarkdownPipe, MarkdownDirective,
ReactiveFormsModule, ReactiveFormsModule,
RouterLink, RouterLink,
SafeHtmlPipe,
ScrollActiveDirective, ScrollActiveDirective,
TooltipDirective, TooltipDirective,
TranslatePipe, TranslatePipe,

2
frontend/src/app/shared/components/search/search-form.component.html

@ -94,7 +94,7 @@
[query]="query" [query]="query"
(queryChange)="changeQuery($event)"></sqx-query> (queryChange)="changeQuery($event)"></sqx-query>
<div class="link" [innerHTML]="'search.help' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div> <div class="link" [sqxMarkdown]="'search.help' | sqxTranslate"></div>
</div> </div>
} }
} @else if (queries && queriesTypes) { } @else if (queries && queriesTypes) {

5
frontend/src/app/shared/components/search/search-form.component.ts

@ -9,7 +9,7 @@ import { AsyncPipe } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ControlErrorsComponent, FocusOnInitDirective, MarkdownPipe, ModalDialogComponent, ModalDirective, SafeHtmlPipe, ShortcutComponent, ShortcutDirective, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/framework'; import { ControlErrorsComponent, FocusOnInitDirective, MarkdownDirective, ModalDialogComponent, ModalDirective, ShortcutComponent, ShortcutDirective, TooltipDirective, TourStepDirective, TranslatePipe } from '@app/framework';
import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, TypedSimpleChanges } from '@app/shared/internal'; import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, TypedSimpleChanges } from '@app/shared/internal';
import { TourHintDirective } from '../tour-hint.directive'; import { TourHintDirective } from '../tour-hint.directive';
import { QueryComponent } from './queries/query.component'; import { QueryComponent } from './queries/query.component';
@ -26,12 +26,11 @@ import { SavedQueriesComponent } from './shared-queries.component';
ControlErrorsComponent, ControlErrorsComponent,
FocusOnInitDirective, FocusOnInitDirective,
FormsModule, FormsModule,
MarkdownPipe, MarkdownDirective,
ModalDialogComponent, ModalDialogComponent,
ModalDirective, ModalDirective,
QueryComponent, QueryComponent,
ReactiveFormsModule, ReactiveFormsModule,
SafeHtmlPipe,
SavedQueriesComponent, SavedQueriesComponent,
ShortcutComponent, ShortcutComponent,
ShortcutDirective, ShortcutDirective,

7
frontend/src/app/shared/internal.ts

@ -19,6 +19,7 @@ export * from './services/contents.service';
export * from './services/contributors.service'; export * from './services/contributors.service';
export * from './services/help.service'; export * from './services/help.service';
export * from './services/history.service'; export * from './services/history.service';
export * from './services/indexes.service';
export * from './services/jobs.service'; export * from './services/jobs.service';
export * from './services/languages.service'; export * from './services/languages.service';
export * from './services/news.service'; export * from './services/news.service';
@ -57,6 +58,8 @@ export * from './state/contents.forms';
export * from './state/contents.state'; export * from './state/contents.state';
export * from './state/contributors.forms'; export * from './state/contributors.forms';
export * from './state/contributors.state'; export * from './state/contributors.state';
export * from './state/indexes.forms';
export * from './state/indexes.state';
export * from './state/jobs.state'; export * from './state/jobs.state';
export * from './state/languages.forms'; export * from './state/languages.forms';
export * from './state/languages.state'; export * from './state/languages.state';
@ -74,11 +77,11 @@ export * from './state/schemas.forms';
export * from './state/schemas.state'; export * from './state/schemas.state';
export * from './state/settings'; export * from './state/settings';
export * from './state/table-settings'; export * from './state/table-settings';
export * from './state/tour.state';
export * from './state/tour.tasks';
export * from './state/teams.forms'; export * from './state/teams.forms';
export * from './state/teams.state'; export * from './state/teams.state';
export * from './state/templates.state'; export * from './state/templates.state';
export * from './state/tour.state';
export * from './state/tour.tasks';
export * from './state/ui-languages'; export * from './state/ui-languages';
export * from './state/ui.state'; export * from './state/ui.state';
export * from './state/workflows.forms'; export * from './state/workflows.forms';

116
frontend/src/app/shared/services/indexes.service.spec.ts

@ -0,0 +1,116 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, IndexDto, IndexesDto, IndexesService, Resource, ResourceLinks } from '@app/shared/internal';
describe('IndexesService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
IndexesService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
],
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get indexes',
inject([IndexesService, HttpTestingController], (indexesService: IndexesService, httpMock: HttpTestingController) => {
let indexes: IndexesDto;
indexesService.getIndexes('my-app', 'my-schema').subscribe(result => {
indexes = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/indexes');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
items: [
indexResponse(12),
indexResponse(13),
],
});
expect(indexes!).toEqual({
items: [
createIndex(12),
createIndex(13),
],
canCreate: false,
});
}));
it('should make post request to create index',
inject([IndexesService, HttpTestingController], (indexesService: IndexesService, httpMock: HttpTestingController) => {
const request = { fields: [] };
indexesService.postIndex('my-app', 'my-schema', request).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/indexes');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make delete request to remove index',
inject([IndexesService, HttpTestingController], (indexesService: IndexesService, httpMock: HttpTestingController) => {
const resource: Resource = {
_links: {
delete: { method: 'DELETE', href: '/api/apps/my-app/schemas/my-schema/indexes/my-index' },
},
};
indexesService.deleteIndex('my-app', resource).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/indexes/my-index');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
function indexResponse(id: number) {
return {
name: `index${id}`,
fields: [
{ name: `field${id}_asc`, order: 'Ascending' },
{ name: `field${id}_desc`, order: 'Descending' },
],
_links: {
download: { method: 'GET', href: '/api/indexes/1' },
},
};
}
});
export function createIndex(id: number) {
const links: ResourceLinks = {
download: { method: 'GET', href: '/api/indexes/1' },
};
return new IndexDto(links,
`index${id}`,
[
{ name: `field${id}_asc`, order: 'Ascending' },
{ name: `field${id}_desc`, order: 'Descending' },
]);
}

93
frontend/src/app/shared/services/indexes.service.ts

@ -0,0 +1,93 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { ApiUrlConfig, hasAnyLink, pretifyError, Resource, ResourceLinks } from '@app/framework';
export type IndexField = { name: string; order: 'Ascending' | 'Descending' };
export class IndexDto {
public readonly _links: ResourceLinks;
public readonly canDelete: boolean;
constructor(links: ResourceLinks,
public readonly name: string,
public readonly fields: ReadonlyArray<IndexField>,
) {
this._links = links;
this.canDelete = hasAnyLink(links, 'delete');
}
}
export type IndexesDto = Readonly<{
// The indexes.
items: ReadonlyArray<IndexDto>;
// The if the user can create a new index.
canCreate?: boolean;
}>;
export type CreateIndexDto = Readonly<{
// The index fields.
fields: IndexField[];
}>;
@Injectable({
providedIn: 'root',
})
export class IndexesService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig,
) {
}
public getIndexes(appName: string, schemaName: string): Observable<IndexesDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/indexes`);
return this.http.get(url).pipe(
map(body => {
return parseIndexes(body as any);
}),
pretifyError('i18n:schemas.indexes.loadFailed'));
}
public postIndex(appName: string, schemaName: string, dto: CreateIndexDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/indexes`);
return this.http.post(url, dto).pipe(
pretifyError('i18n:schemas.indexes.createFailed'));
}
public deleteIndex(appName: string, resource: Resource): Observable<any> {
const link = resource._links['delete'];
const url = this.apiUrl.buildUrl(link.href);
return this.http.request(link.method, url).pipe(
pretifyError('i18n:schemas.indexes.deleteFailed'));
}
}
function parseIndexes(response: { items: any[] } & Resource): IndexesDto {
const { items: list, _links } = response;
const items = list.map(parseIndex);
const canCreate = hasAnyLink(_links, 'create');
return { items, canCreate };
}
function parseIndex(response: any) {
return new IndexDto(response._links,
response.name,
response.fields);
}

4
frontend/src/app/shared/services/jobs.service.spec.ts

@ -91,7 +91,7 @@ describe('JobsService', () => {
])); ]));
})); }));
it('should return null if get restore return 404', it('should return null if get restore returns 404',
inject([JobsService, HttpTestingController], (jobsService: JobsService, httpMock: HttpTestingController) => { inject([JobsService, HttpTestingController], (jobsService: JobsService, httpMock: HttpTestingController) => {
let restore: RestoreDto | null; let restore: RestoreDto | null;
@ -109,7 +109,7 @@ describe('JobsService', () => {
expect(restore!).toBeNull(); expect(restore!).toBeNull();
})); }));
it('should throw error if get restore return non 404', it('should throw error if get restore returns non 404',
inject([JobsService, HttpTestingController], (jobsService: JobsService, httpMock: HttpTestingController) => { inject([JobsService, HttpTestingController], (jobsService: JobsService, httpMock: HttpTestingController) => {
let error: any; let error: any;

36
frontend/src/app/shared/state/indexes.forms.ts

@ -0,0 +1,36 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { UntypedFormControl, Validators } from '@angular/forms';
import { ExtendedFormGroup, Form, TemplatedFormArray } from '@app/framework';
import { IndexDto, IndexField } from '@app/shared/internal';
export class CreateIndexForm extends Form<TemplatedFormArray, IndexField[], IndexDto> {
public get controls(): ReadonlyArray<ExtendedFormGroup> {
return this.form.controls as any;
}
constructor() {
super(new TemplatedFormArray(FieldTemplate.INSTANCE));
}
}
class FieldTemplate {
public static readonly INSTANCE = new FieldTemplate();
public createControl() {
return new ExtendedFormGroup({
name: new UntypedFormControl('',
Validators.required,
),
order: new UntypedFormControl('',
Validators.required,
),
});
}
}

132
frontend/src/app/shared/state/indexes.state.spec.ts

@ -0,0 +1,132 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { of, onErrorResumeNextWith, throwError } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { CreateIndexDto, DialogService, IndexesService, IndexesState, SchemasState } from '@app/shared/internal';
import { createIndex } from '../services/indexes.service.spec';
import { TestValues } from './_test-helpers';
describe('IndexesState', () => {
const {
app,
appsState,
} = TestValues;
const index1 = createIndex(12);
const index2 = createIndex(13);
const schema = 'my-schema';
let dialogs: IMock<DialogService>;
let schemasState: IMock<SchemasState>;
let indexesService: IMock<IndexesService>;
let indexesState: IndexesState;
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
schemasState = Mock.ofType<SchemasState>();
schemasState.setup(x => x.schemaName).returns(() => schema);
indexesService = Mock.ofType<IndexesService>();
indexesState = new IndexesState(appsState.object, schemasState.object, indexesService.object, dialogs.object);
});
afterEach(() => {
indexesService.verifyAll();
});
describe('Loading', () => {
it('should load indexes', () => {
indexesService.setup(x => x.getIndexes(app, schema))
.returns(() => of({ items: [index1, index2] } as any)).verifiable();
indexesState.load().subscribe();
expect(indexesState.snapshot.indexes).toEqual([index1, index2]);
expect(indexesState.snapshot.isLoaded).toBeTruthy();
expect(indexesState.snapshot.isLoading).toBeFalsy();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.never());
});
it('should reset loading state if loading failed', () => {
indexesService.setup(x => x.getIndexes(app, schema))
.returns(() => throwError(() => 'Service Error'));
indexesState.load().pipe(onErrorResumeNextWith()).subscribe();
expect(indexesState.snapshot.isLoading).toBeFalsy();
});
it('should show notification on load if reload is true', () => {
indexesService.setup(x => x.getIndexes(app, schema))
.returns(() => of({ items: [index1, index2] } as any)).verifiable();
indexesState.load(true, false).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should show notification on load error if silent is false', () => {
indexesService.setup(x => x.getIndexes(app, schema))
.returns(() => throwError(() => 'Service Error'));
indexesState.load(true, false).pipe(onErrorResumeNextWith()).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyError(It.isAny()), Times.once());
});
it('should not show notification on load error if silent is true', () => {
indexesService.setup(x => x.getIndexes(app, schema))
.returns(() => throwError(() => 'Service Error'));
indexesState.load(true, true).pipe(onErrorResumeNextWith()).subscribe();
expect().nothing();
dialogs.verify(x => x.notifyError(It.isAny()), Times.never());
});
});
describe('Updates', () => {
beforeEach(() => {
indexesService.setup(x => x.getIndexes(app, schema))
.returns(() => of({ items: [index1, index2] } as any)).verifiable();
indexesState.load().subscribe();
});
it('should not add index to snapshot', () => {
const request: CreateIndexDto = { fields: [{ name: 'field1', order: 'Ascending' }] };
indexesService.setup(x => x.postIndex(app, schema, request))
.returns(() => of({})).verifiable();
indexesState.create(request).subscribe();
expect(indexesState.snapshot.indexes.length).toBe(2);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should not remove index from snapshot', () => {
indexesService.setup(x => x.deleteIndex(app, index1))
.returns(() => of({})).verifiable();
indexesState.delete(index1).subscribe();
expect(indexesState.snapshot.indexes.length).toBe(2);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
});
});

114
frontend/src/app/shared/state/indexes.state.ts

@ -0,0 +1,114 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
import { debug, DialogService, LoadingState, shareSubscribed, State } from '@app/framework';
import { CreateIndexDto, IndexDto, IndexesService } from '../services/indexes.service';
import { AppsState } from './apps.state';
import { SchemasState } from './schemas.state';
interface Snapshot extends LoadingState {
// The current indexes.
indexes: ReadonlyArray<IndexDto>;
// Indicates if the user can add an index.
canCreate?: boolean;
}
@Injectable({
providedIn: 'root',
})
export class IndexesState extends State<Snapshot> {
public indexes =
this.project(x => x.indexes);
public isLoaded =
this.project(x => x.isLoaded === true);
public isLoading =
this.project(x => x.isLoading === true);
public canCreate =
this.project(x => x.canCreate === true);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
public get schemaId() {
return this.schemasState.schemaId;
}
public get schemaName() {
return this.schemasState.schemaName;
}
constructor(
private readonly appsState: AppsState,
private readonly schemasState: SchemasState,
private readonly indexesService: IndexesService,
private readonly dialogs: DialogService,
) {
super({ indexes: [] });
debug(this, 'indexes');
}
public load(isReload = false, silent = false): Observable<any> {
if (isReload && !silent) {
this.resetState('Loading Initial');
}
return this.loadInternal(isReload, silent);
}
private loadInternal(isReload: boolean, silent: boolean): Observable<any> {
this.next({ isLoading: true }, 'Loading Success');
return this.indexesService.getIndexes(this.appName, this.schemasState.schemaName).pipe(
tap(payload => {
if (isReload && !silent) {
this.dialogs.notifyInfo('i18n:schemas.indexes.reloaded');
}
const { canCreate, items: indexes } = payload;
this.next({
canCreate,
isLoaded: true,
isLoading: false,
indexes,
}, 'Loading Success / Updated');
}),
finalize(() => {
this.next({ isLoading: false }, 'Loading Done');
}),
shareSubscribed(this.dialogs, { silent }));
}
public create(request: CreateIndexDto): Observable<any> {
return this.indexesService.postIndex(this.appName, this.schemaName, request).pipe(
tap(() => {
this.dialogs.notifyInfo('i18n:schemas.indexes.created');
}),
shareSubscribed(this.dialogs));
}
public delete(index: IndexDto): Observable<any> {
return this.indexesService.deleteIndex(this.appName, index).pipe(
tap(() => {
this.dialogs.notifyInfo('i18n:schemas.indexes.deleted');
}),
shareSubscribed(this.dialogs));
}
}

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

Loading…
Cancel
Save