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.viewReferences": "View all content items referencing this asset.",
"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.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.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Add Client",
"clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client",
@ -322,6 +317,7 @@
"common.openAPI": "Open API",
"common.options": "Options",
"common.or": "or",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References",
"common.refresh": "Refresh",
"common.remember": "Don't ask again",
"common.remove": "remove",
"common.rename": "Rename",
"common.renameTag": "Rename Tag",
"common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Separator for editing UI.",
"schemas.hideFieldFailed": "Failed to hide field. Please reload.",
"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.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.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Field Rules",
"schemas.rules.when": "when",
"schemas.saved": "Schema saved successfully.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Save and close",
"schemas.saveFieldAndNew": "Save and add field",
"schemas.schemaHintsHint": "Describe this schema for documentation and user interfaces.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.",
"schemas.tabFields": "Fields",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "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.viewReferences": "Afficher tous les éléments de contenu faisant référence à cet élément.",
"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.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.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Ajouter un client",
"clients.add.description": "Ajoutez un client pour permettre à d'autres applications d'accéder à votre contenu.",
"clients.add.title": "Ajouter un nouveau client",
@ -322,6 +317,7 @@
"common.openAPI": "Ouvrir l'API",
"common.options": "Options",
"common.or": "ou",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} de {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} Du total?",
"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.refresh": "Rafraîchir",
"common.remember": "Ne demande plus",
"common.remove": "remove",
"common.rename": "Renommer",
"common.renameTag": "Renommer la balise",
"common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Séparateur pour l'édition de l'interface utilisateur.",
"schemas.hideFieldFailed": "Impossible de masquer le champ. Veuillez recharger.",
"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.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.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Règles de champ",
"schemas.rules.when": "quand",
"schemas.saved": "Schéma enregistré avec succès.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Sauver et fermer",
"schemas.saveFieldAndNew": "Enregistrer et ajouter un champ",
"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.synchronizeFailed": "Échec de la synchronisation du schéma. Veuillez recharger.",
"schemas.tabFields": "Des champs",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Créé",
"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.viewReferences": "View all content items referencing this asset.",
"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.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.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Aggiungi un Client",
"clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client",
@ -322,6 +317,7 @@
"common.openAPI": "Open API",
"common.options": "Options",
"common.or": "o",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References",
"common.refresh": "Aggiorna",
"common.remember": "Ricorda la mia decisione",
"common.remove": "remove",
"common.rename": "Rinomina",
"common.renameTag": "Rename Tag",
"common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Separatore per il pannello delle modifiche della UI.",
"schemas.hideFieldFailed": "Non è stato possibile nascondere il campo. Per favore ricarica.",
"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.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.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Regole per il campo",
"schemas.rules.when": "quando",
"schemas.saved": "Lo Schema è stato salvato con successo.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Salva e chiudi",
"schemas.saveFieldAndNew": "Salva e aggiungi un campo",
"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.synchronizeFailed": "Non è stato possibile sincronizzare lo schema. Per favore ricarica.",
"schemas.tabFields": "Campi",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Creato",
"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.viewReferences": "View all content items referencing this asset.",
"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.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.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Client toevoegen",
"clients.add.description": "Voeg een client toe om andere applicaties toegang te geven tot uw inhoud.",
"clients.add.title": "Client toevoegen",
@ -322,6 +317,7 @@
"common.openAPI": "Open API",
"common.options": "Options",
"common.or": "of",
"common.order": "Order",
"common.pagerInfo": "{itemFirst} - {itemLast} van {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References",
"common.refresh": "Vernieuwen",
"common.remember": "Onthoud mijn keuze",
"common.remove": "remove",
"common.rename": "Hernoemen",
"common.renameTag": "Hernoem Tag",
"common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Scheidingsteken voor het bewerken van gebruikersinterface.",
"schemas.hideFieldFailed": "Kan veld niet verbergen. Laad opnieuw.",
"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.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.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Veldregels",
"schemas.rules.when": "Wanneer",
"schemas.saved": "Schema succesvol opgeslagen.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Opslaan en sluiten",
"schemas.saveFieldAndNew": "Opslaan en veld toevoegen",
"schemas.schemaHintsHint": "Beschrijf dit schema voor documentatie en gebruikersinterfaces.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schema is succesvol gesynchroniseerd.",
"schemas.synchronizeFailed": "Synchroniseren van schema is mislukt. Laad opnieuw.",
"schemas.tabFields": "Velden",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Gemaakt",
"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.viewReferences": "View all content items referencing this asset.",
"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.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.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Adicionar Cliente",
"clients.add.description": "Adicione um cliente para dar a outras aplicações acesso ao seu conteúdo.",
"clients.add.title": "Adicionar um novo Cliente",
@ -322,6 +317,7 @@
"common.openAPI": "Open API",
"common.options": "Options",
"common.or": "ou",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} de {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} do total?",
"common.pagerReload": "Clique para recarregar a vista e obter o número total de itens",
@ -353,6 +349,7 @@
"common.references": "References",
"common.refresh": "Refrescar",
"common.remember": "Não pergunte de novo.",
"common.remove": "remove",
"common.rename": "Renomear",
"common.renameTag": "Renomear Etiqueta",
"common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Separador para edição de UI.",
"schemas.hideFieldFailed": "Falhou em esconder o campo. Por favor, recarregue.",
"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.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.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Regras de Campo",
"schemas.rules.when": "quando",
"schemas.saved": "Esqquema salvo com sucesso.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Salvar e fechar",
"schemas.saveFieldAndNew": "Guardar e adicionar campo",
"schemas.schemaHintsHint": "Descreva este esquema para documentação e interfaces de utilizador.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Esquema sincronizado com sucesso.",
"schemas.synchronizeFailed": "Falhou em sincronizar o esquema. Por favor, recarregue.",
"schemas.tabFields": "Campos",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "Criado",
"schemas.tableHeaders.created_title": "Created",

26
backend/i18n/frontend_zh.json

@ -136,16 +136,11 @@
"assets.uploadHint": "在现有项目上放置文件以使用更新版本替换资源。",
"assets.viewReferences": "View all content items referencing this asset.",
"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.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.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "添加客户端",
"clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client",
@ -322,6 +317,7 @@
"common.openAPI": "Open API",
"common.options": "Options",
"common.or": "或",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} 的 {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References",
"common.refresh": "刷新",
"common.remember": "不要再问了",
"common.remove": "remove",
"common.rename": "重命名",
"common.renameTag": "Rename Tag",
"common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "编辑 UI 的分隔符。",
"schemas.hideFieldFailed": "隐藏字段失败。请重新加载。",
"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.listFieldsEmpty": "将字段拖放到此处或重新排序以显示内容列表中的字段。当未定义列表字段时,使用第一个字段。",
"schemas.loadFailed": "加载Schemas失败。请重新加载。",
@ -986,6 +1000,7 @@
"schemas.rules.title": "字段规则",
"schemas.rules.when": "何时",
"schemas.saved": "Schema saved successfully.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "保存并关闭",
"schemas.saveFieldAndNew": "保存并添加字段",
"schemas.schemaHintsHint": "为文档和用户界面描述这个Schemas。",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "同步Schemas失败。请重新加载。",
"schemas.tabFields": "字段",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json",
"schemas.tableHeaders.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.lockedOutTitle": "Account locked",
"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.emailBusinessPlaceholder": "Enter Business Email",
"users.login.emailPlaceholder": "Enter Email",
"users.login.error": "Email or password not correct",
"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.separator": "OR",
"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.viewReferences": "View all content items referencing this asset.",
"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.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.prompt": "Describe the content you want to generate",
"chat.title": "Chat Bot",
"chat.use": "Use",
"chatBot.questionFailed": "",
"clients.add": "Add Client",
"clients.add.description": "Add a client to give other applications access to your content.",
"clients.add.title": "Add a new Client",
@ -322,6 +317,7 @@
"common.openAPI": "Open API",
"common.options": "Options",
"common.or": "or",
"common.order": "Order",
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
@ -353,6 +349,7 @@
"common.references": "References",
"common.refresh": "Refresh",
"common.remember": "Don't ask again",
"common.remove": "remove",
"common.rename": "Rename",
"common.renameTag": "Rename Tag",
"common.reply": "Reply",
@ -957,6 +954,23 @@
"schemas.fieldTypes.ui.description": "Separator for editing UI.",
"schemas.hideFieldFailed": "Failed to hide field. Please reload.",
"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.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.",
@ -986,6 +1000,7 @@
"schemas.rules.title": "Field Rules",
"schemas.rules.when": "when",
"schemas.saved": "Schema saved successfully.",
"schemas.saveField": "Save Field",
"schemas.saveFieldAndClose": "Save and close",
"schemas.saveFieldAndNew": "Save and add field",
"schemas.schemaHintsHint": "Describe this schema for documentation and user interfaces.",
@ -998,6 +1013,7 @@
"schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.",
"schemas.tabFields": "Fields",
"schemas.tabIndexes": "Indexes",
"schemas.tabJson": "Json",
"schemas.tableHeaders.created": "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("'(?<Key>[^\']+)' \\| sqxTranslate");
AddTranslations("\"(?<Key>[^\"]+)\"[\\s]*\\|[\\s]*sqxTranslate");
AddTranslations("\'(?<Key>[^\']+)\'[\\s]*\\|[\\s]*sqxTranslate");
return translations;
}

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

@ -10,7 +10,7 @@ using Squidex.Translator.State;
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);
@ -22,7 +22,7 @@ public class TranslateBackend(DirectoryInfo folder, TranslationService service)
var isReplaced = false;
content = Regex.Replace(content, "\"[^\"]*\"", match =>
content = VariableRegex().Replace(content, match =>
{
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
{
// Keep our original indentation.
textNode.Text = originalPrefix + $"{{{{ '{key}' | sqxTranslate }}}}" + originalSuffix;
textNode.Text = originalPrefix + $"{{{{ \"{key}\" | sqxTranslate }}}}" + originalSuffix;
isReplaced = true;
}
@ -140,7 +140,7 @@ public partial class TranslateTemplates(DirectoryInfo folder, TranslationService
{
if (attribute.Name.Contains('[', StringComparison.Ordinal))
{
node.SetAttributeValue(attribute.Name, $"{{{{ '{key}' | sqxTranslate }}}}");
node.SetAttributeValue(attribute.Name, $"{{{{ \"{key}\" | sqxTranslate }}}}");
}
else
{

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

@ -28,7 +28,7 @@ public partial class MongoContentRepository(
IMongoDatabase database,
IAppProvider appProvider,
string shardKey,
IOptions<ContentOptions> options,
IOptions<ContentsOptions> options,
ILogger<MongoContentRepository> log)
: MongoBase<MongoContentEntity>, IContentRepository, IInitializable
{
@ -38,7 +38,7 @@ public partial class MongoContentRepository(
private readonly MongoContentCollection collectionPublished =
new MongoContentCollection($"States_Contents_Published3{shardKey}", database, log,
ReadPreference.Secondary, options.Value.OptimizeForSelfHosting);
private readonly ContentOptions options = options.Value;
private readonly ContentsOptions options = options.Value;
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;
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;
public sealed class ContentOptions
public sealed class ContentsOptions
{
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(
IAppProvider appprovider,
ITextIndex textIndex,
IOptions<ContentOptions> options,
IOptions<ContentsOptions> options,
IMemoryCache cache,
IJsonSerializer serializer)
{
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,
CancellationToken ct = default)

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

@ -22,12 +22,12 @@ public sealed class ContentQueryService(
IContentEnricher contentEnricher,
IContentRepository contentRepository,
IContentLoader contentLoader,
IOptions<ContentOptions> options,
IOptions<ContentsOptions> options,
ContentQueryParser queryParser)
: IContentQueryService
{
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,
[EnumeratorCancellation] CancellationToken ct = default)

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve">
<value>Vous ne pouvez pas vous verrouiller.</value>
</data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<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>
</data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve">
<value>{action} avec &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</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">
<value>Entrer le mot de passe</value>
</data>

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve">
<value>Non puoi bloccare te stesso.</value>
</data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<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>
</data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve">
<value>{action} con &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</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">
<value>Inserisci la password</value>
</data>

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve">
<value>Je kunt jezelf niet vergrendelen.</value>
</data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<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>
</data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve">
<value>{action} met &lt;strong&gt; {provider} &lt;/strong&gt;</value>
</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">
<value>Voer wachtwoord in</value>
</data>

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve">
<value>Você não pode se trancar.</value>
</data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<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>
</data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve">
<value>{action} com &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</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">
<value>Introduzir Password</value>
</data>

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve">
<value>You cannot lock yourself.</value>
</data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<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>
</data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve">
<value>{action} with &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</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">
<value>Enter Password</value>
</data>

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

@ -1132,9 +1132,6 @@
<data name="users.lockYourselfError" xml:space="preserve">
<value>你不能锁定自己。</value>
</data>
<data name="users.login.askAdmin" xml:space="preserve">
<value />
</data>
<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>
</data>
@ -1150,18 +1147,6 @@
<data name="users.login.loginWith" xml:space="preserve">
<value>{action} with &lt;strong&gt;{provider}&lt;/strong&gt;</value>
</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">
<value>输入密码</value>
</data>

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

@ -20,11 +20,11 @@ public sealed class UrlGenerator(
IGenericUrlGenerator urlGenerator,
IAssetFileStore assetFileStore,
IOptions<AssetOptions> assetOptions,
IOptions<ContentOptions> contentOptions)
IOptions<ContentsOptions> contentOptions)
: IUrlGenerator, IHttpImageEndpoint
{
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)
{

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>
/// <response code="204">Schema index deletion added to job queue.</response>
/// <response code="404">Schema or app not found.</response>
[HttpPost]
[HttpDelete]
[Route("apps/{app}/schemas/{schema}/indexes/{name}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[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;
if (uiOptions != null)
{
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)
{
services.Configure<ContentOptions>(config,
services.Configure<ContentsOptions>(config,
"contents");
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 Squidex.AI;
using Squidex.Areas.Api.Controllers.UI;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Hosting;
using Squidex.Text.Translations;
@ -56,6 +57,13 @@ public static class FontendServices
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) =>
{
if (string.IsNullOrWhiteSpace(options.CollaborationService))

3
backend/src/Squidex/appsettings.json

@ -161,6 +161,9 @@
// Hide all onboarding tooltips and dialogs.
"hideOnboarding": false,
// Hide the indexes UI.
"hideIndexes": false,
// Hide the today and now button.
"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;
})
.Configure<ContentOptions>(x =>
.Configure<ContentsOptions>(x =>
{
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 =
new ServiceCollection()
.AddSingleton(Options.Create(new ContentOptions { OptimizeForSelfHosting = selfHosting }))
.AddSingleton(Options.Create(new ContentsOptions { OptimizeForSelfHosting = selfHosting }))
.AddSingleton(CreateAppProvider())
.AddSingleton(mongoClient)
.AddSingleton(mongoDatabase)

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

@ -26,7 +26,7 @@ public class ContentQueryParserTests : GivenContext
public ContentQueryParserTests()
{
var options = Options.Create(new ContentOptions { DefaultPageSize = 30 });
var options = Options.Create(new ContentsOptions { DefaultPageSize = 30 });
Schema = Schema
.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))
.ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!));
var options = Options.Create(new ContentOptions());
var options = Options.Create(new ContentsOptions());
sut = new ContentQueryService(
AppProvider,

1
frontend/.prettierrc

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

8
frontend/package-lock.json

@ -107,7 +107,7 @@
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.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",
"storybook": "^8.1.5",
"stylelint": "16.6.1",
@ -22682,9 +22682,9 @@
}
},
"node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"

2
frontend/package.json

@ -114,7 +114,7 @@
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.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",
"storybook": "^8.1.5",
"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>
}
@if (info) {
<div class="apps-section">
<small class="info">{{ info }}</small>

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

@ -16,7 +16,7 @@
{{ "tour.welcome" | sqxTranslate }}
<span class="header-focus">{{ "tour.welcomeProduct" | sqxTranslate }}</span>
</h1>
<div [innerHTML]="'tour.stepIntroText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div [sqxMarkdown]="'tour.stepIntroText' | sqxTranslate"></div>
<div class="mt-4">
<button class="btn btn-success" (click)="next()">
{{ "tour.stepIntroNext" | sqxTranslate }}
@ -32,7 +32,7 @@
<div class="onboarding-enter-leave" @slide>
<form [formGroup]="answersForm" (ngSubmit)="submitAnswers()">
<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">
<label for="role">{{ "tour.stepDataCompanyRole" | sqxTranslate }}</label>
<select class="form-select" id="companyRole" formControlName="companyRole">
@ -79,7 +79,7 @@
<img class="header-left" @fade src="./images/logo-white-small.png" />
<div class="onboarding-enter-leave text-center" @slide>
<h2>{{ "tour.stepTourTitle" | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepTourText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div [sqxMarkdown]="'tour.stepTourText' | sqxTranslate"></div>
<div class="mt-4">
<button class="btn btn-success" (click)="start()">
{{ "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 { 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({
standalone: true,
@ -20,10 +20,9 @@ import { fadeAnimation, MarkdownPipe, ModalDialogComponent, SafeHtmlPipe, slideA
],
imports: [
FormsModule,
MarkdownPipe,
MarkdownDirective,
ModalDialogComponent,
ReactiveFormsModule,
SafeHtmlPipe,
TranslatePipe,
],
})

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

@ -62,7 +62,7 @@
<div class="col-auto">
<button
class="btn btn-success"
attr.aria-label="{{ 'i18n:assets.createFolder' | sqxTranslate }}"
attr.aria-label="{{ 'assets.createFolder' | sqxTranslate }}"
(click)="addAssetFolderDialog.show()"
shortcut="CTRL + U"
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>
</div>
@if (isDeleted) {
<div
[innerHTML]="
'contents.versionViewingDeleted' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml
"></div>
<div inline="true" [sqxMarkdown]="'contents.versionViewingDeleted' | sqxTranslate: { version: contentVersion }"></div>
}
@if (!isDeleted) {
<div
[innerHTML]="
'contents.versionViewing' | sqxTranslate: { version: contentVersion } | sqxMarkdownInline | sqxSafeHtml
"></div>
<div inline="true" [sqxMarkdown]="'contents.versionViewing' | sqxTranslate: { version: contentVersion }"></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 { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
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';
@Component({
@ -24,8 +24,7 @@ import { ContentSectionComponent } from '../../../shared/forms/content-section.c
FormErrorComponent,
FormsModule,
ListViewComponent,
MarkdownInlinePipe,
SafeHtmlPipe,
MarkdownDirective,
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="col" [class.disabled]="snapshot.isDisabled">
<button type="button"
class="btn btn-outline-secondary btn-dashed btn-block btn-add"
(click)="assetsDialog.show()">
<button class="btn btn-outline-secondary btn-dashed btn-block btn-add" (click)="assetsDialog.show()" type="button">
{{ "contents.assetsUpload" | sqxTranslate }}
</button>
</div>
@ -13,7 +11,12 @@
[sqxDropDisabled]="snapshot.isDisabled"
(sqxDropFile)="addFiles($event)">
{{ "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>

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

@ -5,9 +5,7 @@
@if (!isScrolled) {
<div class="dashboard-summary" @fade sqxTourStep="appDashboard">
<h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1>
<div
class="subtext"
[innerHTML]="'dashboard.welcomeText' | sqxTranslate: { app: app.displayName } | sqxMarkdown | sqxSafeHtml"></div>
<div class="subtext" [sqxMarkdown]="'dashboard.welcomeText' | sqxTranslate: { app: app.displayName }"></div>
</div>
}
<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 { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
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 { ContentSummaryCardComponent } from './cards/content-summary-card.component';
import { GithubCardComponent } from './cards/github-card.component';
@ -42,10 +42,9 @@ import { DashboardConfigComponent } from './dashboard-config.component';
GridsterItemComponent,
HistoryCardComponent,
IFrameCardComponent,
MarkdownPipe,
MarkdownDirective,
RandomCatCardComponent,
RandomDogCardComponent,
SafeHtmlPipe,
SchemaCardComponent,
SupportCardComponent,
TitleComponent,

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

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

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

@ -27,5 +27,6 @@
</button>
}
}
<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" />
<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>
</div>
</div>
@ -41,7 +41,7 @@
</div>
<sqx-form-hint>
{{ "schemas.fieldTypes.assets.resolveHint" | sqxTranslate | sqxMarkdownInline }}
<span inline="true" [sqxMarkdown]="'schemas.fieldTypes.assets.resolveHint' | sqxTranslate"></span>
</sqx-form-hint>
</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 { 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({
standalone: true,
@ -18,9 +18,8 @@ import { AssetFolderDropdownComponent, AssetsFieldPropertiesDto, FieldDto, FormH
AssetFolderDropdownComponent,
FormHintComponent,
FormsModule,
MarkdownInlinePipe,
MarkdownDirective,
ReactiveFormsModule,
SafeHtmlPipe,
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" />
<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>
</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 { 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({
standalone: true,
@ -18,9 +18,8 @@ import { DATETIME_FIELD_EDITORS, DateTimeFieldPropertiesDto, FieldDto, FloatConv
imports: [
FormHintComponent,
FormsModule,
MarkdownInlinePipe,
MarkdownDirective,
ReactiveFormsModule,
SafeHtmlPipe,
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 }}
</a>
</li>
@if (!hideIndexes) {
<li>
<a class="nav-link" [class.active]="tab === 'indexes'" [queryParams]="{ tab: 'indexes' }" [routerLink]="[]">
{{ "schemas.tabIndexes" | sqxTranslate }}
</a>
</li>
}
<li>
<a class="nav-link" [class.active]="tab === 'more'" [queryParams]="{ tab: 'more' }" [routerLink]="[]">
{{ "schemas.tabMore" | sqxTranslate }}
@ -94,6 +101,9 @@
@case ("json") {
<sqx-schema-export-form [schema]="schema"></sqx-schema-export-form>
}
@case ("indexes") {
<sqx-schema-indexes [schema]="schema"></sqx-schema-indexes>
}
@case ("more") {
<sqx-list-view innerWidth="50rem">
<div>

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

@ -6,14 +6,15 @@
*/
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 { 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 { SchemaEditFormComponent } from './common/schema-edit-form.component';
import { SchemaExportFormComponent } from './export/schema-export-form.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 { SchemaFieldRulesFormComponent } from './rules/schema-field-rules-form.component';
import { SchemaScriptsFormComponent } from './scripts/schema-scripts-form.component';
@ -39,6 +40,7 @@ import { SchemaUIFormComponent } from './ui/schema-ui-form.component';
SchemaExportFormComponent,
SchemaFieldRulesFormComponent,
SchemaFieldsComponent,
SchemaIndexesComponent,
SchemaPreviewUrlsFormComponent,
SchemaScriptsFormComponent,
SchemaUIFormComponent,
@ -52,7 +54,7 @@ import { SchemaUIFormComponent } from './ui/schema-ui-form.component';
export class SchemaPageComponent implements OnInit {
private readonly subscriptions = new Subscriptions();
public readonly exact = { exact: true };
public readonly hideIndexes = inject(UIOptions).value.hideIndexes;
public schema!: SchemaDto;
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") {
<h3>{{ "clients.connectWizard.step1Title" | sqxTranslate }}</h3>
<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>
<div class="section">
<div class="option" (click)="go('HTTP')">
@ -108,7 +108,7 @@
<span class="badge rounded-pill bg-dark">1</span>
{{ "clients.connectWizard.cliStep1" | sqxTranslate }}
</h5>
<div [innerHTML]="'clients.connectWizard.cliStep1Download' | sqxTranslate | sqxMarkdown: true | sqxSafeHtml"></div>
<div [sqxMarkdown]="'clients.connectWizard.cliStep1Download' | sqxTranslate" trusted="true"></div>
<sqx-form-hint>
{{ "clients.connectWizard.cliStep1Hint" | sqxTranslate }}
</sqx-form-hint>
@ -116,7 +116,7 @@
<div class="section step">
<h5>
<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>
</div>
<div class="section step">
@ -150,7 +150,7 @@
<div class="sdk-header" [class.active]="sdk === availableSDK.value" (click)="select(availableSDK.value)">
<div
class="logo d-flex align-items-center justify-content-center"
[innerHTML]="availableSDK.value.logo | sqxSafeHtml"></div>
[sqxMarkdown]="availableSDK.value.logo"></div>
{{ availableSDK.value.name }}
</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 { 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({
standalone: true,
@ -21,10 +21,7 @@ import { AccessTokenDto, ApiUrlConfig, AppsState, ClientDto, ClientsService, Cli
FormHintComponent,
KeyValuePipe,
MarkdownDirective,
MarkdownInlinePipe,
MarkdownPipe,
ModalDialogComponent,
SafeHtmlPipe,
TooltipDirective,
TranslatePipe,
],

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

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

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

@ -35,7 +35,7 @@
<button
class="btn btn-outline-secondary btn-expand ms-1"
[class.expanded]="expanded"
[class.expanded]="isExpanded"
(click)="toggleExpanded()"
type="button">
<span class="hidden">{{ "common.settings" | sqxTranslate }}</span>
@ -56,7 +56,7 @@
</div>
</div>
@if (expanded) {
@if (isExpanded) {
<div class="table-items-row-details">
<div class="job-header">
<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 { 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({
standalone: true,
@ -21,9 +21,7 @@ import { ApiUrlConfig, CodeEditorComponent, ConfirmClickDirective, Duration, Ext
ExternalLinkDirective,
FormsModule,
FromNowPipe,
KNumberPipe,
StatusIconComponent,
TooltipDirective,
TranslatePipe,
],
})
@ -34,7 +32,7 @@ export class JobComponent {
public duration = '';
public details = '';
public expanded = false;
public isExpanded = false;
constructor(
public readonly apiUrl: ApiUrlConfig,
@ -65,6 +63,6 @@ export class JobComponent {
}
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">
<h5 class="card-title">Squidex Referal Program</h5>
<sqx-form-hint>
<div [innerHTML]="'plans.referralHint' | sqxTranslate: referral | sqxMarkdown | sqxSafeHtml"></div>
<div [innerHTML]="referral.condition | sqxMarkdown | sqxSafeHtml"></div>
<div [innerHTML]="'plans.referralEarned' | sqxTranslate: referral | sqxMarkdown | sqxSafeHtml"></div>
<div [sqxMarkdown]="'plans.referralHint' | sqxTranslate: referral"></div>
<div [sqxMarkdown]="referral.condition"></div>
<div [sqxMarkdown]="'plans.referralEarned' | sqxTranslate: referral"></div>
</sqx-form-hint>
</div>
}

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

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
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';
@Component({
@ -22,13 +22,12 @@ import { PlanComponent } from './plan.component';
FormHintComponent,
LayoutComponent,
ListViewComponent,
MarkdownPipe,
MarkdownDirective,
NotifoComponent,
PlanComponent,
RouterLink,
RouterLinkActive,
RouterOutlet,
SafeHtmlPipe,
ShortcutDirective,
SidebarMenuDirective,
TitleComponent,

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

@ -25,7 +25,7 @@
</div>
<div class="table-items-row-details-tab">
@if (details | async; as loadedDetails) {
<div class="help" [innerHTML]="loadedDetails | sqxMarkdown | sqxSafeHtml"></div>
<div class="help" [sqxMarkdown]="loadedDetails"></div>
} @else {
<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 { ChangeDetectionStrategy, Component, Input } from '@angular/core';
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({
standalone: true,
@ -20,8 +20,7 @@ import { AppsState, ClientsState, FormHintComponent, LoaderComponent, MarkdownPi
AsyncPipe,
FormHintComponent,
LoaderComponent,
MarkdownPipe,
SafeHtmlPipe,
MarkdownDirective,
TranslatePipe,
],
})

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

@ -16,7 +16,7 @@
<ng-container>
<sqx-list-view innerWidth="50rem" [isLoading]="templatesState.isLoading | async">
<sqx-form-alert light="true">
<div [innerHTML]="'templates.cliHint' | sqxTranslate | sqxMarkdownInline | sqxSafeHtml"></div>
<div inline="true" [sqxMarkdown]="'templates.cliHint' | sqxTranslate"></div>
</sqx-form-alert>
@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 { Component, OnInit } from '@angular/core';
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';
@Component({
@ -21,11 +21,10 @@ import { TemplateComponent } from './template.component';
FormAlertComponent,
LayoutComponent,
ListViewComponent,
MarkdownInlinePipe,
MarkdownDirective,
RouterLink,
RouterLinkActive,
RouterOutlet,
SafeHtmlPipe,
ShortcutDirective,
SidebarMenuDirective,
TemplateComponent,

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

@ -5,9 +5,7 @@
@if (!isScrolled) {
<div class="dashboard-summary" @fade>
<h1 class="dashboard-title">{{ "dashboard.welcomeTitle" | sqxTranslate: { user: user } }}</h1>
<div
class="subtext"
[innerHTML]="'dashboard.welcomeTextTeam' | sqxTranslate: { team: team.name } | sqxMarkdown | sqxSafeHtml"></div>
<div class="subtext" [sqxMarkdown]="'dashboard.welcomeTextTeam' | sqxTranslate: { team: team.name }"></div>
</div>
}
<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 { AfterViewInit, Component, NgZone, OnInit, Renderer2, ViewChild } from '@angular/core';
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';
@Component({
@ -33,10 +33,9 @@ import { AppsCardComponent } from './cards/apps-card.component';
GridsterComponent,
GridsterItemComponent,
IFrameCardComponent,
MarkdownPipe,
MarkdownDirective,
RandomCatCardComponent,
RandomDogCardComponent,
SafeHtmlPipe,
SupportCardComponent,
TitleComponent,
TranslatePipe,

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

@ -72,7 +72,7 @@
titlePosition="left">
<i class="icon-time"></i>
</a>
<a
class="panel-link"
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">
<h5 class="card-title">Squidex Referal Program</h5>
<sqx-form-hint>
<div [innerHTML]="'plans.referralHint' | sqxTranslate: referral | sqxMarkdown | sqxSafeHtml"></div>
<div [innerHTML]="referral.condition | sqxMarkdown | sqxSafeHtml"></div>
<div [innerHTML]="'plans.referralEarned' | sqxTranslate: referral | sqxMarkdown | sqxSafeHtml"></div>
<div [sqxMarkdown]="'plans.referralHint' | sqxTranslate: referral"></div>
<div [sqxMarkdown]="referral.condition"></div>
<div [sqxMarkdown]="'plans.referralEarned' | sqxTranslate: referral"></div>
</sqx-form-hint>
</div>
}

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

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common';
import { Component, OnInit } from '@angular/core';
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 { PlanComponent } from './plan.component';
@ -23,13 +23,12 @@ import { PlanComponent } from './plan.component';
FormHintComponent,
LayoutComponent,
ListViewComponent,
MarkdownPipe,
MarkdownDirective,
NotifoComponent,
PlanComponent,
RouterLink,
RouterLinkActive,
RouterOutlet,
SafeHtmlPipe,
ShortcutDirective,
SidebarMenuDirective,
TitleComponent,

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

@ -7,13 +7,6 @@
.anchor {
@include absolute(0, null, null, 0);
background-color: red !important;
z-index: 1000;
opacity: .1;
& * {
background-color: red !important;
}
&.full-size {
@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()">
<i class="icon-close"></i>
</span>
<div [innerHTML]="error | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div [sqxMarkdown]="error | sqxTranslate"></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 { ErrorDto, TypedSimpleChanges } from '@app/framework/internal';
import { MarkdownPipe } from '../pipes/markdown.pipe';
import { MarkdownDirective } from '../markdown.directive';
import { TranslatePipe } from '../pipes/translate.pipe';
import { SafeHtmlPipe } from '../safe-html.pipe';
@Component({
standalone: true,
@ -19,8 +18,7 @@ import { SafeHtmlPipe } from '../safe-html.pipe';
templateUrl: './form-error.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MarkdownPipe,
SafeHtmlPipe,
MarkdownDirective,
TranslatePipe,
],
})

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

@ -7,7 +7,7 @@
{{ request.title | sqxTranslate }}
</ng-container>
<ng-container content>
<span [innerHTML]="request.text | sqxTranslate | sqxMarkdown | sqxSafeHtml"></span>
<span [sqxMarkdown]="request.text | sqxTranslate"></span>
@if (request.canRemember) {
<div class="form-check mt-4">
<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">
<button class="btn-close" data-dismiss="alert" (dialogClose)="close(notification)" type="button"></button>
<div class="shadow"></div>
<div class="shadowed" [innerHTML]="notification.message | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="shadowed" [sqxMarkdown]="notification.message | sqxTranslate"></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 { DialogModel, DialogRequest, DialogService, fadeAnimation, Notification, StatefulComponent, Subscriptions, Tooltip } from '@app/framework/internal';
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 { SafeHtmlPipe } from '../safe-html.pipe';
import { ModalDialogComponent } from './modal-dialog.component';
import { ModalPlacementDirective } from './modal-placement.directive';
import { ModalDirective } from './modal.directive';
@ -42,11 +41,10 @@ interface State {
imports: [
FocusOnInitDirective,
FormsModule,
MarkdownPipe,
MarkdownDirective,
ModalDialogComponent,
ModalDirective,
ModalPlacementDirective,
SafeHtmlPipe,
TooltipDirective,
TranslatePipe,
],

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

@ -7,7 +7,11 @@
<ng-content select="[title]"></ng-content>
</h4>
<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>
}

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

@ -19,9 +19,9 @@
</div>
<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 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 { 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 { SafeHtmlPipe } from '../safe-html.pipe';
import { ModalPlacementDirective } from './modal-placement.directive';
import { ModalDirective } from './modal.directive';
import { StepDefinition, TourService } from './tour.service';
@ -25,11 +24,9 @@ import { StepDefinition, TourService } from './tour.service';
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MarkdownInlinePipe,
MarkdownPipe,
MarkdownDirective,
ModalDirective,
ModalPlacementDirective,
SafeHtmlPipe,
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/highlight.pipe';
export * from './angular/pipes/keys.pipe';
export * from './angular/pipes/markdown.pipe';
export * from './angular/pipes/name.pipe';
export * from './angular/pipes/numbers.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 { FormsModule } from '@angular/forms';
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 { ChatItemComponent } from './chat-item.component';
@ -33,7 +33,6 @@ interface State {
ChatItemComponent,
FormsModule,
ModalDialogComponent,
ResizedDirective,
TooltipDirective,
TranslatePipe,
],

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

@ -54,7 +54,11 @@
</span>
}
@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 }}
@if (snapshot.isCopying) {
<sqx-loader></sqx-loader>

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

@ -26,7 +26,7 @@
{{ commentItem.comment.user | sqxUserNameRef: null }}
</div>
</div>
<div [innerHTML]="commentItem.comment.text | sqxMarkdown | sqxSafeHtml"></div>
<div [sqxMarkdown]="commentItem.comment.text"></div>
<div class="comment-created text-muted">
@if (canFollow && commentItem.comment.url) {
<a [routerLink]="commentItem.comment.url">{{ "comments.follow" | sqxTranslate }}</a>
@ -69,8 +69,8 @@
autoFocus="true"
autoSelectFirst="false"
(editorKeyPress)="onKeyPressUpdate($event)"
[itemsSource]="contributors"
formControlName="text"
[itemsSource]="contributors"
startCharacter="@"
textArea="true">
</sqx-autocomplete>
@ -102,8 +102,8 @@
autoFocus="true"
autoSelectFirst="false"
(editorKeyPress)="onKeyPressReply($event)"
[itemsSource]="contributors"
formControlName="text"
[itemsSource]="contributors"
sqxFocusOnInit
startCharacter="@"
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 { ReactiveFormsModule } from '@angular/forms';
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 { UserNameRefPipe, UserPictureRefPipe } from '../pipes';
@ -30,10 +30,9 @@ interface State {
AutocompleteComponent,
ConfirmClickDirective,
FromNowPipe,
MarkdownPipe,
MarkdownDirective,
ReactiveFormsModule,
RouterLink,
SafeHtmlPipe,
ScrollActiveDirective,
TooltipDirective,
TranslatePipe,

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

@ -94,7 +94,7 @@
[query]="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>
}
} @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 { FormsModule, ReactiveFormsModule } from '@angular/forms';
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 { TourHintDirective } from '../tour-hint.directive';
import { QueryComponent } from './queries/query.component';
@ -26,12 +26,11 @@ import { SavedQueriesComponent } from './shared-queries.component';
ControlErrorsComponent,
FocusOnInitDirective,
FormsModule,
MarkdownPipe,
MarkdownDirective,
ModalDialogComponent,
ModalDirective,
QueryComponent,
ReactiveFormsModule,
SafeHtmlPipe,
SavedQueriesComponent,
ShortcutComponent,
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/help.service';
export * from './services/history.service';
export * from './services/indexes.service';
export * from './services/jobs.service';
export * from './services/languages.service';
export * from './services/news.service';
@ -57,6 +58,8 @@ export * from './state/contents.forms';
export * from './state/contents.state';
export * from './state/contributors.forms';
export * from './state/contributors.state';
export * from './state/indexes.forms';
export * from './state/indexes.state';
export * from './state/jobs.state';
export * from './state/languages.forms';
export * from './state/languages.state';
@ -74,11 +77,11 @@ export * from './state/schemas.forms';
export * from './state/schemas.state';
export * from './state/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.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.state';
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) => {
let restore: RestoreDto | null;
@ -109,7 +109,7 @@ describe('JobsService', () => {
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) => {
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