Browse Source

WIP: Ng update (#1014)

* More required.

* Temp

* Update angular.

* Just some progress

* Update angular.

* Temporary

* Get rid of change detector.

* Temp

* More progress

* Progress

* Simplify modal.

* Add transform

* Apply transforms.

* Update storybook.

* Finalize tour.
pull/1015/head^2
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
089f6f5ead
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      backend/i18n/extract_keys.bat
  2. 70
      backend/i18n/frontend_en.json
  3. 70
      backend/i18n/frontend_fr.json
  4. 70
      backend/i18n/frontend_it.json
  5. 70
      backend/i18n/frontend_nl.json
  6. 70
      backend/i18n/frontend_pt.json
  7. 70
      backend/i18n/frontend_zh.json
  8. 70
      backend/i18n/source/frontend_en.json
  9. 2
      backend/i18n/source/frontend_fr.json
  10. 2
      backend/i18n/source/frontend_it.json
  11. 2
      backend/i18n/source/frontend_nl.json
  12. 2
      backend/i18n/source/frontend_pt.json
  13. 2
      backend/i18n/source/frontend_zh.json
  14. 5
      backend/i18n/translator/Squidex.Translator/Commands.cs
  15. 37
      backend/i18n/translator/Squidex.Translator/Processes/CheckFrontend.cs
  16. 8
      backend/i18n/translator/Squidex.Translator/Processes/Helper.cs
  17. 7
      backend/i18n/translator/Squidex.Translator/Processes/TranslateTemplates.cs
  18. 8
      backend/i18n/translator/Squidex.Translator/Processes/TranslateTypescript.cs
  19. 14
      backend/i18n/translator/Squidex.Translator/State/TranslationService.cs
  20. 21
      backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs
  21. 57
      backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  22. 33
      frontend/.storybook/main.js
  23. 5
      frontend/.stylelintrc.json
  24. 31
      frontend/angular.json
  25. 65085
      frontend/package-lock.json
  26. 171
      frontend/package.json
  27. 18
      frontend/src/app/app.component.html
  28. 2
      frontend/src/app/app.module.ts
  29. 4
      frontend/src/app/app.routes.ts
  30. 4
      frontend/src/app/features/administration/guards/user-must-exist.guard.ts
  31. 32
      frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  32. 10
      frontend/src/app/features/administration/pages/restore/restore-page.component.html
  33. 2
      frontend/src/app/features/administration/pages/users/user-page.component.html
  34. 12
      frontend/src/app/features/administration/pages/users/users-page.component.html
  35. 6
      frontend/src/app/features/administration/state/event-consumers.state.ts
  36. 6
      frontend/src/app/features/administration/state/users.state.ts
  37. 8
      frontend/src/app/features/api/api-area.component.html
  38. 56
      frontend/src/app/features/api/pages/graphql/graphql-page.component.html
  39. 12
      frontend/src/app/features/api/pages/graphql/graphql-page.component.ts
  40. 22
      frontend/src/app/features/apps/pages/app.component.html
  41. 4
      frontend/src/app/features/apps/pages/app.component.ts
  42. 39
      frontend/src/app/features/apps/pages/apps-page.component.html
  43. 13
      frontend/src/app/features/apps/pages/apps-page.component.scss
  44. 22
      frontend/src/app/features/apps/pages/apps-page.component.ts
  45. 4
      frontend/src/app/features/apps/pages/news-dialog.component.ts
  46. 90
      frontend/src/app/features/apps/pages/onboarding-dialog.component.html
  47. 4
      frontend/src/app/features/apps/pages/onboarding-dialog.component.scss
  48. 13
      frontend/src/app/features/apps/pages/onboarding-dialog.component.ts
  49. 20
      frontend/src/app/features/apps/pages/team.component.html
  50. 4
      frontend/src/app/features/apps/pages/team.component.ts
  51. 4
      frontend/src/app/features/assets/pages/asset-tag-dialog.component.html
  52. 14
      frontend/src/app/features/assets/pages/asset-tag-dialog.component.ts
  53. 6
      frontend/src/app/features/assets/pages/asset-tags.component.html
  54. 10
      frontend/src/app/features/assets/pages/asset-tags.component.ts
  55. 2
      frontend/src/app/features/assets/pages/assets-filters-page.component.html
  56. 22
      frontend/src/app/features/assets/pages/assets-page.component.html
  57. 2
      frontend/src/app/features/content/module.ts
  58. 170
      frontend/src/app/features/content/pages/calendar/calendar-page.component.html
  59. 2
      frontend/src/app/features/content/pages/comments/comments-page.component.html
  60. 6
      frontend/src/app/features/content/pages/content/content-event.component.ts
  61. 126
      frontend/src/app/features/content/pages/content/content-history-page.component.html
  62. 71
      frontend/src/app/features/content/pages/content/content-page.component.html
  63. 2
      frontend/src/app/features/content/pages/content/editor/content-editor.component.html
  64. 22
      frontend/src/app/features/content/pages/content/editor/content-editor.component.ts
  65. 18
      frontend/src/app/features/content/pages/content/editor/content-field.component.ts
  66. 2
      frontend/src/app/features/content/pages/content/editor/content-section.component.html
  67. 30
      frontend/src/app/features/content/pages/content/editor/content-section.component.ts
  68. 52
      frontend/src/app/features/content/pages/content/editor/field-copy-button.component.html
  69. 6
      frontend/src/app/features/content/pages/content/editor/field-copy-button.component.ts
  70. 13
      frontend/src/app/features/content/pages/content/editor/field-languages.component.html
  71. 12
      frontend/src/app/features/content/pages/content/editor/field-languages.component.ts
  72. 4
      frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.html
  73. 10
      frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.ts
  74. 2
      frontend/src/app/features/content/pages/content/references/content-references.component.html
  75. 8
      frontend/src/app/features/content/pages/content/references/content-references.component.ts
  76. 2
      frontend/src/app/features/content/pages/contents/contents-filters-page.component.html
  77. 43
      frontend/src/app/features/content/pages/contents/contents-page.component.html
  78. 6
      frontend/src/app/features/content/pages/contents/custom-view-editor.component.ts
  79. 2
      frontend/src/app/features/content/pages/references/references-page.component.html
  80. 2
      frontend/src/app/features/content/pages/schemas/schemas-page.component.html
  81. 2
      frontend/src/app/features/content/pages/sidebar/sidebar-page.component.html
  82. 6
      frontend/src/app/features/content/shared/content-extension.component.ts
  83. 68
      frontend/src/app/features/content/shared/due-time-selector.component.html
  84. 4
      frontend/src/app/features/content/shared/due-time-selector.component.ts
  85. 12
      frontend/src/app/features/content/shared/forms/array-editor.component.html
  86. 28
      frontend/src/app/features/content/shared/forms/array-editor.component.ts
  87. 30
      frontend/src/app/features/content/shared/forms/array-item.component.ts
  88. 18
      frontend/src/app/features/content/shared/forms/assets-editor.component.html
  89. 15
      frontend/src/app/features/content/shared/forms/assets-editor.component.ts
  90. 6
      frontend/src/app/features/content/shared/forms/chat-dialog.component.html
  91. 8
      frontend/src/app/features/content/shared/forms/chat-dialog.component.ts
  92. 2
      frontend/src/app/features/content/shared/forms/component-section.component.html
  93. 22
      frontend/src/app/features/content/shared/forms/component-section.component.ts
  94. 12
      frontend/src/app/features/content/shared/forms/component.component.html
  95. 20
      frontend/src/app/features/content/shared/forms/component.component.ts
  96. 19
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  97. 30
      frontend/src/app/features/content/shared/forms/field-editor.component.ts
  98. 22
      frontend/src/app/features/content/shared/forms/iframe-editor.component.html
  99. 28
      frontend/src/app/features/content/shared/forms/iframe-editor.component.ts
  100. 60
      frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html

3
backend/i18n/extract_keys.bat

@ -0,0 +1,3 @@
cd translator\Squidex.Translator
dotnet run translate check-frontend ..\..\..\.. -l en --fix

70
backend/i18n/frontend_en.json

@ -173,6 +173,7 @@
"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",
@ -316,6 +317,7 @@
"common.field": "Field",
"common.files": "Files",
"common.filters": "Filters",
"common.finish": "Finish",
"common.folder": "Folder",
"common.folders": "Folders",
"common.from": "From",
@ -348,6 +350,7 @@
"common.monthly": "Monthly",
"common.more": "More",
"common.name": "Name",
"common.next": "Continue",
"common.no": "No",
"common.nothingChanged": "Nothing has been changed.",
"common.notSupported": "Not Supported",
@ -363,6 +366,7 @@
"common.patterns": "Patterns",
"common.permission": "Permission",
"common.permissions": "Permissions",
"common.prev": "Back",
"common.preview": "Preview",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
@ -533,7 +537,6 @@
"contents.updated": "Content updated successfully.",
"contents.updateFailed": "Failed to update content. Please reload.",
"contents.validate": "Validate",
"contents.validationHint": "Please remember to check all languages when you see validation errors.",
"contents.versionCompare": "Compare",
"contents.versionDelete": "Delete this Version",
"contents.versionViewing": "Viewing version **{version}**.",
@ -644,7 +647,6 @@
"news.headline": "What's new?",
"news.title": "New Features",
"notifications.empty": "No notifications yet",
"notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.",
"plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.",
"plans.billingPortal": "Billing Portal",
"plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.",
@ -805,7 +807,6 @@
"schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.",
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.contextMenuTour": "Open the context menu to delete the schema or to create some scripts for content changes.",
"schemas.create": "Create Schema",
"schemas.createCategory": "Create new category...",
"schemas.createFailed": "Failed to create schema. Please reload.",
@ -962,7 +963,6 @@
"schemas.previewUrls.title": "Preview URLs",
"schemas.previewUrls.urlPlaceholder": "URL with variables",
"schemas.published": "Published",
"schemas.publishedTour": "Note, that you have to publish the schema before you can add content to it.",
"schemas.publishFailed": "Failed to publish schema. Please reload.",
"schemas.referenceFields": "Reference Fields",
"schemas.referenceFieldsEmpty": "Drop field here or reorder them to show the fields when referenced by another content. When no reference field is defined, the list fields are used instead.",
@ -1029,9 +1029,7 @@
"search.addFilter": "Add Filter",
"search.addGroup": "Add Group",
"search.addSorting": "Add Sorting",
"search.advancedTour": "Click this icon to show the advanced search menu!",
"search.customQuery": "Custom Query",
"search.fullTextTour": "Search for content using full text search over all fields and languages!",
"search.help": "Read more about filtering in the [Documentation](https://docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "My queries",
"search.nameQuery": "Name your query",
@ -1063,8 +1061,44 @@
"templates.loadFailed": "Failed to load templates. Please reload.",
"templates.refreshTooltip": "Refresh Templates",
"templates.reloaded": "Templates reloaded.",
"tour.joinForum": "Join our Forum",
"tour.joinGithub": "Join us on Github",
"tour.checkClient.clientContent": "We have created a **Client** for you.\n\nA Client is like an API Key and the ID and secret are used to request access tokens to call the API.",
"tour.checkClient.connectContent": "Click to **Connect** button and follow the guided form to learn how to use the client.",
"tour.checkClient.description": "Learn how to connect your App with your code to query content items.",
"tour.checkClient.navigateClientsContent": "Navigate to the **Clients** page.",
"tour.checkClient.navigateContent": "Navigate to the **Settings** section.",
"tour.checkClient.title": "Connect your Code",
"tour.complete": "Complete the Tour",
"tour.createApp.buttonContent": "In this tour we start with an empty app. We also provide ready to use sample apps.",
"tour.createApp.dashboardContent": "You have successfully created your first app.\n\nAt the dashboard we provide statistics about the data usage of your app.",
"tour.createApp.description": "The place for all contents, assets, rules and settings.",
"tour.createApp.formContent": "The name is used to identify your app and can therefore not be changed.",
"tour.createApp.title": "Create your first App",
"tour.createAsset.filterContent": "Expand the filter pane to see all available tags and to query assets based on tags.",
"tour.createAsset.navigateContent": "Go to the **Assets** section.",
"tour.createAsset.title": "Upload an Asset",
"tour.createAsset.uploadContent": "Choose an asset from your local machine to upload it to Squidex.\n\nImages will be processed to remove all metadata.",
"tour.createContent.addContent": "Click the **New** button to move to the content editor.",
"tour.createContent.description": "Use the editor to fill your app with published content.",
"tour.createContent.navigateContent": "Navigate to **Content**.",
"tour.createContent.saveContent": "First, fill in your fields.\n\nThen **Save and Publish** your content item.\n\nOnly published content items are available via the API.",
"tour.createContent.selectSchemaContent": "Select the schema, that you have created in the previous step.",
"tour.createContent.statusContent": "If you want to unpublish your content item or change the status in general, you can use this button.",
"tour.createContent.title": "Publish a Content Item",
"tour.createSchema.addContent": "Click the **Plus** button to open the form for new schemas.",
"tour.createSchema.addFieldContent": "Click this button to add a new field.\n\nNext time you can also use the big, round button at the bottom right of the screen.",
"tour.createSchema.description": "Define the structure of your content items.",
"tour.createSchema.fieldFormContent": "Add a new **Field** to your schema.\n\nPlease add a String field to keep it simple. You can add more fields later.",
"tour.createSchema.formContent": "Create a new **schema**.\n\nWe will add fields in the next step.\n\nPlease ensure that you select **Multiple contents**, so that you can add multiple content items.",
"tour.createSchema.helpContent": "Most pages have a help section with some basic information. This is useful for advanced concepts, but does not replace the documentation page.",
"tour.createSchema.historyContent": "Squidex tracks all changes and provides a history of previous updates and modifications.",
"tour.createSchema.navigateContent": "Navigate to the **Schemas** section.",
"tour.createSchema.publishContent": "**Publish** the schema by clicking on the **Published** button.\n\nThis schema cannot be used yet. Before you can add content items, you have to publish it to make it available to content editors.",
"tour.createSchema.schemaFieldContent": "The field has been added to your schema. You can make changes at anytime. Just press the **gear icon** at the right.",
"tour.createSchema.title": "Add a Schema",
"tour.documentation": "Documentation",
"tour.general.selectAppContent": "Start by selecting your **App**",
"tour.guide-title": "Onboarding Guide",
"tour.hint": "Do you know?",
"tour.projectBackend": "Backend Data Management",
"tour.projectCommerce": "E-Commerce Solution",
"tour.projectLearning": "None, I am just here for learning",
@ -1084,12 +1118,8 @@
"tour.sizeSmall": "2-10",
"tour.sizeVeryLarge": "1000+",
"tour.skip": "Skip Tour",
"tour.stepAppNext": "Continue",
"tour.stepAppText": "An app is the repository for your project (for example a blog, web shop or mobile app). You can assign contributors to your app to work together.\n\nCreate an unlimited number of apps in Squidex to manage multiple projects at the same time.",
"tour.stepAssetsNext": "Got It!",
"tour.stepAssetsText": "The assets contains all files that can also be linked to your content. For example images, videos or documents.\n\nYou can upload the assets here and use them later or also upload them directly when you create a new content item with an asset field.",
"tour.stepContentNext": "Almost there!",
"tour.stepContentText": "Content is the actual data in your app which is grouped by the schema.\n\nSelect a published schema first, then add content for this schema.",
"tour.startNo": "No, Thanks",
"tour.startYes": "Yes, Please",
"tour.stepDataCompanyRole": "What is your role?",
"tour.stepDataCompanySize": "How many people work in a company?",
"tour.stepDataNext": "Continue",
@ -1098,10 +1128,14 @@
"tour.stepDataTitle": "About You",
"tour.stepIntroNext": "Let's take a look around",
"tour.stepIntroText": "You can start managing and distributing your content right away, but we we'd like to walk you through some basics first...\n\n",
"tour.stepOutroText": "But that's not all of the support we can provide.\n\nYou can go to https://docs.squidex.io to read more.\n\nDo you want to join our community?",
"tour.stepOutroTitle": "Awesome, now you know the basics!",
"tour.stepSchemasNext": "Keep going!",
"tour.stepSchemasText": "Schemas define the structure of your content, the fields and the data types of a content item.\n\nBefore you can add content to your schema, make sure to hit the publish button at the top to make the schema available to your content editors.",
"tour.stepTourText": "We have prepared an interactive tour, where you learn the basics of Squidex. You can cancel it anytime. Do you want to start the tour?",
"tour.stepTourTitle": "Interactive Tour",
"tour.support": "Support",
"tour.testGraphQL.description": "Complete the tour and check out the GraphQL playground.",
"tour.testGraphQL.navigateContent": "Navigate to the **API** section.",
"tour.testGraphQL.navigateGraphQLContent": "Navigate to the **GraphQL** page.",
"tour.testGraphQL.queryContent": "This is the **GraphQL** playground. You can explore your API and make test queries, before you implement them later in your application.",
"tour.testGraphQL.title": "Make a test query",
"tour.tooltipConfirm": "Got It",
"tour.tooltipStop": "Stop Tour",
"tour.welcome": "Welcome to",

70
backend/i18n/frontend_fr.json

@ -173,6 +173,7 @@
"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",
@ -316,6 +317,7 @@
"common.field": "Champ",
"common.files": "Des dossiers",
"common.filters": "Filtres",
"common.finish": "Finish",
"common.folder": "Dossier",
"common.folders": "Dossiers",
"common.from": "Depuis",
@ -348,6 +350,7 @@
"common.monthly": "Mensuel",
"common.more": "Plus",
"common.name": "Nom",
"common.next": "Continue",
"common.no": "Non",
"common.nothingChanged": "Rien n'a été changé.",
"common.notSupported": "Non supporté",
@ -363,6 +366,7 @@
"common.patterns": "Motifs",
"common.permission": "Autorisation",
"common.permissions": "Autorisations",
"common.prev": "Back",
"common.preview": "Aperçu",
"common.product": "CMS sans tête Squidex",
"common.project": "Projet",
@ -533,7 +537,6 @@
"contents.updated": "Contenu mis à jour avec succès.",
"contents.updateFailed": "Échec de la mise à jour du contenu. Veuillez recharger.",
"contents.validate": "Valider",
"contents.validationHint": "N'oubliez pas de vérifier toutes les langues lorsque vous voyez des erreurs de validation.",
"contents.versionCompare": "Comparer",
"contents.versionDelete": "Supprimer cette version",
"contents.versionViewing": "Affichage de la version **{version}**.",
@ -644,7 +647,6 @@
"news.headline": "Quoi de neuf?",
"news.title": "Nouvelles fonctionnalités",
"notifications.empty": "Aucune notification pour le moment",
"notifo.subscripeTooltip": "Cliquez sur ce bouton pour vous abonner à toutes les modifications et recevoir des notifications push.",
"plans.allApps": "L'abonnement est partagé entre toutes les applications de cette équipe. Vérifiez le tableau de bord pour les applications attribuées.",
"plans.billingPortal": "Portail de facturation",
"plans.billingPortalHint": "Accédez au portail de facturation pour consulter l'historique des paiements et l'aperçu de l'abonnement.",
@ -805,7 +807,6 @@
"schemas.contentSidebarUrlHint": "URL du plug-in pour la barre latérale dans la vue détaillée.",
"schemas.contentsSidebarUrl": "Contenu de l'extension de la barre latérale",
"schemas.contentsSidebarUrlHint": "URL du plug-in pour la barre latérale dans la vue de liste.",
"schemas.contextMenuTour": "Ouvrez le menu contextuel pour supprimer le schéma ou pour créer des scripts pour les modifications de contenu.",
"schemas.create": "Créer un schéma",
"schemas.createCategory": "Créer une nouvelle catégorie...",
"schemas.createFailed": "Échec de la création du schéma. Veuillez recharger.",
@ -962,7 +963,6 @@
"schemas.previewUrls.title": "Aperçu des URL",
"schemas.previewUrls.urlPlaceholder": "URL avec variables",
"schemas.published": "Publié",
"schemas.publishedTour": "Notez que vous devez publier le schéma avant de pouvoir y ajouter du contenu.",
"schemas.publishFailed": "Échec de la publication du schéma. Veuillez recharger.",
"schemas.referenceFields": "Champs de référence",
"schemas.referenceFieldsEmpty": "Déposez le champ ici ou réorganisez-les pour afficher les champs lorsqu'ils sont référencés par un autre contenu. Lorsqu'aucun champ de référence n'est défini, les champs de liste sont utilisés à la place.",
@ -1029,9 +1029,7 @@
"search.addFilter": "Ajouter un filtre",
"search.addGroup": "Ajouter un groupe",
"search.addSorting": "Ajouter un tri",
"search.advancedTour": "Cliquez sur cette icône pour afficher le menu de recherche avancée\u00A0!",
"search.customQuery": "Requête personnalisée",
"search.fullTextTour": "Recherchez du contenu à l'aide de la recherche plein texte dans tous les champs et dans toutes les langues\u00A0!",
"search.help": "En savoir plus sur le filtrage dans la [Documentation](https://docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "Mes questions",
"search.nameQuery": "Nommez votre requête",
@ -1063,8 +1061,44 @@
"templates.loadFailed": "Échec du chargement des modèles. Veuillez recharger.",
"templates.refreshTooltip": "Actualiser les modèles",
"templates.reloaded": "Modèles rechargés.",
"tour.joinForum": "Rejoignez notre forum",
"tour.joinGithub": "Rejoignez-nous sur Github",
"tour.checkClient.clientContent": "We have created a **Client** for you.\n\nA Client is like an API Key and the ID and secret are used to request access tokens to call the API.",
"tour.checkClient.connectContent": "Click to **Connect** button and follow the guided form to learn how to use the client.",
"tour.checkClient.description": "Learn how to connect your App with your code to query content items.",
"tour.checkClient.navigateClientsContent": "Navigate to the **Clients** page.",
"tour.checkClient.navigateContent": "Navigate to the **Settings** section.",
"tour.checkClient.title": "Connect your Code",
"tour.complete": "Complete the Tour",
"tour.createApp.buttonContent": "In this tour we start with an empty app. We also provide ready to use sample apps.",
"tour.createApp.dashboardContent": "You have successfully created your first app.\n\nAt the dashboard we provide statistics about the data usage of your app.",
"tour.createApp.description": "The place for all contents, assets, rules and settings.",
"tour.createApp.formContent": "The name is used to identify your app and can therefore not be changed.",
"tour.createApp.title": "Create your first App",
"tour.createAsset.filterContent": "Expand the filter pane to see all available tags and to query assets based on tags.",
"tour.createAsset.navigateContent": "Go to the **Assets** section.",
"tour.createAsset.title": "Upload an Asset",
"tour.createAsset.uploadContent": "Choose an asset from your local machine to upload it to Squidex.\n\nImages will be processed to remove all metadata.",
"tour.createContent.addContent": "Click the **New** button to move to the content editor.",
"tour.createContent.description": "Use the editor to fill your app with published content.",
"tour.createContent.navigateContent": "Navigate to **Content**.",
"tour.createContent.saveContent": "First, fill in your fields.\n\nThen **Save and Publish** your content item.\n\nOnly published content items are available via the API.",
"tour.createContent.selectSchemaContent": "Select the schema, that you have created in the previous step.",
"tour.createContent.statusContent": "If you want to unpublish your content item or change the status in general, you can use this button.",
"tour.createContent.title": "Publish a Content Item",
"tour.createSchema.addContent": "Click the **Plus** button to open the form for new schemas.",
"tour.createSchema.addFieldContent": "Click this button to add a new field.\n\nNext time you can also use the big, round button at the bottom right of the screen.",
"tour.createSchema.description": "Define the structure of your content items.",
"tour.createSchema.fieldFormContent": "Add a new **Field** to your schema.\n\nPlease add a String field to keep it simple. You can add more fields later.",
"tour.createSchema.formContent": "Create a new **schema**.\n\nWe will add fields in the next step.\n\nPlease ensure that you select **Multiple contents**, so that you can add multiple content items.",
"tour.createSchema.helpContent": "Most pages have a help section with some basic information. This is useful for advanced concepts, but does not replace the documentation page.",
"tour.createSchema.historyContent": "Squidex tracks all changes and provides a history of previous updates and modifications.",
"tour.createSchema.navigateContent": "Navigate to the **Schemas** section.",
"tour.createSchema.publishContent": "**Publish** the schema by clicking on the **Published** button.\n\nThis schema cannot be used yet. Before you can add content items, you have to publish it to make it available to content editors.",
"tour.createSchema.schemaFieldContent": "The field has been added to your schema. You can make changes at anytime. Just press the **gear icon** at the right.",
"tour.createSchema.title": "Add a Schema",
"tour.documentation": "Documentation",
"tour.general.selectAppContent": "Start by selecting your **App**",
"tour.guide-title": "Onboarding Guide",
"tour.hint": "Do you know?",
"tour.projectBackend": "Backend Data Management",
"tour.projectCommerce": "E-Commerce Solution",
"tour.projectLearning": "None, I am just here for learning",
@ -1084,12 +1118,8 @@
"tour.sizeSmall": "2-10",
"tour.sizeVeryLarge": "1000+",
"tour.skip": "Passer le tour",
"tour.stepAppNext": "Continuer",
"tour.stepAppText": "Une application est le référentiel de votre projet, par exemple (blog, boutique en ligne ou application mobile). Vous pouvez affecter des contributeurs à votre application pour travailler ensemble. Vous pouvez créer un nombre illimité d'applications dans Squidex pour gérer plusieurs projets en même temps.",
"tour.stepAssetsNext": "J'ai compris!",
"tour.stepAssetsText": "Les actifs contiennent tous les fichiers qui peuvent également être liés à votre contenu. Par exemple des images, des vidéos ou des documents. Vous pouvez télécharger les actifs ici et les utiliser plus tard ou également les télécharger directement lorsque vous créez un nouvel élément de contenu avec un champ d'actif.",
"tour.stepContentNext": "Presque là!",
"tour.stepContentText": "Le contenu est les données réelles de votre application qui sont regroupées par le schéma. Sélectionnez d'abord un schéma publié, puis ajoutez du contenu pour ce schéma.",
"tour.startNo": "No, Thanks",
"tour.startYes": "Yes, Please",
"tour.stepDataCompanyRole": "What is your role?",
"tour.stepDataCompanySize": "How many people work in a company?",
"tour.stepDataNext": "Continue",
@ -1098,10 +1128,14 @@
"tour.stepDataTitle": "About You",
"tour.stepIntroNext": "Jetons un coup d'œil autour",
"tour.stepIntroText": "Vous pouvez commencer à gérer et à distribuer votre contenu tout de suite, mais nous aimerions d'abord vous expliquer quelques notions de base...",
"tour.stepOutroText": "Mais ce n'est pas tout le soutien que nous pouvons fournir. Vous pouvez aller sur https://docs.squidex.io pour en savoir plus. Vous souhaitez rejoindre notre communauté ?",
"tour.stepOutroTitle": "Génial, maintenant vous connaissez les bases !",
"tour.stepSchemasNext": "Continuer!",
"tour.stepSchemasText": "Les schémas définissent la structure de votre contenu, les champs et les types de données d'un élément de contenu. Avant de pouvoir ajouter du contenu à votre schéma, assurez-vous d'appuyer sur le bouton \"Publier\" en haut pour rendre le schéma disponible pour vos appSettings.editors de contenu.",
"tour.stepTourText": "We have prepared an interactive tour, where you learn the basics of Squidex. You can cancel it anytime. Do you want to start the tour?",
"tour.stepTourTitle": "Interactive Tour",
"tour.support": "Support",
"tour.testGraphQL.description": "Complete the tour and check out the GraphQL playground.",
"tour.testGraphQL.navigateContent": "Navigate to the **API** section.",
"tour.testGraphQL.navigateGraphQLContent": "Navigate to the **GraphQL** page.",
"tour.testGraphQL.queryContent": "This is the **GraphQL** playground. You can explore your API and make test queries, before you implement them later in your application.",
"tour.testGraphQL.title": "Make a test query",
"tour.tooltipConfirm": "J'ai compris",
"tour.tooltipStop": "Visite d'arrêt",
"tour.welcome": "Bienvenue à",

70
backend/i18n/frontend_it.json

@ -173,6 +173,7 @@
"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",
@ -316,6 +317,7 @@
"common.field": "Campo",
"common.files": "Campi",
"common.filters": "Filtri",
"common.finish": "Finish",
"common.folder": "Cartella",
"common.folders": "Cartelle",
"common.from": "From",
@ -348,6 +350,7 @@
"common.monthly": "Monthly",
"common.more": "More",
"common.name": "Nome",
"common.next": "Continue",
"common.no": "No",
"common.nothingChanged": "Non è stato cambiato niente.",
"common.notSupported": "Not Supported",
@ -363,6 +366,7 @@
"common.patterns": "Pattern",
"common.permission": "Permission",
"common.permissions": "Permessi",
"common.prev": "Back",
"common.preview": "Anteprima",
"common.product": "Squidex Headless CMS",
"common.project": "Progetto",
@ -533,7 +537,6 @@
"contents.updated": "Contenuto aggiornato con successo.",
"contents.updateFailed": "Non è stato possibile aggiornare il contenuto. Per favore ricarica.",
"contents.validate": "Convalida",
"contents.validationHint": "Ricorda di verificare tutte le lingue quando vedi errori di validazione.",
"contents.versionCompare": "Confronta",
"contents.versionDelete": "Cancella questa Versione",
"contents.versionViewing": "Stai guardando la versione **{version}**.",
@ -644,7 +647,6 @@
"news.headline": "Che cosa c'è di nuovo?",
"news.title": "Nuove funzionalità",
"notifications.empty": "No notifications yet",
"notifo.subscripeTooltip": "Fai clic su questo pulsante per iscriverti a tutte le modifiche e ricevere le notifiche push.",
"plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.",
"plans.billingPortal": "Portale di fatturazione",
"plans.billingPortalHint": "Vai al portale di fatturazione per lo storico dei pagamenti e una panoramica per l'abbonamento.",
@ -805,7 +807,6 @@
"schemas.contentSidebarUrlHint": "URL del plug-in per la barra di navigazione laterale nella visualizzazione dei dettagli.",
"schemas.contentsSidebarUrl": "Estensione della barra di navigazione laterale (liste)",
"schemas.contentsSidebarUrlHint": "URL del plug-in per la barra di navigazione laterale nella visualizzazione delle liste.",
"schemas.contextMenuTour": "Apri il menu per cancellare lo schema o per inserire alcuni script che modificano il contenuto.",
"schemas.create": "Crea uno Schema",
"schemas.createCategory": "Crea una nuova categoria...",
"schemas.createFailed": "Non è stato possibile creare lo schema. Per favore ricarica.",
@ -962,7 +963,6 @@
"schemas.previewUrls.title": "URL dell'anteprima",
"schemas.previewUrls.urlPlaceholder": "URL con variabili",
"schemas.published": "Pubblicato",
"schemas.publishedTour": "!Per poter creare un contenuto devi prima aver pubblicato il relativo schema.",
"schemas.publishFailed": "Non è stato possibile pubblicare lo schema. Per favore ricarica.",
"schemas.referenceFields": "Campi per i collegamenti (riferimenti)",
"schemas.referenceFieldsEmpty": "Trascina qui il file qui oppure riordina i campi da visualizzare nella lista che viene visualizzata quando colleghi il contenuto ad un altro. Quando non è impostato alcun campo per il contenuto che si desidera collegare, sono visualizzati la lista dei campi in ordine di utilizzo.",
@ -1029,9 +1029,7 @@
"search.addFilter": "Aggiungi un Filtro",
"search.addGroup": "Aggiungi un Gruppo",
"search.addSorting": "Aggiungi ordinamento",
"search.advancedTour": "Fai clic su questa icona per visualizzare il menu della ricerca avanzata!",
"search.customQuery": "Query personalizzata",
"search.fullTextTour": "Cerca contenuti utilizzando la ricerca testuale su tutti i campi e le lingue!",
"search.help": "Ulteriori informazioni sui filtri su [Documentazione](https://docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "Le mie query",
"search.nameQuery": "Dai un nome alla query",
@ -1063,8 +1061,44 @@
"templates.loadFailed": "Failed to load templates. Please reload.",
"templates.refreshTooltip": "Refresh Templates",
"templates.reloaded": "Templates reloaded.",
"tour.joinForum": "Unisciti al nostro Forum",
"tour.joinGithub": "Unisciti a Github",
"tour.checkClient.clientContent": "We have created a **Client** for you.\n\nA Client is like an API Key and the ID and secret are used to request access tokens to call the API.",
"tour.checkClient.connectContent": "Click to **Connect** button and follow the guided form to learn how to use the client.",
"tour.checkClient.description": "Learn how to connect your App with your code to query content items.",
"tour.checkClient.navigateClientsContent": "Navigate to the **Clients** page.",
"tour.checkClient.navigateContent": "Navigate to the **Settings** section.",
"tour.checkClient.title": "Connect your Code",
"tour.complete": "Complete the Tour",
"tour.createApp.buttonContent": "In this tour we start with an empty app. We also provide ready to use sample apps.",
"tour.createApp.dashboardContent": "You have successfully created your first app.\n\nAt the dashboard we provide statistics about the data usage of your app.",
"tour.createApp.description": "The place for all contents, assets, rules and settings.",
"tour.createApp.formContent": "The name is used to identify your app and can therefore not be changed.",
"tour.createApp.title": "Create your first App",
"tour.createAsset.filterContent": "Expand the filter pane to see all available tags and to query assets based on tags.",
"tour.createAsset.navigateContent": "Go to the **Assets** section.",
"tour.createAsset.title": "Upload an Asset",
"tour.createAsset.uploadContent": "Choose an asset from your local machine to upload it to Squidex.\n\nImages will be processed to remove all metadata.",
"tour.createContent.addContent": "Click the **New** button to move to the content editor.",
"tour.createContent.description": "Use the editor to fill your app with published content.",
"tour.createContent.navigateContent": "Navigate to **Content**.",
"tour.createContent.saveContent": "First, fill in your fields.\n\nThen **Save and Publish** your content item.\n\nOnly published content items are available via the API.",
"tour.createContent.selectSchemaContent": "Select the schema, that you have created in the previous step.",
"tour.createContent.statusContent": "If you want to unpublish your content item or change the status in general, you can use this button.",
"tour.createContent.title": "Publish a Content Item",
"tour.createSchema.addContent": "Click the **Plus** button to open the form for new schemas.",
"tour.createSchema.addFieldContent": "Click this button to add a new field.\n\nNext time you can also use the big, round button at the bottom right of the screen.",
"tour.createSchema.description": "Define the structure of your content items.",
"tour.createSchema.fieldFormContent": "Add a new **Field** to your schema.\n\nPlease add a String field to keep it simple. You can add more fields later.",
"tour.createSchema.formContent": "Create a new **schema**.\n\nWe will add fields in the next step.\n\nPlease ensure that you select **Multiple contents**, so that you can add multiple content items.",
"tour.createSchema.helpContent": "Most pages have a help section with some basic information. This is useful for advanced concepts, but does not replace the documentation page.",
"tour.createSchema.historyContent": "Squidex tracks all changes and provides a history of previous updates and modifications.",
"tour.createSchema.navigateContent": "Navigate to the **Schemas** section.",
"tour.createSchema.publishContent": "**Publish** the schema by clicking on the **Published** button.\n\nThis schema cannot be used yet. Before you can add content items, you have to publish it to make it available to content editors.",
"tour.createSchema.schemaFieldContent": "The field has been added to your schema. You can make changes at anytime. Just press the **gear icon** at the right.",
"tour.createSchema.title": "Add a Schema",
"tour.documentation": "Documentation",
"tour.general.selectAppContent": "Start by selecting your **App**",
"tour.guide-title": "Onboarding Guide",
"tour.hint": "Do you know?",
"tour.projectBackend": "Backend Data Management",
"tour.projectCommerce": "E-Commerce Solution",
"tour.projectLearning": "None, I am just here for learning",
@ -1084,12 +1118,8 @@
"tour.sizeSmall": "2-10",
"tour.sizeVeryLarge": "1000+",
"tour.skip": "Salta il Tour",
"tour.stepAppNext": "Continua",
"tour.stepAppText": "Un'App è il repository per il tuo progetto, ad es. (un blog, un negozio sul web o una app per dispositivi mobili). Puoi assegnare collaboratori alla tua App per lavorare insieme.\n\nPuoi creare un numero illimitato di app in Squidex per gestire più progetti contemporaneamente.",
"tour.stepAssetsNext": "Fatto!",
"tour.stepAssetsText": "Le risorse contengono tutti i file che tu puoi collegare ai tuoi contenuti. Per esempio immagini, video o documenti.\n\nPuoi caricare qui le risorse e usarle successivamente oppure caricarle direttamente quando stai creando un contenuto utilizzando un campo risorse.",
"tour.stepContentNext": "Ci siamo quasi!",
"tour.stepContentText": "I contenuti della tua app sono raggruppati tramite gli schemi.\n\nSeleziona uno schema pubblicato e poi crea il relativo contenuto.",
"tour.startNo": "No, Thanks",
"tour.startYes": "Yes, Please",
"tour.stepDataCompanyRole": "What is your role?",
"tour.stepDataCompanySize": "How many people work in a company?",
"tour.stepDataNext": "Continue",
@ -1098,10 +1128,14 @@
"tour.stepDataTitle": "About You",
"tour.stepIntroNext": "Guardiamoci intorno",
"tour.stepIntroText": "Puoi iniziare subito a gestire e distribuire i tuoi contenuti, ma prima vorremmo illustrarti alcune nozioni di base...\n\nIn questo modo",
"tour.stepOutroText": "Ma non è tutto il supporto che possiamo fornire.\n\nPer maggiori informazioni visita il sito https://docs.squidex.io.\n\nVuoi far parte della nostra community?",
"tour.stepOutroTitle": "Fantastico, ora conosci le basi!",
"tour.stepSchemasNext": "Vai avanti!",
"tour.stepSchemasText": "Gli Schemi definiscono la struttura per i tuoi contenuti, i campi e i tipi di dato per ogni tipo di contenuto.\n\nPrima di creare un contenuto riferito al tuo schema, assicurati di aver pubblicato lo schema premendo il pulsante 'Pubblica' visualizzato nell'editor dei contenuti.",
"tour.stepTourText": "We have prepared an interactive tour, where you learn the basics of Squidex. You can cancel it anytime. Do you want to start the tour?",
"tour.stepTourTitle": "Interactive Tour",
"tour.support": "Support",
"tour.testGraphQL.description": "Complete the tour and check out the GraphQL playground.",
"tour.testGraphQL.navigateContent": "Navigate to the **API** section.",
"tour.testGraphQL.navigateGraphQLContent": "Navigate to the **GraphQL** page.",
"tour.testGraphQL.queryContent": "This is the **GraphQL** playground. You can explore your API and make test queries, before you implement them later in your application.",
"tour.testGraphQL.title": "Make a test query",
"tour.tooltipConfirm": "Capito",
"tour.tooltipStop": "Ferma il Tour",
"tour.welcome": "Benvenuto su",

70
backend/i18n/frontend_nl.json

@ -173,6 +173,7 @@
"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",
@ -316,6 +317,7 @@
"common.field": "Veld",
"common.files": "Bestanden",
"common.filters": "Filters",
"common.finish": "Finish",
"common.folder": "Map",
"common.folders": "Mappen",
"common.from": "Van",
@ -348,6 +350,7 @@
"common.monthly": "Maandelijks",
"common.more": "Meer",
"common.name": "Naam",
"common.next": "Continue",
"common.no": "Nee",
"common.nothingChanged": "Er is niets veranderd.",
"common.notSupported": "Not Supported",
@ -363,6 +366,7 @@
"common.patterns": "Patronen",
"common.permission": "Permission",
"common.permissions": "Rechten",
"common.prev": "Back",
"common.preview": "Preview",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
@ -533,7 +537,6 @@
"contents.updated": "Inhoud succesvol bijgewerkt.",
"contents.updateFailed": "Bijwerken van inhoud is mislukt. Laad opnieuw.",
"contents.validate": "Valideren",
"contents.validationHint": "Denk eraan om alle talen te controleren wanneer je validatiefouten ziet.",
"contents.versionCompare": "Vergelijk",
"contents.versionDelete": "Verwijder deze versie",
"contents.versionViewing": "Bekijk versie **{version}**.",
@ -644,7 +647,6 @@
"news.headline": "Wat is er nieuw?",
"news.title": "Nieuwe functies",
"notifications.empty": "Nog geen meldingen",
"notifo.subscripeTooltip": "Klik op deze knop om je te abonneren op alle wijzigingen en om pushmeldingen te ontvangen.",
"plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.",
"plans.billingPortal": "Factureringsportal",
"plans.billingPortalHint": "Ga naar het factureringsportaal voor betalingsgeschiedenis en abonnementsoverzicht.",
@ -805,7 +807,6 @@
"schemas.contentSidebarUrlHint": "URL naar de plug-in voor de zijbalk in de detailweergave.",
"schemas.contentsSidebarUrl": "Inhoud zijbalk uitbreiding",
"schemas.contentsSidebarUrlHint": "URL naar de plug-in voor de zijbalk in de lijstweergave.",
"schemas.contextMenuTour": "Open het contextmenu om het schema te verwijderen of om scripts te maken voor wijzigingen in de inhoud.",
"schemas.create": "Schema maken",
"schemas.createCategory": "Nieuwe categorie maken ...",
"schemas.createFailed": "Kan schema niet maken. Laad opnieuw.",
@ -962,7 +963,6 @@
"schemas.previewUrls.title": "Voorbeeld-URL's",
"schemas.previewUrls.urlPlaceholder": "URL met variabelen",
"schemas.published": "Gepubliceerd",
"schemas.publishedTour": "Merk op dat je het schema moet publiceren voordat je er inhoud aan kunt toevoegen.",
"schemas.publishFailed": "Kan schema niet publiceren. Laad opnieuw.",
"schemas.referenceFields": "Referentievelden",
"schemas.referenceFieldsEmpty": "Zet het veld hier neer of rangschik ze opnieuw om de velden weer te geven wanneer ernaar wordt verwezen door een andere inhoud. Als er geen referentieveld is gedefinieerd, worden de lijstvelden gebruikt.",
@ -1029,9 +1029,7 @@
"search.addFilter": "Filter toevoegen",
"search.addGroup": "Groep toevoegen",
"search.addSorting": "Sortering toevoegen",
"search.advancedTour": "Klik op dit pictogram om het geavanceerde zoekmenu weer te geven!",
"search.customQuery": "Aangepaste zoekopdracht",
"search.fullTextTour": "Zoek naar inhoud met volledige tekstzoekopdracht in alle velden en talen!",
"search.help": "Lees meer over filteren in de [Documentation] (https: // https: //docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "Mijn zoekopdrachten",
"search.nameQuery": "Geef uw zoekopdracht een naam",
@ -1063,8 +1061,44 @@
"templates.loadFailed": "Failed to load templates. Please reload.",
"templates.refreshTooltip": "Refresh Templates",
"templates.reloaded": "Templates reloaded.",
"tour.joinForum": "Word lid van ons forum",
"tour.joinGithub": "Bezoek ons ​​op Github",
"tour.checkClient.clientContent": "We have created a **Client** for you.\n\nA Client is like an API Key and the ID and secret are used to request access tokens to call the API.",
"tour.checkClient.connectContent": "Click to **Connect** button and follow the guided form to learn how to use the client.",
"tour.checkClient.description": "Learn how to connect your App with your code to query content items.",
"tour.checkClient.navigateClientsContent": "Navigate to the **Clients** page.",
"tour.checkClient.navigateContent": "Navigate to the **Settings** section.",
"tour.checkClient.title": "Connect your Code",
"tour.complete": "Complete the Tour",
"tour.createApp.buttonContent": "In this tour we start with an empty app. We also provide ready to use sample apps.",
"tour.createApp.dashboardContent": "You have successfully created your first app.\n\nAt the dashboard we provide statistics about the data usage of your app.",
"tour.createApp.description": "The place for all contents, assets, rules and settings.",
"tour.createApp.formContent": "The name is used to identify your app and can therefore not be changed.",
"tour.createApp.title": "Create your first App",
"tour.createAsset.filterContent": "Expand the filter pane to see all available tags and to query assets based on tags.",
"tour.createAsset.navigateContent": "Go to the **Assets** section.",
"tour.createAsset.title": "Upload an Asset",
"tour.createAsset.uploadContent": "Choose an asset from your local machine to upload it to Squidex.\n\nImages will be processed to remove all metadata.",
"tour.createContent.addContent": "Click the **New** button to move to the content editor.",
"tour.createContent.description": "Use the editor to fill your app with published content.",
"tour.createContent.navigateContent": "Navigate to **Content**.",
"tour.createContent.saveContent": "First, fill in your fields.\n\nThen **Save and Publish** your content item.\n\nOnly published content items are available via the API.",
"tour.createContent.selectSchemaContent": "Select the schema, that you have created in the previous step.",
"tour.createContent.statusContent": "If you want to unpublish your content item or change the status in general, you can use this button.",
"tour.createContent.title": "Publish a Content Item",
"tour.createSchema.addContent": "Click the **Plus** button to open the form for new schemas.",
"tour.createSchema.addFieldContent": "Click this button to add a new field.\n\nNext time you can also use the big, round button at the bottom right of the screen.",
"tour.createSchema.description": "Define the structure of your content items.",
"tour.createSchema.fieldFormContent": "Add a new **Field** to your schema.\n\nPlease add a String field to keep it simple. You can add more fields later.",
"tour.createSchema.formContent": "Create a new **schema**.\n\nWe will add fields in the next step.\n\nPlease ensure that you select **Multiple contents**, so that you can add multiple content items.",
"tour.createSchema.helpContent": "Most pages have a help section with some basic information. This is useful for advanced concepts, but does not replace the documentation page.",
"tour.createSchema.historyContent": "Squidex tracks all changes and provides a history of previous updates and modifications.",
"tour.createSchema.navigateContent": "Navigate to the **Schemas** section.",
"tour.createSchema.publishContent": "**Publish** the schema by clicking on the **Published** button.\n\nThis schema cannot be used yet. Before you can add content items, you have to publish it to make it available to content editors.",
"tour.createSchema.schemaFieldContent": "The field has been added to your schema. You can make changes at anytime. Just press the **gear icon** at the right.",
"tour.createSchema.title": "Add a Schema",
"tour.documentation": "Documentation",
"tour.general.selectAppContent": "Start by selecting your **App**",
"tour.guide-title": "Onboarding Guide",
"tour.hint": "Do you know?",
"tour.projectBackend": "Backend Data Management",
"tour.projectCommerce": "E-Commerce Solution",
"tour.projectLearning": "None, I am just here for learning",
@ -1084,12 +1118,8 @@
"tour.sizeSmall": "2-10",
"tour.sizeVeryLarge": "1000+",
"tour.skip": "Tour overslaan",
"tour.stepAppNext": "Doorgaan",
"tour.stepAppText": "Een app is de opslagplaats voor uw project, bijvoorbeeld (blog, webshop of mobiele app). Je kunt bijdragers aan uw app toewijzen om samen te werken. \n \n Je kunt een onbeperkt aantal apps maken in Squidex om meerdere projecten tegelijkertijd te beheren. ",
"tour.stepAssetsNext": "Begrepen!",
"tour.stepAssetsText": "De mappen bevatten alle bestanden die ook aan uw inhoud kunnen worden gekoppeld. Bijvoorbeeld afbeeldingen, video's of documenten. \n \n Je kunt de bestanden hier uploaden en later gebruiken of ze direct uploaden wanneer je een nieuw contentitem met een bestandveld maakt. ",
"tour.stepContentNext": "Bijna klaar!",
"tour.stepContentText": "Inhoud zijn de feitelijke gegevens in uw app die zijn gegroepeerd op basis van het schema. \n \n Selecteer eerst een gepubliceerd schema en voeg vervolgens inhoud toe voor dit schema.",
"tour.startNo": "No, Thanks",
"tour.startYes": "Yes, Please",
"tour.stepDataCompanyRole": "What is your role?",
"tour.stepDataCompanySize": "How many people work in a company?",
"tour.stepDataNext": "Continue",
@ -1098,10 +1128,14 @@
"tour.stepDataTitle": "About You",
"tour.stepIntroNext": "Laten we eens rondkijken",
"tour.stepIntroText": "Je kunt direct beginnen met het beheren en distribueren van uw inhoud, maar we willen je graag eerst wat basisbeginselen laten zien ... \n \n Hoe dat",
"tour.stepOutroText": "Maar dat is niet alle ondersteuning die we kunnen bieden. \n \n Je kunt naar https://docs.squidex.io gaan om meer te lezen. \n \n Wil je lid worden van onze community ? ",
"tour.stepOutroTitle": "Geweldig, nu ken je de basis!",
"tour.stepSchemasNext": "Ga door!",
"tour.stepSchemasText": "Schema's bepalen de structuur van uw inhoud, de velden en de gegevenstypen van een inhoudsitem. \n \n Voordat je inhoud aan uw schema kunt toevoegen, moet je op de knop 'Publiceren' bovenaan klikken om het schema beschikbaar te maken voor uw inhoudappSettings.editors. ",
"tour.stepTourText": "We have prepared an interactive tour, where you learn the basics of Squidex. You can cancel it anytime. Do you want to start the tour?",
"tour.stepTourTitle": "Interactive Tour",
"tour.support": "Support",
"tour.testGraphQL.description": "Complete the tour and check out the GraphQL playground.",
"tour.testGraphQL.navigateContent": "Navigate to the **API** section.",
"tour.testGraphQL.navigateGraphQLContent": "Navigate to the **GraphQL** page.",
"tour.testGraphQL.queryContent": "This is the **GraphQL** playground. You can explore your API and make test queries, before you implement them later in your application.",
"tour.testGraphQL.title": "Make a test query",
"tour.tooltipConfirm": "Begrepen",
"tour.tooltipStop": "Tour stoppen",
"tour.welcome": "Welkom bij",

70
backend/i18n/frontend_pt.json

@ -173,6 +173,7 @@
"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",
@ -316,6 +317,7 @@
"common.field": "Campo",
"common.files": "Ficheiros",
"common.filters": "Filtros",
"common.finish": "Finish",
"common.folder": "Pasta",
"common.folders": "Pastas",
"common.from": "De",
@ -348,6 +350,7 @@
"common.monthly": "Mensal",
"common.more": "Mais",
"common.name": "Nome",
"common.next": "Continue",
"common.no": "Não",
"common.nothingChanged": "Nada foi mudado.",
"common.notSupported": "Não suportado",
@ -363,6 +366,7 @@
"common.patterns": "Padrões",
"common.permission": "Permission",
"common.permissions": "Permissões",
"common.prev": "Back",
"common.preview": "Previsualizar",
"common.product": "CMS Headless Squidex",
"common.project": "Projeto",
@ -533,7 +537,6 @@
"contents.updated": "Conteúdo atualizado com sucesso.",
"contents.updateFailed": "Falhou na atualização do conteúdo. Por favor, recarregue.",
"contents.validate": "Validar",
"contents.validationHint": "Por favor, lembre-se de verificar todos os idiomas quando vir erros de validação.",
"contents.versionCompare": "Comparar",
"contents.versionDelete": "Excluir esta versão",
"contents.versionViewing": "Versão de visualização **{version}***",
@ -644,7 +647,6 @@
"news.headline": "O que há de novo?",
"news.title": "Novas Funcionalidades",
"notifications.empty": "Ainda não há notificações",
"notifo.subscripeTooltip": "Clique neste botão para subscrever todas as alterações e para receber notificações push.",
"plans.allApps": "A subscrição é partilhada entre todas as aplicações desta equipa. Verifique o painel de instrumentos para as aplicações atribuídas.",
"plans.billingPortal": "Portal de Faturação",
"plans.billingPortalHint": "Vá ao Portal de Faturação para obter o histórico de pagamentos e a visão geral da subscrição.",
@ -805,7 +807,6 @@
"schemas.contentSidebarUrlHint": "URL para o plugin para a barra lateral na vista de detalhes.",
"schemas.contentsSidebarUrl": "Extensão da barra lateral de conteúdo",
"schemas.contentsSidebarUrlHint": "URL para o plugin para a barra lateral na vista da lista.",
"schemas.contextMenuTour": "Abra o menu de contexto para eliminar o esquema ou para criar alguns scripts para alterações de conteúdo.",
"schemas.create": "Criar Esquema",
"schemas.createCategory": "Criar nova categoria...",
"schemas.createFailed": "Falhou em criar esquema. Por favor, recarregue.",
@ -962,7 +963,6 @@
"schemas.previewUrls.title": "URLs de pré-visualização",
"schemas.previewUrls.urlPlaceholder": "URL com variáveis",
"schemas.published": "Publicado",
"schemas.publishedTour": "Note que tem de publicar o esquema antes de poder adicionar conteúdo.",
"schemas.publishFailed": "Falhou em publicar o esquema. Por favor, recarregue.",
"schemas.referenceFields": "Campos de Referência",
"schemas.referenceFieldsEmpty": "Deixe cair aqui ou reencomenda-os para mostrar os campos quando referenciados por outro conteúdo. Quando não é definido nenhum campo de referência, os campos de lista são utilizados.",
@ -1029,9 +1029,7 @@
"search.addFilter": "Adicionar filtro",
"search.addGroup": "Adicionar Grupo",
"search.addSorting": "Adicionar Classificação",
"search.advancedTour": "Clique neste ícone para mostrar o menu de pesquisa avançado!",
"search.customQuery": "Consulta personalizada",
"search.fullTextTour": "Pes para obter conteúdos com textos completos para todos os campos e para os direitos de texto.",
"search.help": "Ler mais sobre filtros em [Documentação](https://docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "Minhas consultas",
"search.nameQuery": "Diga o seu nome",
@ -1063,8 +1061,44 @@
"templates.loadFailed": "Falhou em carregar modelos. Por favor, recarregue.",
"templates.refreshTooltip": "Modelos de atualização",
"templates.reloaded": "Modelos recarregados.",
"tour.joinForum": "Junte-se ao nosso Fórum",
"tour.joinGithub": "Junte-se a nós em Github",
"tour.checkClient.clientContent": "We have created a **Client** for you.\n\nA Client is like an API Key and the ID and secret are used to request access tokens to call the API.",
"tour.checkClient.connectContent": "Click to **Connect** button and follow the guided form to learn how to use the client.",
"tour.checkClient.description": "Learn how to connect your App with your code to query content items.",
"tour.checkClient.navigateClientsContent": "Navigate to the **Clients** page.",
"tour.checkClient.navigateContent": "Navigate to the **Settings** section.",
"tour.checkClient.title": "Connect your Code",
"tour.complete": "Complete the Tour",
"tour.createApp.buttonContent": "In this tour we start with an empty app. We also provide ready to use sample apps.",
"tour.createApp.dashboardContent": "You have successfully created your first app.\n\nAt the dashboard we provide statistics about the data usage of your app.",
"tour.createApp.description": "The place for all contents, assets, rules and settings.",
"tour.createApp.formContent": "The name is used to identify your app and can therefore not be changed.",
"tour.createApp.title": "Create your first App",
"tour.createAsset.filterContent": "Expand the filter pane to see all available tags and to query assets based on tags.",
"tour.createAsset.navigateContent": "Go to the **Assets** section.",
"tour.createAsset.title": "Upload an Asset",
"tour.createAsset.uploadContent": "Choose an asset from your local machine to upload it to Squidex.\n\nImages will be processed to remove all metadata.",
"tour.createContent.addContent": "Click the **New** button to move to the content editor.",
"tour.createContent.description": "Use the editor to fill your app with published content.",
"tour.createContent.navigateContent": "Navigate to **Content**.",
"tour.createContent.saveContent": "First, fill in your fields.\n\nThen **Save and Publish** your content item.\n\nOnly published content items are available via the API.",
"tour.createContent.selectSchemaContent": "Select the schema, that you have created in the previous step.",
"tour.createContent.statusContent": "If you want to unpublish your content item or change the status in general, you can use this button.",
"tour.createContent.title": "Publish a Content Item",
"tour.createSchema.addContent": "Click the **Plus** button to open the form for new schemas.",
"tour.createSchema.addFieldContent": "Click this button to add a new field.\n\nNext time you can also use the big, round button at the bottom right of the screen.",
"tour.createSchema.description": "Define the structure of your content items.",
"tour.createSchema.fieldFormContent": "Add a new **Field** to your schema.\n\nPlease add a String field to keep it simple. You can add more fields later.",
"tour.createSchema.formContent": "Create a new **schema**.\n\nWe will add fields in the next step.\n\nPlease ensure that you select **Multiple contents**, so that you can add multiple content items.",
"tour.createSchema.helpContent": "Most pages have a help section with some basic information. This is useful for advanced concepts, but does not replace the documentation page.",
"tour.createSchema.historyContent": "Squidex tracks all changes and provides a history of previous updates and modifications.",
"tour.createSchema.navigateContent": "Navigate to the **Schemas** section.",
"tour.createSchema.publishContent": "**Publish** the schema by clicking on the **Published** button.\n\nThis schema cannot be used yet. Before you can add content items, you have to publish it to make it available to content editors.",
"tour.createSchema.schemaFieldContent": "The field has been added to your schema. You can make changes at anytime. Just press the **gear icon** at the right.",
"tour.createSchema.title": "Add a Schema",
"tour.documentation": "Documentation",
"tour.general.selectAppContent": "Start by selecting your **App**",
"tour.guide-title": "Onboarding Guide",
"tour.hint": "Do you know?",
"tour.projectBackend": "Backend Data Management",
"tour.projectCommerce": "E-Commerce Solution",
"tour.projectLearning": "None, I am just here for learning",
@ -1084,12 +1118,8 @@
"tour.sizeSmall": "2-10",
"tour.sizeVeryLarge": "1000+",
"tour.skip": "Tour de salto",
"tour.stepAppNext": "Continuar",
"tour.stepAppText": "Uma App é o repositório do seu projeto, por exemplo (blog, web shop ou aplicativo móvel). Pode atribuir colaboradores à sua aplicação para trabalharem em conjunto.\n\nPode criar um número ilimitado de Apps em Squidex para gerir vários projetos ao mesmo tempo.",
"tour.stepAssetsNext": "Apanhei!",
"tour.stepAssetsText": "Os ativos contêm todos os ficheiros que também podem estar ligados ao seu conteúdo. Por exemplo, imagens, vídeos ou documentos.\n\nPode fazer o upload dos ativos aqui e usá-los mais tarde ou também carregá-los diretamente quando criar um novo item de conteúdo com um campo de ativos.",
"tour.stepContentNext": "Está quase!",
"tour.stepContentText": "O conteúdo é o dado real da sua app que é agrupado pelo esquema.\n\nSelecione primeiro um esquema publicado e, em seguida, adicione conteúdo para este esquema.",
"tour.startNo": "No, Thanks",
"tour.startYes": "Yes, Please",
"tour.stepDataCompanyRole": "What is your role?",
"tour.stepDataCompanySize": "How many people work in a company?",
"tour.stepDataNext": "Continue",
@ -1098,10 +1128,14 @@
"tour.stepDataTitle": "About You",
"tour.stepIntroNext": "Vamos dar uma vista de olhos.",
"tour.stepIntroText": "Pode começar a gerir e distribuir o seu conteúdo imediatamente, mas gostaríamos de o acompanhar primeiro...\n\nComo é que",
"tour.stepOutroText": "Mas não é todo o apoio que podemos dar.\n\nPode ir a https://docs.squidex.io ler mais.\n\nQuer se juntar à nossa comunidade?",
"tour.stepOutroTitle": "Incrível, agora já sabes o básico!",
"tour.stepSchemasNext": "Continua!",
"tour.stepSchemasText": "Os esquemas definem a estrutura do seu conteúdo, os campos e os tipos de dados de um item de conteúdo.\n\nAntes de poder adicionar conteúdo ao seu esquema, certifique-se de carregar no botão 'Publicar' no topo para disponibilizar o esquema para as suas aplicações de conteúdoSettings.editores.",
"tour.stepTourText": "We have prepared an interactive tour, where you learn the basics of Squidex. You can cancel it anytime. Do you want to start the tour?",
"tour.stepTourTitle": "Interactive Tour",
"tour.support": "Support",
"tour.testGraphQL.description": "Complete the tour and check out the GraphQL playground.",
"tour.testGraphQL.navigateContent": "Navigate to the **API** section.",
"tour.testGraphQL.navigateGraphQLContent": "Navigate to the **GraphQL** page.",
"tour.testGraphQL.queryContent": "This is the **GraphQL** playground. You can explore your API and make test queries, before you implement them later in your application.",
"tour.testGraphQL.title": "Make a test query",
"tour.tooltipConfirm": "Entendi",
"tour.tooltipStop": "Terminar Tour",
"tour.welcome": "Bem vindo a",

70
backend/i18n/frontend_zh.json

@ -173,6 +173,7 @@
"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",
@ -316,6 +317,7 @@
"common.field": "字段",
"common.files": "文件",
"common.filters": "过滤器",
"common.finish": "Finish",
"common.folder": "文件夹",
"common.folders": "文件夹",
"common.from": "From",
@ -348,6 +350,7 @@
"common.monthly": "Monthly",
"common.more": "More",
"common.name": "名称",
"common.next": "Continue",
"common.no": "不",
"common.nothingChanged": "什么都没有改变。",
"common.notSupported": "Not Supported",
@ -363,6 +366,7 @@
"common.patterns": "模式",
"common.permission": "Permission",
"common.permissions": "权限",
"common.prev": "Back",
"common.preview": "预览",
"common.product": "Squidex Headless CMS",
"common.project": "项目",
@ -533,7 +537,6 @@
"contents.updated": "内容更新成功。",
"contents.updateFailed": "更新内容失败,请重新加载。",
"contents.validate": "验证",
"contents.validationHint": "当您看到验证错误时,请记住检查所有语言。",
"contents.versionCompare": "比较",
"contents.versionDelete": "删除此版本",
"contents.versionViewing": "查看版本**{version}**。",
@ -644,7 +647,6 @@
"news.headline": "有什么新鲜事?",
"news.title": "新功能",
"notifications.empty": "No notifications yet",
"notifo.subscripeTooltip": "单击此按钮可订阅所有更改并接收推送通知。",
"plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.",
"plans.billingPortal": "计费门户",
"plans.billingPortalHint": "前往账单门户查看付款历史和订阅概览。",
@ -805,7 +807,6 @@
"schemas.contentSidebarUrlHint": "详细信息视图中侧边栏插件的 URL。",
"schemas.contentsSidebarUrl": "内容侧边栏扩展",
"schemas.contentsSidebarUrlHint": "列表视图中侧边栏插件的 URL。",
"schemas.contextMenuTour": "打开上下文菜单以删除Schemas或为内容更改创建一些脚本。",
"schemas.create": "Create Schema",
"schemas.createCategory": "创建新类别...",
"schemas.createFailed": "无法创建Schemas。请重新加载。",
@ -962,7 +963,6 @@
"schemas.previewUrls.title": "预览网址",
"schemas.previewUrls.urlPlaceholder": "带变量的 URL",
"schemas.published": "Published",
"schemas.publishedTour": "请注意,您必须先发布Schemas,然后才能向其中添加内容。",
"schemas.publishFailed": "无法发布Schemas。请重新加载。",
"schemas.referenceFields": "参考字段",
"schemas.referenceFieldsEmpty": "将字段拖放到此处或重新排序以在被其他内容引用时显示字段。当未定义引用字段时,将使用列表字段代替。",
@ -1029,9 +1029,7 @@
"search.addFilter": "添加过滤器",
"search.addGroup": "添加组",
"search.addSorting": "添加排序",
"search.advancedTour": "单击此图标可显示高级搜索菜单!",
"search.customQuery": "自定义查询",
"search.fullTextTour": "使用全文搜索在所有领域和语言中搜索内容!",
"search.help": "在 [文档](https://docs.squidex.io/04-guides/02-api.html) 中阅读有关过滤的更多信息。",
"search.myQueries": "我的查询",
"search.nameQuery": "命名查询",
@ -1063,8 +1061,44 @@
"templates.loadFailed": "Failed to load templates. Please reload.",
"templates.refreshTooltip": "Refresh Templates",
"templates.reloaded": "Templates reloaded.",
"tour.joinForum": "加入我们的论坛",
"tour.joinGithub": "加入我们的 Github",
"tour.checkClient.clientContent": "We have created a **Client** for you.\n\nA Client is like an API Key and the ID and secret are used to request access tokens to call the API.",
"tour.checkClient.connectContent": "Click to **Connect** button and follow the guided form to learn how to use the client.",
"tour.checkClient.description": "Learn how to connect your App with your code to query content items.",
"tour.checkClient.navigateClientsContent": "Navigate to the **Clients** page.",
"tour.checkClient.navigateContent": "Navigate to the **Settings** section.",
"tour.checkClient.title": "Connect your Code",
"tour.complete": "Complete the Tour",
"tour.createApp.buttonContent": "In this tour we start with an empty app. We also provide ready to use sample apps.",
"tour.createApp.dashboardContent": "You have successfully created your first app.\n\nAt the dashboard we provide statistics about the data usage of your app.",
"tour.createApp.description": "The place for all contents, assets, rules and settings.",
"tour.createApp.formContent": "The name is used to identify your app and can therefore not be changed.",
"tour.createApp.title": "Create your first App",
"tour.createAsset.filterContent": "Expand the filter pane to see all available tags and to query assets based on tags.",
"tour.createAsset.navigateContent": "Go to the **Assets** section.",
"tour.createAsset.title": "Upload an Asset",
"tour.createAsset.uploadContent": "Choose an asset from your local machine to upload it to Squidex.\n\nImages will be processed to remove all metadata.",
"tour.createContent.addContent": "Click the **New** button to move to the content editor.",
"tour.createContent.description": "Use the editor to fill your app with published content.",
"tour.createContent.navigateContent": "Navigate to **Content**.",
"tour.createContent.saveContent": "First, fill in your fields.\n\nThen **Save and Publish** your content item.\n\nOnly published content items are available via the API.",
"tour.createContent.selectSchemaContent": "Select the schema, that you have created in the previous step.",
"tour.createContent.statusContent": "If you want to unpublish your content item or change the status in general, you can use this button.",
"tour.createContent.title": "Publish a Content Item",
"tour.createSchema.addContent": "Click the **Plus** button to open the form for new schemas.",
"tour.createSchema.addFieldContent": "Click this button to add a new field.\n\nNext time you can also use the big, round button at the bottom right of the screen.",
"tour.createSchema.description": "Define the structure of your content items.",
"tour.createSchema.fieldFormContent": "Add a new **Field** to your schema.\n\nPlease add a String field to keep it simple. You can add more fields later.",
"tour.createSchema.formContent": "Create a new **schema**.\n\nWe will add fields in the next step.\n\nPlease ensure that you select **Multiple contents**, so that you can add multiple content items.",
"tour.createSchema.helpContent": "Most pages have a help section with some basic information. This is useful for advanced concepts, but does not replace the documentation page.",
"tour.createSchema.historyContent": "Squidex tracks all changes and provides a history of previous updates and modifications.",
"tour.createSchema.navigateContent": "Navigate to the **Schemas** section.",
"tour.createSchema.publishContent": "**Publish** the schema by clicking on the **Published** button.\n\nThis schema cannot be used yet. Before you can add content items, you have to publish it to make it available to content editors.",
"tour.createSchema.schemaFieldContent": "The field has been added to your schema. You can make changes at anytime. Just press the **gear icon** at the right.",
"tour.createSchema.title": "Add a Schema",
"tour.documentation": "Documentation",
"tour.general.selectAppContent": "Start by selecting your **App**",
"tour.guide-title": "Onboarding Guide",
"tour.hint": "Do you know?",
"tour.projectBackend": "Backend Data Management",
"tour.projectCommerce": "E-Commerce Solution",
"tour.projectLearning": "None, I am just here for learning",
@ -1084,12 +1118,8 @@
"tour.sizeSmall": "2-10",
"tour.sizeVeryLarge": "1000+",
"tour.skip": "跳过游览",
"tour.stepAppNext": "继续",
"tour.stepAppText": "应用程序是您项目的存储库,例如(博客、网上商店或移动应用程序)。您可以为您的应用程序分配贡献者以协同工作。\n\n您可以在其中创建无限数量的应用程序Squidex 同时管理多个项目。",
"tour.stepAssetsNext": "知道了!",
"tour.stepAssetsText": "资源包含所有也可以链接到您的内容的文件。例如图像、视频或文档。\n\n您可以在此处上传资源供以后使用,也可以在创建时直接上传带有资源字段的新内容项。",
"tour.stepContentNext": "快到了!",
"tour.stepContentText": "内容是您的应用程序中按Schemas分组的实际数据。\n\n首先选择一个已发布的Schemas,然后为此Schemas添加内容。",
"tour.startNo": "No, Thanks",
"tour.startYes": "Yes, Please",
"tour.stepDataCompanyRole": "What is your role?",
"tour.stepDataCompanySize": "How many people work in a company?",
"tour.stepDataNext": "Continue",
@ -1098,10 +1128,14 @@
"tour.stepDataTitle": "About You",
"tour.stepIntroNext": "我们看看周围",
"tour.stepIntroText": "您可以立即开始管理和分发您的内容,但我们想先向您介绍一些基础知识...\n\n如何",
"tour.stepOutroText": "但这还不是我们可以提供的全部支持。\n\n您可以访问 https://docs.squidex.io 阅读更多信息。\n\n您想加入我们的社区吗? ?",
"tour.stepOutroTitle": "太棒了,现在你知道基础了!",
"tour.stepSchemasNext": "继续!",
"tour.stepSchemasText": "Schemas定义内容的结构、内容项的字段和数据类型。\n\n在向Schemas添加内容之前,请确保点击顶部的“发布”按钮使Schemas可用于您的内容 appSettings.editors。",
"tour.stepTourText": "We have prepared an interactive tour, where you learn the basics of Squidex. You can cancel it anytime. Do you want to start the tour?",
"tour.stepTourTitle": "Interactive Tour",
"tour.support": "Support",
"tour.testGraphQL.description": "Complete the tour and check out the GraphQL playground.",
"tour.testGraphQL.navigateContent": "Navigate to the **API** section.",
"tour.testGraphQL.navigateGraphQLContent": "Navigate to the **GraphQL** page.",
"tour.testGraphQL.queryContent": "This is the **GraphQL** playground. You can explore your API and make test queries, before you implement them later in your application.",
"tour.testGraphQL.title": "Make a test query",
"tour.tooltipConfirm": "知道了",
"tour.tooltipStop": "停止游览",
"tour.welcome": "欢迎来到",

70
backend/i18n/source/frontend_en.json

@ -173,6 +173,7 @@
"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",
@ -316,6 +317,7 @@
"common.field": "Field",
"common.files": "Files",
"common.filters": "Filters",
"common.finish": "Finish",
"common.folder": "Folder",
"common.folders": "Folders",
"common.from": "From",
@ -348,6 +350,7 @@
"common.monthly": "Monthly",
"common.more": "More",
"common.name": "Name",
"common.next": "Continue",
"common.no": "No",
"common.nothingChanged": "Nothing has been changed.",
"common.notSupported": "Not Supported",
@ -363,6 +366,7 @@
"common.patterns": "Patterns",
"common.permission": "Permission",
"common.permissions": "Permissions",
"common.prev": "Back",
"common.preview": "Preview",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
@ -533,7 +537,6 @@
"contents.updated": "Content updated successfully.",
"contents.updateFailed": "Failed to update content. Please reload.",
"contents.validate": "Validate",
"contents.validationHint": "Please remember to check all languages when you see validation errors.",
"contents.versionCompare": "Compare",
"contents.versionDelete": "Delete this Version",
"contents.versionViewing": "Viewing version **{version}**.",
@ -644,7 +647,6 @@
"news.headline": "What's new?",
"news.title": "New Features",
"notifications.empty": "No notifications yet",
"notifo.subscripeTooltip": "Click this button to subscribe to all changes and to receive push notifications.",
"plans.allApps": "The subscription is shared between all apps of this team. Check the dashboard for the assigned apps.",
"plans.billingPortal": "Billing Portal",
"plans.billingPortalHint": "Go to Billing Portal for payment history and subscription overview.",
@ -805,7 +807,6 @@
"schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.",
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.contextMenuTour": "Open the context menu to delete the schema or to create some scripts for content changes.",
"schemas.create": "Create Schema",
"schemas.createCategory": "Create new category...",
"schemas.createFailed": "Failed to create schema. Please reload.",
@ -962,7 +963,6 @@
"schemas.previewUrls.title": "Preview URLs",
"schemas.previewUrls.urlPlaceholder": "URL with variables",
"schemas.published": "Published",
"schemas.publishedTour": "Note, that you have to publish the schema before you can add content to it.",
"schemas.publishFailed": "Failed to publish schema. Please reload.",
"schemas.referenceFields": "Reference Fields",
"schemas.referenceFieldsEmpty": "Drop field here or reorder them to show the fields when referenced by another content. When no reference field is defined, the list fields are used instead.",
@ -1029,9 +1029,7 @@
"search.addFilter": "Add Filter",
"search.addGroup": "Add Group",
"search.addSorting": "Add Sorting",
"search.advancedTour": "Click this icon to show the advanced search menu!",
"search.customQuery": "Custom Query",
"search.fullTextTour": "Search for content using full text search over all fields and languages!",
"search.help": "Read more about filtering in the [Documentation](https://docs.squidex.io/04-guides/02-api.html).",
"search.myQueries": "My queries",
"search.nameQuery": "Name your query",
@ -1063,8 +1061,44 @@
"templates.loadFailed": "Failed to load templates. Please reload.",
"templates.refreshTooltip": "Refresh Templates",
"templates.reloaded": "Templates reloaded.",
"tour.joinForum": "Join our Forum",
"tour.joinGithub": "Join us on Github",
"tour.checkClient.clientContent": "We have created a **Client** for you.\n\nA Client is like an API Key and the ID and secret are used to request access tokens to call the API.",
"tour.checkClient.connectContent": "Click to **Connect** button and follow the guided form to learn how to use the client.",
"tour.checkClient.description": "Learn how to connect your App with your code to query content items.",
"tour.checkClient.navigateClientsContent": "Navigate to the **Clients** page.",
"tour.checkClient.navigateContent": "Navigate to the **Settings** section.",
"tour.checkClient.title": "Connect your Code",
"tour.complete": "Complete the Tour",
"tour.createApp.buttonContent": "In this tour we start with an empty app. We also provide ready to use sample apps.",
"tour.createApp.dashboardContent": "You have successfully created your first app.\n\nAt the dashboard we provide statistics about the data usage of your app.",
"tour.createApp.description": "The place for all contents, assets, rules and settings.",
"tour.createApp.formContent": "The name is used to identify your app and can therefore not be changed.",
"tour.createApp.title": "Create your first App",
"tour.createAsset.filterContent": "Expand the filter pane to see all available tags and to query assets based on tags.",
"tour.createAsset.navigateContent": "Go to the **Assets** section.",
"tour.createAsset.title": "Upload an Asset",
"tour.createAsset.uploadContent": "Choose an asset from your local machine to upload it to Squidex.\n\nImages will be processed to remove all metadata.",
"tour.createContent.addContent": "Click the **New** button to move to the content editor.",
"tour.createContent.description": "Use the editor to fill your app with published content.",
"tour.createContent.navigateContent": "Navigate to **Content**.",
"tour.createContent.saveContent": "First, fill in your fields.\n\nThen **Save and Publish** your content item.\n\nOnly published content items are available via the API.",
"tour.createContent.selectSchemaContent": "Select the schema, that you have created in the previous step.",
"tour.createContent.statusContent": "If you want to unpublish your content item or change the status in general, you can use this button.",
"tour.createContent.title": "Publish a Content Item",
"tour.createSchema.addContent": "Click the **Plus** button to open the form for new schemas.",
"tour.createSchema.addFieldContent": "Click this button to add a new field.\n\nNext time you can also use the big, round button at the bottom right of the screen.",
"tour.createSchema.description": "Define the structure of your content items.",
"tour.createSchema.fieldFormContent": "Add a new **Field** to your schema.\n\nPlease add a String field to keep it simple. You can add more fields later.",
"tour.createSchema.formContent": "Create a new **schema**.\n\nWe will add fields in the next step.\n\nPlease ensure that you select **Multiple contents**, so that you can add multiple content items.",
"tour.createSchema.helpContent": "Most pages have a help section with some basic information. This is useful for advanced concepts, but does not replace the documentation page.",
"tour.createSchema.historyContent": "Squidex tracks all changes and provides a history of previous updates and modifications.",
"tour.createSchema.navigateContent": "Navigate to the **Schemas** section.",
"tour.createSchema.publishContent": "**Publish** the schema by clicking on the **Published** button.\n\nThis schema cannot be used yet. Before you can add content items, you have to publish it to make it available to content editors.",
"tour.createSchema.schemaFieldContent": "The field has been added to your schema. You can make changes at anytime. Just press the **gear icon** at the right.",
"tour.createSchema.title": "Add a Schema",
"tour.documentation": "Documentation",
"tour.general.selectAppContent": "Start by selecting your **App**",
"tour.guide-title": "Onboarding Guide",
"tour.hint": "Do you know?",
"tour.projectBackend": "Backend Data Management",
"tour.projectCommerce": "E-Commerce Solution",
"tour.projectLearning": "None, I am just here for learning",
@ -1084,12 +1118,8 @@
"tour.sizeSmall": "2-10",
"tour.sizeVeryLarge": "1000+",
"tour.skip": "Skip Tour",
"tour.stepAppNext": "Continue",
"tour.stepAppText": "An app is the repository for your project (for example a blog, web shop or mobile app). You can assign contributors to your app to work together.\n\nCreate an unlimited number of apps in Squidex to manage multiple projects at the same time.",
"tour.stepAssetsNext": "Got It!",
"tour.stepAssetsText": "The assets contains all files that can also be linked to your content. For example images, videos or documents.\n\nYou can upload the assets here and use them later or also upload them directly when you create a new content item with an asset field.",
"tour.stepContentNext": "Almost there!",
"tour.stepContentText": "Content is the actual data in your app which is grouped by the schema.\n\nSelect a published schema first, then add content for this schema.",
"tour.startNo": "No, Thanks",
"tour.startYes": "Yes, Please",
"tour.stepDataCompanyRole": "What is your role?",
"tour.stepDataCompanySize": "How many people work in a company?",
"tour.stepDataNext": "Continue",
@ -1098,10 +1128,14 @@
"tour.stepDataTitle": "About You",
"tour.stepIntroNext": "Let's take a look around",
"tour.stepIntroText": "You can start managing and distributing your content right away, but we we'd like to walk you through some basics first...\n\n",
"tour.stepOutroText": "But that's not all of the support we can provide.\n\nYou can go to https://docs.squidex.io to read more.\n\nDo you want to join our community?",
"tour.stepOutroTitle": "Awesome, now you know the basics!",
"tour.stepSchemasNext": "Keep going!",
"tour.stepSchemasText": "Schemas define the structure of your content, the fields and the data types of a content item.\n\nBefore you can add content to your schema, make sure to hit the publish button at the top to make the schema available to your content editors.",
"tour.stepTourText": "We have prepared an interactive tour, where you learn the basics of Squidex. You can cancel it anytime. Do you want to start the tour?",
"tour.stepTourTitle": "Interactive Tour",
"tour.support": "Support",
"tour.testGraphQL.description": "Complete the tour and check out the GraphQL playground.",
"tour.testGraphQL.navigateContent": "Navigate to the **API** section.",
"tour.testGraphQL.navigateGraphQLContent": "Navigate to the **GraphQL** page.",
"tour.testGraphQL.queryContent": "This is the **GraphQL** playground. You can explore your API and make test queries, before you implement them later in your application.",
"tour.testGraphQL.title": "Make a test query",
"tour.tooltipConfirm": "Got It",
"tour.tooltipStop": "Stop Tour",
"tour.welcome": "Welcome to",

2
backend/i18n/source/frontend_fr.json

@ -1039,8 +1039,6 @@
"templates.loadFailed": "Échec du chargement des modèles. Veuillez recharger.",
"templates.refreshTooltip": "Actualiser les modèles",
"templates.reloaded": "Modèles rechargés.",
"tour.joinForum": "Rejoignez notre forum",
"tour.joinGithub": "Rejoignez-nous sur Github",
"tour.skip": "Passer le tour",
"tour.stepAppNext": "Continuer",
"tour.stepAppText": "Une application est le référentiel de votre projet, par exemple (blog, boutique en ligne ou application mobile). Vous pouvez affecter des contributeurs à votre application pour travailler ensemble. Vous pouvez créer un nombre illimité d'applications dans Squidex pour gérer plusieurs projets en même temps.",

2
backend/i18n/source/frontend_it.json

@ -812,8 +812,6 @@
"start.loginHint": "Il pulsante per accedere aprirà un popup. Una volta effettuato l'accesso sarai indirizzato al portale per la gestione di Squidex.",
"start.madeBy": "Realizzato con orgoglio da",
"start.madeByCopyright": "Sebastian Stehle e Collaboratori, 2016-2020",
"tour.joinForum": "Unisciti al nostro Forum",
"tour.joinGithub": "Unisciti a Github",
"tour.skip": "Salta il Tour",
"tour.stepAppNext": "Continua",
"tour.stepAppText": "Un'App è il repository per il tuo progetto, ad es. (un blog, un negozio sul web o una app per dispositivi mobili). Puoi assegnare collaboratori alla tua App per lavorare insieme.\n\nPuoi creare un numero illimitato di app in Squidex per gestire più progetti contemporaneamente.",

2
backend/i18n/source/frontend_nl.json

@ -934,8 +934,6 @@
"start.loginHint": "De login-knop opent een nieuwe pop-up. Zodra je succesvol bent ingelogd, zullen we je doorverwijzen naar het Squidex beheerportaal.",
"start.madeBy": "Met trots gemaakt door",
"start.madeByCopyright": "Sebastian Stehle en medewerkers, 2016-2020",
"tour.joinForum": "Word lid van ons forum",
"tour.joinGithub": "Bezoek ons ​​op Github",
"tour.skip": "Tour overslaan",
"tour.stepAppNext": "Doorgaan",
"tour.stepAppText": "Een app is de opslagplaats voor uw project, bijvoorbeeld (blog, webshop of mobiele app). Je kunt bijdragers aan uw app toewijzen om samen te werken. \n \n Je kunt een onbeperkt aantal apps maken in Squidex om meerdere projecten tegelijkertijd te beheren. ",

2
backend/i18n/source/frontend_pt.json

@ -1010,8 +1010,6 @@
"templates.loadFailed": "Falhou em carregar modelos. Por favor, recarregue.",
"templates.refreshTooltip": "Modelos de atualização",
"templates.reloaded": "Modelos recarregados.",
"tour.joinForum": "Junte-se ao nosso Fórum",
"tour.joinGithub": "Junte-se a nós em Github",
"tour.skip": "Tour de salto",
"tour.stepAppNext": "Continuar",
"tour.stepAppText": "Uma App é o repositório do seu projeto, por exemplo (blog, web shop ou aplicativo móvel). Pode atribuir colaboradores à sua aplicação para trabalharem em conjunto.\n\nPode criar um número ilimitado de Apps em Squidex para gerir vários projetos ao mesmo tempo.",

2
backend/i18n/source/frontend_zh.json

@ -823,8 +823,6 @@
"start.loginHint": "登录按钮将打开一个新的弹出窗口。一旦您登录成功,我们会将您重定向到 Squidex 管理门户。",
"start.madeBy": "自豪地制作",
"start.madeByCopyright": "Sebastian Stehle 和贡献者,2016-2021",
"tour.joinForum": "加入我们的论坛",
"tour.joinGithub": "加入我们的 Github",
"tour.skip": "跳过游览",
"tour.stepAppNext": "继续",
"tour.stepAppText": "应用程序是您项目的存储库,例如(博客、网上商店或移动应用程序)。您可以为您的应用程序分配贡献者以协同工作。\n\n您可以在其中创建无限数量的应用程序Squidex 同时管理多个项目。",

5
backend/i18n/translator/Squidex.Translator/Commands.cs

@ -42,7 +42,7 @@ public class Commands
{
var (folder, service) = Setup(arguments, "frontend");
new CheckFrontend(folder, service).Run();
new CheckFrontend(folder, service).Run(arguments.Fix);
}
[Command(Name = "backend", Description = "Translate backend files.")]
@ -173,6 +173,9 @@ public class Commands
[Option(LongName = "report", ShortName = "r")]
public bool Report { get; set; }
[Option(LongName = "fix")]
public bool Fix { get; set; }
[Option(LongName = "locale", ShortName = "l")]
public IEnumerable<string> Locales { get; set; }

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

@ -22,7 +22,7 @@ public class CheckFrontend
this.service = service;
}
public void Run()
public void Run(bool fix)
{
var all = new HashSet<string>();
@ -35,7 +35,15 @@ public class CheckFrontend
all.Add(translation);
}
Helper.CheckForFile(service, relativeName, translations);
var notTranslated = Helper.CheckForFile(service, relativeName, translations);
if (fix)
{
foreach (var key in notTranslated)
{
service.Add(key);
}
}
}
foreach (var (file, relativeName) in Frontend.GetTypescriptFiles(folder))
@ -47,10 +55,27 @@ public class CheckFrontend
all.Add(translation);
}
Helper.CheckForFile(service, relativeName, translations);
var notTranslated = Helper.CheckForFile(service, relativeName, translations);
if (fix)
{
foreach (var key in notTranslated)
{
service.Add(key);
}
}
}
var notUsed = Helper.CheckUnused(service, all);
if (fix)
{
foreach (var key in notUsed)
{
service.Remove(key);
}
}
Helper.CheckUnused(service, all);
Helper.CheckOtherLocales(service);
service.Save();
@ -73,7 +98,8 @@ public class CheckFrontend
}
AddTranslations("\"i18n\\:(?<Key>[^\"]+)\"");
AddTranslations("'i18n\\:(?<Key>[^\']+)'");
AddTranslations("\'i18n\\:(?<Key>[^\']+)\'");
AddTranslations("'(?<Key>[^\']+)' \\| sqxTranslate");
return translations;
@ -96,6 +122,7 @@ public class CheckFrontend
}
AddTranslations("'i18n\\:(?<Key>[^\']+)'");
AddTranslations("localizer.get\\('(?<Key>[^\']+)'\\)");
AddTranslations("localizer.getOrKey\\('(?<Key>[^\']+)'\\)");

8
backend/i18n/translator/Squidex.Translator/Processes/Helper.cs

@ -88,7 +88,7 @@ public static class Helper
}
}
public static void CheckUnused(TranslationService service, HashSet<string> translations)
public static ISet<string> CheckUnused(TranslationService service, HashSet<string> translations)
{
var notUsing = new SortedSet<string>();
@ -114,9 +114,11 @@ public static class Helper
Console.WriteLine(key);
}
}
return notUsing;
}
public static void CheckForFile(TranslationService service, string relativeName, HashSet<string> translations)
public static ISet<string> CheckForFile(TranslationService service, string relativeName, HashSet<string> translations)
{
if (translations.Count > 0)
{
@ -158,6 +160,8 @@ public static class Helper
Console.WriteLine();
}
}
return translations;
}
private static bool HasInvalidPrefixes(HashSet<string> prefixes)

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

@ -11,7 +11,7 @@ using Squidex.Translator.State;
namespace Squidex.Translator.Processes;
public class TranslateTemplates
public partial class TranslateTemplates
{
private static readonly HashSet<string> TagsToIgnore = new HashSet<string>
{
@ -198,8 +198,11 @@ public class TranslateTemplates
// Fix the attributes, because html agility packs converts attributes without value to attributes with empty string.
// For example
// <ng-container content> becomes <ng-container content="">
text = Regex.Replace(text, " (?<Name>[^\\s]*)=\"\"", x => " " + x.Groups["Name"].Value);
text = TextRegex().Replace(text, x => " " + x.Groups["Name"].Value);
File.WriteAllText(file.FullName, text);
}
[GeneratedRegex(" (?<Name>[^\\s]*)=\"\"")]
private static partial Regex TextRegex();
}

8
backend/i18n/translator/Squidex.Translator/Processes/TranslateTypescript.cs

@ -10,7 +10,7 @@ using Squidex.Translator.State;
namespace Squidex.Translator.Processes;
public class TranslateTypescript
public partial class TranslateTypescript
{
private readonly TranslationService service;
private readonly DirectoryInfo folder;
@ -30,7 +30,7 @@ public class TranslateTypescript
var isReplaced = false;
content = Regex.Replace(content, "'[^']*'", match =>
content = TextRegex().Replace(content, match =>
{
var value = match.Value[1..^1];
@ -41,7 +41,6 @@ public class TranslateTypescript
service.Translate(relativeName, value, "Code", key =>
{
result = $"\'i18n:{key}\'";
isReplaced = true;
});
}
@ -58,4 +57,7 @@ public class TranslateTypescript
}
}
}
[GeneratedRegex("'[^']*'")]
private static partial Regex TextRegex();
}

14
backend/i18n/translator/Squidex.Translator/State/TranslationService.cs

@ -10,6 +10,7 @@ using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Squidex.Translator.State.Old;
using static System.Net.Mime.MediaTypeNames;
namespace Squidex.Translator.State;
@ -159,6 +160,14 @@ public sealed class TranslationService
Save();
}
public void Remove(string key)
{
foreach (var (_, texts) in translations)
{
texts.Remove(key);
}
}
public void Save()
{
foreach (var (locale, texts) in translations)
@ -297,6 +306,11 @@ public sealed class TranslationService
return translationToIgnore.TryGetValue(name, out var ignores) && (ignores.Contains(text) || ignores.Contains("*"));
}
public void Add(string key)
{
MainTranslations[key] = string.Empty;
}
private void AddText(string key, string text)
{
MainTranslations[key] = text;

21
backend/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs

@ -1,21 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Areas.Api.Controllers.UI.Models;
public sealed class UISettingsDto
{
/// <summary>
/// True when the user can create apps.
/// </summary>
public bool CanCreateApps { get; set; }
/// <summary>
/// True when the user can create teams.
/// </summary>
public bool CanCreateTeams { get; set; }
}

57
backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -9,7 +9,9 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Web;
@ -18,6 +20,7 @@ namespace Squidex.Areas.Api.Controllers.UI;
public sealed class UIController : ApiController
{
private static readonly DomainId SharedKey = DomainId.Create("__shared");
private static readonly Permission CreateAppPermission = new Permission(PermissionIds.AdminAppCreate);
private static readonly Permission CreateTeamPermission = new Permission(PermissionIds.AdminTeamCreate);
private readonly MyUIOptions uiOptions;
@ -36,16 +39,23 @@ public sealed class UIController : ApiController
/// <response code="200">UI settings returned.</response>.
[HttpGet]
[Route("ui/settings/")]
[ProducesResponseType(typeof(UISettingsDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Dictionary<string, JsonValue>), StatusCodes.Status200OK)]
[ApiPermission]
public IActionResult GetSettings()
public async Task<IActionResult> GetCommonSettings()
{
var result = new UISettingsDto
var common = await appUISettings.GetAsync(SharedKey, UserId, HttpContext.RequestAborted);
var result = new Dictionary<string, JsonValue>
{
CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateAppPermission),
CanCreateTeams = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateTeamPermission),
["canCreateApps"] = !uiOptions.OnlyAdminsCanCreateApps || Context.UserPermissions.Includes(CreateAppPermission),
["canCreateTeams"] = !uiOptions.OnlyAdminsCanCreateTeams || Context.UserPermissions.Includes(CreateTeamPermission),
};
foreach (var (key, value) in common)
{
result[key] = value;
}
return Ok(result);
}
@ -57,7 +67,7 @@ public sealed class UIController : ApiController
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/ui/settings/")]
[ProducesResponseType(typeof(Dictionary<string, string>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Dictionary<string, JsonValue>), StatusCodes.Status200OK)]
[ApiPermission]
public async Task<IActionResult> GetSettings(string app)
{
@ -74,7 +84,7 @@ public sealed class UIController : ApiController
/// <response code="404">App not found.</response>.
[HttpGet]
[Route("apps/{app}/ui/settings/me")]
[ProducesResponseType(typeof(Dictionary<string, string>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Dictionary<string, JsonValue>), StatusCodes.Status200OK)]
[ApiPermission]
public async Task<IActionResult> GetUserSettings(string app)
{
@ -83,6 +93,23 @@ public sealed class UIController : ApiController
return Ok(result);
}
/// <summary>
/// Set ui settings.
/// </summary>
/// <param name="key">The name of the setting.</param>
/// <param name="request">The request with the value to update.</param>
/// <response code="200">UI setting set.</response>.
/// <response code="404">App not found.</response>.
[HttpPut]
[Route("ui/settings/{key}")]
[ApiPermission]
public async Task<IActionResult> PutCommonSetting(string key, [FromBody] UpdateSettingDto request)
{
await appUISettings.SetAsync(SharedKey, UserId, key, request.Value, HttpContext.RequestAborted);
return NoContent();
}
/// <summary>
/// Set ui settings.
/// </summary>
@ -119,6 +146,22 @@ public sealed class UIController : ApiController
return NoContent();
}
/// <summary>
/// Remove ui settings.
/// </summary>
/// <param name="key">The name of the setting.</param>
/// <response code="200">UI setting removed.</response>.
/// <response code="404">App not found.</response>.
[HttpDelete]
[Route("ui/settings/{key}")]
[ApiPermission]
public async Task<IActionResult> DeleteCommonSetting(string key)
{
await appUISettings.RemoveAsync(SharedKey, UserId, key, HttpContext.RequestAborted);
return NoContent();
}
/// <summary>
/// Remove ui settings.
/// </summary>

33
frontend/.storybook/main.js

@ -6,23 +6,18 @@
*/
const customConfig = require('./../src/config/webpack.config');
module.exports = {
stories: [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
framework: "@storybook/angular",
core: {
builder: "@storybook/builder-webpack5"
},
webpackFinal: async (config) => {
customConfig(config, {}, {});
return config;
},
}
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions"],
framework: {
name: "@storybook/angular",
options: {}
},
webpackFinal: async config => {
customConfig(config, {}, {});
return config;
},
docs: {
autodocs: true
}
};

5
frontend/.stylelintrc.json

@ -9,12 +9,10 @@
"color-function-notation": null,
"declaration-block-no-redundant-longhand-properties": null,
"declaration-empty-line-before": null,
"indentation": 4,
"no-duplicate-selectors": null,
"no-empty-source": null,
"no-invalid-position-at-import-rule": null,
"no-missing-end-of-source-newline": null,
"number-leading-zero": "never",
"scss/at-rule-no-unknown": true,
"scss/comment-no-empty": null,
"scss/no-global-function-names": null,
@ -26,8 +24,7 @@
"ng-deep"
]
}
],
"string-quotes": "single"
]
},
"ignoreFiles": [
"**/*.cs",

31
frontend/angular.json

@ -30,6 +30,7 @@
"inlineStyleLanguage": "scss",
"allowedCommonJsDependencies": [
"@tweenjs/tween.js",
"copy-to-clipboard",
"cropperjs",
"crypto-js",
"crypto-js/core.js",
@ -38,9 +39,11 @@
"crypto-js/sha256.js",
"mousetrap",
"mersenne-twister",
"nullthrows",
"pikaday",
"pikaday/pikaday",
"progressbar.js",
"set-value",
"slugify"
],
"assets": [
@ -50,7 +53,9 @@
"src/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": ["./src/app/theme"]
"includePaths": [
"./src/app/theme"
]
},
"customWebpackConfig": {
"path": "./src/config/webpack.config.js"
@ -125,10 +130,30 @@
"src/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": ["./src/app/theme"]
"includePaths": [
"./src/app/theme"
]
},
"scripts": []
}
},
"storybook": {
"builder": "@storybook/angular:start-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "squidex:build",
"compodoc": false,
"port": 6006
}
},
"build-storybook": {
"builder": "@storybook/angular:build-storybook",
"options": {
"configDir": ".storybook",
"browserTarget": "squidex:build",
"compodoc": false,
"outputDir": "storybook-static"
}
}
}
}
@ -136,4 +161,4 @@
"cli": {
"analytics": "a157454d-c7c0-4947-986a-982746edc974"
}
}
}

65085
frontend/package-lock.json

File diff suppressed because it is too large

171
frontend/package.json

@ -10,121 +10,130 @@
"test": "ng test",
"test:coverage": "ng test --no-watch --code-coverage --browsers=ChromeHeadlessNoSandbox",
"watch": "ng build --watch --configuration development",
"storybook": "start-storybook -p 6006",
"storybook-build": "build-storybook"
"storybook": "ng run squidex:storybook",
"storybook-build": "ng run squidex:build-storybook"
},
"private": true,
"dependencies": {
"@angular/animations": "15.0.4",
"@angular/cdk": "15.0.3",
"@angular/cdk-experimental": "15.0.3",
"@angular/common": "15.0.4",
"@angular/core": "15.0.4",
"@angular/forms": "15.0.4",
"@angular/localize": "15.0.4",
"@angular/platform-browser": "15.0.4",
"@angular/platform-browser-dynamic": "15.0.4",
"@angular/platform-server": "15.0.4",
"@angular/router": "15.0.4",
"@babel/runtime": "^7.20.6",
"@angular/animations": "16.1.8",
"@angular/cdk": "16.1.7",
"@angular/cdk-experimental": "16.1.7",
"@angular/common": "16.1.8",
"@angular/core": "16.1.8",
"@angular/forms": "16.1.8",
"@angular/localize": "16.1.8",
"@angular/platform-browser": "16.1.8",
"@angular/platform-browser-dynamic": "16.1.8",
"@angular/platform-server": "16.1.8",
"@angular/router": "16.1.8",
"@babel/runtime": "^7.22.6",
"@egjs/hammerjs": "2.0.17",
"@graphiql/toolkit": "^0.8.0",
"@marker.io/browser": "^0.16.0",
"ace-builds": "1.14.0",
"angular-gridster2": "15.0.0",
"@floating-ui/dom": "^1.5.1",
"@graphiql/toolkit": "^0.9.1",
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
"@lithiumjs/angular": "^7.3.0",
"@lithiumjs/ngx-virtual-scroll": "^0.3.0",
"@marker.io/browser": "^0.19.0",
"ace-builds": "1.23.4",
"angular-gridster2": "16.0.0",
"angular-mentions": "1.5.0",
"babel-polyfill": "6.26.0",
"bootstrap": "5.2.3",
"core-js": "3.26.1",
"core-js": "3.32.0",
"cropperjs": "2.0.0-alpha.1",
"date-fns": "2.29.3",
"date-fns": "2.30.0",
"font-awesome": "4.7.0",
"graphiql": "2.2.0",
"graphql": "16.6.0",
"graphql-ws": "^5.11.2",
"graphiql": "3.0.5",
"graphql": "16.7.1",
"graphql-ws": "^5.14.0",
"image-focus": "1.2.1",
"keycharm": "0.4.0",
"leaflet": "1.9.3",
"leaflet": "1.9.4",
"leaflet-control-geocoder": "2.4.0",
"marked": "4.2.4",
"marked": "6.0.0",
"mersenne-twister": "1.1.0",
"moment": "^2.29.4",
"mousetrap": "1.6.5",
"ng2-charts": "^4.1.1",
"ngx-color-picker": "13.0.0",
"ngx-color-picker": "14.0.0",
"ngx-doc-viewer": "15.0.1",
"ngx-virtual-scroller": "^4.0.3",
"oidc-client-ts": "^2.2.2",
"ngx-ui-tour-core": "^11.0.3",
"oidc-client-ts": "^2.2.4",
"pikaday": "1.8.2",
"progressbar.js": "1.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"rxjs": "7.8.0",
"rxjs": "7.8.1",
"simplemde": "1.11.2",
"slugify": "1.6.5",
"slugify": "1.6.6",
"tinymce": "5.10.2",
"tslib": "2.4.1",
"tslib": "2.6.1",
"tui-calendar": "^1.15.3",
"typemoq": "^2.1.0",
"video.js": "7.20.3",
"vis-data": "7.1.4",
"vis-network": "9.1.2",
"video.js": "8.3.0",
"vis-data": "7.1.6",
"vis-network": "9.1.6",
"vis-util": "5.0.3",
"zone.js": "0.12.0"
"zone.js": "0.13.1"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^15.0.0",
"@angular-devkit/build-angular": "^15.0.4",
"@angular/cli": "^15.0.4",
"@angular/compiler": "^15.0.4",
"@angular/compiler-cli": "^15.0.4",
"@angular/elements": "^15.0.4",
"@babel/core": "^7.20.5",
"@compodoc/compodoc": "^1.1.19",
"@storybook/addon-actions": "^6.5.15",
"@storybook/addon-essentials": "^6.5.15",
"@storybook/addon-interactions": "^6.5.15",
"@storybook/addon-links": "^6.5.15",
"@storybook/angular": "^6.5.15",
"@storybook/builder-webpack5": "^6.5.15",
"@storybook/manager-webpack5": "^6.5.15",
"@storybook/testing-library": "^0.0.13",
"@types/codemirror": "5.60.5",
"@angular-builders/custom-webpack": "^16.0.0",
"@angular-devkit/build-angular": "^16.1.7",
"@angular/cli": "^16.1.7",
"@angular/compiler": "^16.1.8",
"@angular/compiler-cli": "^16.1.8",
"@angular/elements": "^16.1.8",
"@babel/core": "^7.22.9",
"@compodoc/compodoc": "^1.1.21",
"@storybook/addon-actions": "^7.2.1",
"@storybook/addon-essentials": "^7.2.1",
"@storybook/addon-interactions": "^7.2.1",
"@storybook/addon-links": "^7.2.1",
"@storybook/angular": "^7.2.1",
"@storybook/testing-library": "^0.2.0",
"@types/codemirror": "5.60.8",
"@types/core-js": "2.5.5",
"@types/jasmine": "4.3.1",
"@types/marked": "4.0.8",
"@types/mersenne-twister": "1.1.2",
"@types/jasmine": "4.3.5",
"@types/marked": "5.0.1",
"@types/mersenne-twister": "1.1.3",
"@types/mousetrap": "1.6.11",
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
"@types/node": "20.4.7",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"@types/simplemde": "1.11.8",
"@types/tapable": "2.2.2",
"@types/tapable": "2.2.3",
"@types/tinymce": "4.6.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.47.0",
"@typescript-eslint/parser": "5.47.0",
"@webcomponents/custom-elements": "^1.5.1",
"babel-loader": "^8.2.5",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.2.1",
"@typescript-eslint/parser": "6.2.1",
"@webcomponents/custom-elements": "^1.6.0",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "11.0.0",
"eslint": "8.30.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-plugin-deprecation": "^1.3.3",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-webpack-plugin": "3.2.0",
"jasmine-core": "~4.5.0",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.1",
"karma-coverage": "~2.2.0",
"eslint": "8.46.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-plugin-deprecation": "^1.5.0",
"eslint-plugin-import": "2.28.0",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-storybook": "^0.6.13",
"eslint-webpack-plugin": "4.0.1",
"jasmine-core": "~5.1.0",
"karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"karma-jasmine-html-reporter": "~2.1.0",
"raw-loader": "^4.0.2",
"stylelint": "14.16.0",
"stylelint-config-standard": "29.0.0",
"stylelint-config-standard-scss": "^5.0.0",
"stylelint-scss": "4.3.0",
"stylelint-webpack-plugin": "3.3.0",
"typescript": "4.8.4"
"storybook": "^7.2.1",
"stylelint": "15.10.2",
"stylelint-config-standard": "34.0.0",
"stylelint-config-standard-scss": "^10.0.0",
"stylelint-scss": "5.0.1",
"stylelint-webpack-plugin": "4.1.1",
"typescript": "5.1.6"
},
"overrides": {
"ng2-charts": {
"ng2-charts-schematics": "0.1.7"
}
}
}

18
frontend/src/app/app.component.html

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

2
frontend/src/app/app.module.ts

@ -22,7 +22,7 @@ import { routing } from './app.routes';
import { ApiUrlConfig, DateHelper, LocalizerService, SqxFrameworkModule, SqxSharedModule, TitlesConfig, UIOptions } from './shared';
import { SqxShellModule } from './shell';
const options = window['options'] || {};
const options = (window as any)['options'] || {};
DateHelper.setlocale(options.more?.culture);

4
frontend/src/app/app.routes.ts

@ -7,7 +7,7 @@
import { ModuleWithProviders } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppMustExistGuard, LoadAppsGuard, LoadTeamsGuard, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, TeamMustExistGuard, UnsetAppGuard, UnsetTeamGuard } from './shared';
import { AppMustExistGuard, LoadAppsGuard, LoadSettingsGuard, LoadTeamsGuard, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, TeamMustExistGuard, UnsetAppGuard, UnsetTeamGuard } from './shared';
import { AppAreaComponent, ForbiddenPageComponent, HomePageComponent, InternalAreaComponent, LoginPageComponent, LogoutPageComponent, NotFoundPageComponent, TeamsAreaComponent } from './shell';
const routes: Routes = [
@ -19,7 +19,7 @@ const routes: Routes = [
{
path: 'app',
component: InternalAreaComponent,
canActivate: [MustBeAuthenticatedGuard, LoadAppsGuard, LoadTeamsGuard],
canActivate: [MustBeAuthenticatedGuard, LoadAppsGuard, LoadTeamsGuard, LoadSettingsGuard],
children: [
{
path: '',

4
frontend/src/app/features/administration/guards/user-must-exist.guard.ts

@ -6,14 +6,14 @@
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { UsersState } from '@app/features/administration/internal';
import { allParams } from '@app/framework';
@Injectable()
export class UserMustExistGuard implements CanActivate {
export class UserMustExistGuard {
constructor(
private readonly usersState: UsersState,
private readonly router: Router,

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

@ -1,6 +1,6 @@
<sqx-title message="i18n:eventConsumers.pageTitle"></sqx-title>
<sqx-layout layout="main" titleText="i18n:common.consumers" titleIcon="time" [innerWidth]="50">
<sqx-layout layout="main" titleText="i18n:common.consumers" titleIcon="time" innerWidth="50">
<ng-container menu>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:eventConsumers.refreshTooltip" shortcut="CTRL + B">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
@ -8,7 +8,7 @@
</ng-container>
<ng-container>
<sqx-list-view innerWidth="50rem" [isLoading]="eventConsumersState.isLoading | async" [table]="true">
<sqx-list-view innerWidth="50rem" [isLoading]="eventConsumersState.isLoading | async" table="true">
<ng-container header>
<table class="table table-items table-fixed" #header>
<thead>
@ -42,7 +42,13 @@
<ng-container sidebarMenu>
<div class="panel-nav">
<a class="panel-link" routerLink="help" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.help" titlePosition="left-center">
<a class="panel-link"
routerLink="help"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.help"
titlePosition="left"
sqxTourStep="help">
<i class="icon-help2"></i>
</a>
</div>
@ -51,14 +57,12 @@
<router-outlet></router-outlet>
<ng-container *sqxModal="eventConsumerErrorDialog">
<sqx-modal-dialog (close)="eventConsumerErrorDialog.hide()">
<ng-container title>
{{ 'common.error' | sqxTranslate }}
</ng-container>
<ng-container content>
<textarea readonly class="form-control error-message small">{{eventConsumerError}}</textarea>
</ng-container>
</sqx-modal-dialog>
</ng-container>
<sqx-modal-dialog *sqxModal="eventConsumerErrorDialog" (close)="eventConsumerErrorDialog.hide()">
<ng-container title>
{{ 'common.error' | sqxTranslate }}
</ng-container>
<ng-container content>
<textarea readonly class="form-control error-message small">{{eventConsumerError}}</textarea>
</ng-container>
</sqx-modal-dialog>

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

@ -1,6 +1,6 @@
<sqx-title message="i18n:backups.restorePageTitle"></sqx-title>
<sqx-layout layout="main" titleText="i18n:backups.restoreTitle" titleIcon="backup" [innerWidth]="70">
<sqx-layout layout="main" titleText="i18n:backups.restoreTitle" titleIcon="backup" innerWidth="70">
<ng-container>
<sqx-list-view innerWidth="70rem">
<div class="card section" *ngIf="restoreJob | async; let job">
@ -70,7 +70,13 @@
<ng-container sidebarMenu>
<div class="panel-nav">
<a class="panel-link" routerLink="help" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.help" titlePosition="left-center">
<a class="panel-link"
routerLink="help"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.help"
titlePosition="left"
sqxTourStep="help">
<i class="icon-help2"></i>
</a>
</div>

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

@ -2,7 +2,7 @@
<form [formGroup]="userForm.form" (ngSubmit)="save()">
<input style="display: none;" type="password" name="foilautofill">
<sqx-layout layout="right" [width]="30" [white]="true" [padding]="true" [overflow]="true">
<sqx-layout layout="right" width="30" white="true" padding="true" overflow="true">
<ng-container title>
<ng-container *ngIf="usersState.selectedUser | async; else noUserTitle">
<sqx-title message="i18n:users.editPageTitle"></sqx-title>

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

@ -1,6 +1,6 @@
<sqx-title message="i18n:users.listPageTitle"></sqx-title>
<sqx-layout layout="main" titleText="i18n:users.listTitle" titleIcon="user-o" [innerWidth]="50">
<sqx-layout layout="main" titleText="i18n:users.listTitle" titleIcon="user-o" innerWidth="50">
<ng-container menu>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:users.refreshTooltip" shortcut="CTRL + B">
@ -22,7 +22,7 @@
</ng-container>
<ng-container>
<sqx-list-view innerWidth="50rem" [isLoading]="usersState.isLoading | async" [table]="true">
<sqx-list-view innerWidth="50rem" [isLoading]="usersState.isLoading | async" table="true">
<ng-container header>
<table class="table table-items table-fixed" #header>
<thead>
@ -58,7 +58,13 @@
<ng-container sidebarMenu>
<div class="panel-nav">
<a class="panel-link" routerLink="help" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.help" titlePosition="left-center">
<a class="panel-link"
routerLink="help"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.help"
titlePosition="left"
sqxTourStep="help">
<i class="icon-help2"></i>
</a>
</div>

6
frontend/src/app/features/administration/state/event-consumers.state.ts

@ -8,7 +8,7 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { finalize, tap } from 'rxjs/operators';
import { DialogService, LoadingState, shareSubscribed, State } from '@app/shared';
import { debug, DialogService, LoadingState, shareSubscribed, State } from '@app/shared';
import { EventConsumerDto, EventConsumersService } from './../services/event-consumers.service';
interface Snapshot extends LoadingState {
@ -31,7 +31,9 @@ export class EventConsumersState extends State<Snapshot> {
private readonly dialogs: DialogService,
private readonly eventConsumersService: EventConsumersService,
) {
super({ eventConsumers: [] }, 'EventConsumers');
super({ eventConsumers: [] });
debug(this, 'eventConsumers');
}
public load(isReload = false, silent = false): Observable<any> {

6
frontend/src/app/features/administration/state/users.state.ts

@ -11,7 +11,7 @@ import { Injectable } from '@angular/core';
import '@app/framework/utils/rxjs-extensions';
import { EMPTY, Observable, of } from 'rxjs';
import { catchError, finalize, tap } from 'rxjs/operators';
import { DialogService, getPagingInfo, ListState, shareSubscribed, State } from '@app/shared';
import { debug, DialogService, getPagingInfo, ListState, shareSubscribed, State } from '@app/shared';
import { UpsertUserDto, UserDto, UsersService } from './../services/users.service';
interface Snapshot extends ListState<string> {
@ -57,7 +57,9 @@ export class UsersState extends State<Snapshot> {
page: 0,
pageSize: 10,
total: 0,
}, 'Users');
});
debug(this, 'users');
}
public select(id: string | null): Observable<UserDto | null> {

8
frontend/src/app/features/api/api-area.component.html

@ -1,9 +1,9 @@
<sqx-title message="i18n:api.pageTitle"></sqx-title>
<sqx-layout layout="left" titleText="i18n:api.title" titleIcon="api" [width]="16" [white]="true" [overflow]="true" [padding]="true">
<sqx-layout layout="left" titleText="i18n:api.title" titleIcon="api" width="16" white="true" overflow="true" padding="true">
<ng-container>
<ul class="nav nav-light flex-column">
<li class="nav-item">
<li class="nav-item" sqxTourStep="graphql">
<a class="nav-link" routerLink="graphql" routerLinkActive="active">
{{ 'api.graphql' | sqxTranslate }}
</a>
@ -12,12 +12,12 @@
<li class="nav-item nav-heading">
{{ 'common.openAPI' | sqxTranslate }}
</li>
<li class="nav-item">
<li class="nav-item" sqxTourStep="contentApi">
<a class="nav-link" href="/api/content/{{appsState.appName}}/docs" sqxExternalLink>
{{ 'api.contentApi' | sqxTranslate }}
</a>
</li>
<li class="nav-item">
<li class="nav-item" sqxTourStep="generalAPi">
<a class="nav-link" href="/api/docs" sqxExternalLink>
{{ 'api.generalApi' | sqxTranslate }}
</a>

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

@ -1,40 +1,38 @@
<sqx-title message="i18n:api.graphqlPageTitle"></sqx-title>
<sqx-layout layout="main" [hideHeader]="true" [hideSidebar]="true">
<div inner #graphiQLContainer></div>
<sqx-layout layout="main" hideHeader="true" hideSidebar="true">
<div inner #graphiQLContainer sqxTourStep="graphQLExplorer"></div>
<button class="btn btn-simple btn-options" *ngIf="clientsReadable" (click)="clientsDialog.show()">
<i class="icon-clients"></i>
</button>
</sqx-layout>
<ng-container *sqxModal="clientsDialog">
<sqx-modal-dialog (close)="clientsDialog.hide()">
<ng-container title>
{{ 'api.selectClient' | sqxTranslate }}
</ng-container>
<sqx-modal-dialog *sqxModal="clientsDialog" (close)="clientsDialog.hide()">
<ng-container title>
{{ 'api.selectClient' | sqxTranslate }}
</ng-container>
<ng-container content>
<sqx-form-hint>
{{ 'api.selectClientDescription' | sqxTranslate }}
</sqx-form-hint>
<ng-container content>
<sqx-form-hint>
{{ 'api.selectClientDescription' | sqxTranslate }}
</sqx-form-hint>
<div class="form-group">
<label for="client">{{ 'common.client' | sqxTranslate }}</label>
<select class="form-control" id="client"
[ngModel]="clientSelected"
(ngModelChange)="selectClient($event)">
<option [ngValue]="null">{{ 'api.noClient' | sqxTranslate }}</option>
<option *ngFor="let client of clientsState.clients | async" [ngValue]="client">{{client.id}}</option>
</select>
</div>
</ng-container>
<div class="form-group">
<label for="client">{{ 'common.client' | sqxTranslate }}</label>
<select class="form-control" id="client"
[ngModel]="clientSelected"
(ngModelChange)="selectClient($event)">
<option [ngValue]="null">{{ 'api.noClient' | sqxTranslate }}</option>
<option *ngFor="let client of clientsState.clients | async" [ngValue]="client">{{client.id}}</option>
</select>
</div>
</ng-container>
<ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="clientsDialog.hide()">
{{ 'common.close' | sqxTranslate }}
</button>
</ng-container>
</sqx-modal-dialog>
</ng-container>
<ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="clientsDialog.hide()">
{{ 'common.close' | sqxTranslate }}
</button>
</ng-container>
</sqx-modal-dialog>

12
frontend/src/app/features/api/pages/graphql/graphql-page.component.ts

@ -10,7 +10,7 @@ import { createGraphiQLFetcher } from '@graphiql/toolkit';
import GraphiQL from 'graphiql';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { ApiUrlConfig, AppsState, AuthService, ClientDto, ClientsService, ClientsState, DialogModel } from '@app/shared';
import { ApiUrlConfig, AppsState, AuthService, ClientDto, ClientsService, ClientsState, DialogModel, MessageBus, QueryExecuted, Types } from '@app/shared';
@Component({
selector: 'sqx-graphql-page',
@ -30,6 +30,7 @@ export class GraphQLPageComponent implements AfterViewInit, OnInit {
private readonly apiUrl: ApiUrlConfig,
private readonly authService: AuthService,
private readonly clientsService: ClientsService,
private readonly messageBus: MessageBus,
public readonly clientsState: ClientsState,
) {
}
@ -75,6 +76,15 @@ export class GraphQLPageComponent implements AfterViewInit, OnInit {
headers: {
Authorization: `Bearer ${accessToken}`,
},
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
const isIntrospection = Types.isString(init?.body) && init!.body.indexOf('IntrospectionQuery') >= 0;
if (!isIntrospection) {
this.messageBus.emit(new QueryExecuted());
}
return fetch(input, init);
},
subscriptionUrl,
});

22
frontend/src/app/features/apps/pages/app.component.html

@ -1,5 +1,5 @@
<div class="card card-href card-app" [routerLink]="['/app', app.name]">
<div class="card-body">
<div class="card-body" sqxTourStep="app">
<div class="row g-0">
<div class="col-auto card-left">
<sqx-avatar [image]="app.image" [identifier]="app.name"></sqx-avatar>
@ -29,17 +29,15 @@
<i class="icon-dots"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonOptions" [scrollY]="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="leave.emit(app)"
confirmTitle="i18n:apps.leaveConfirmTitle"
confirmText="i18n:apps.leaveConfirmText"
confirmRememberKey="leaveApp">
{{ 'apps.leave' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="leave.emit(app)"
confirmTitle="i18n:apps.leaveConfirmTitle"
confirmText="i18n:apps.leaveConfirmText"
confirmRememberKey="leaveApp">
{{ 'apps.leave' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
</div>
</div>

4
frontend/src/app/features/apps/pages/app.component.ts

@ -9,13 +9,13 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
import { AppDto, ModalModel } from '@app/shared';
@Component({
selector: 'sqx-app[app]',
selector: 'sqx-app',
styleUrls: ['./app.component.scss'],
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
@Input()
@Input({ required: true })
public app!: AppDto;
@Output()

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

@ -2,29 +2,20 @@
<div class="panel-container page" *ngIf="authState.userChanges | async; let user">
<div class="apps-section">
<div class="row g-3">
<div class="col-auto">
<div class="squid d-flex align-items-center justify-content-center bordered">
<img src="./images/squid.svg">
</div>
</div>
<div class="col">
<h1 class="apps-title">{{ 'apps.welcomeTitle' | sqxTranslate: { user: user.displayName } }}</h1>
<h1 class="apps-title">{{ 'apps.welcomeTitle' | sqxTranslate: { user: user.displayName } }}</h1>
<div class="subtext">
{{ 'apps.welcomeSubtitle' | sqxTranslate }}
</div>
</div>
<div class="subtext">
{{ 'apps.welcomeSubtitle' | sqxTranslate }}
</div>
</div>
<ng-container *ngIf="groupedApps | async; let groups">
<div class="apps-section">
<div class="apps-section" sqxTourStep="allApps">
<div class="empty" *ngIf="groups.length === 0">
<h3 class="empty-headline">{{ 'apps.empty' | sqxTranslate }}</h3>
</div>
<div class="team" *ngFor="let group of groups; trackByGroup">
<div class="team" *ngFor="let group of groups; trackBy: trackByGroup">
<div class="team-header" *ngIf="group.team">
<sqx-team [team]="group.team" (leave)="leaveTeam($event)"></sqx-team>
</div>
@ -41,7 +32,7 @@
</ng-container>
<div class="apps-section" *ngIf="(uiState.settings | async)?.canCreateApps">
<div class="card card-template card-href" (click)="createNewApp()">
<div class="card card-template card-href" (click)="createNewApp()" sqxTourStep="addApp">
<div class="card-body">
<div class="card-image">
<img src="./images/add-app.svg">
@ -75,14 +66,14 @@
</div>
</div>
<ng-container *sqxModal="addAppDialog">
<sqx-app-form [template]="addAppTemplate" (complete)="addAppDialog.hide()"></sqx-app-form>
</ng-container>
<sqx-app-form *sqxModal="addAppDialog"
(close)="addAppDialog.hide()" [template]="addAppTemplate">
</sqx-app-form>
<ng-container *sqxModal="onboardingDialog">
<sqx-onboarding-dialog (close)="onboardingDialog.hide()"></sqx-onboarding-dialog>
</ng-container>
<sqx-onboarding-dialog *sqxModal="onboardingDialog"
(close)="onboardingDialog.hide()">
</sqx-onboarding-dialog>
<ng-container *sqxModal="newsDialog">
<sqx-news-dialog [features]="newsFeatures!" (close)="newsDialog.hide()"></sqx-news-dialog>
</ng-container>
<sqx-news-dialog *sqxModal="newsDialog" [features]="newsFeatures!"
(close)="newsDialog.hide()">
</sqx-news-dialog>

13
frontend/src/app/features/apps/pages/apps-page.component.scss

@ -11,19 +11,6 @@
overflow-y: auto;
}
.squid {
@include circle(60px);
background: $color-white;
&.bordered {
border: 1px solid $color-border;
}
img {
max-height: 80%;
}
}
:host ::ng-deep {
.card {
@include hover-visible('.deeplinks', inline);

22
frontend/src/app/features/apps/pages/apps-page.component.ts

@ -8,7 +8,7 @@
import { Component, OnInit } from '@angular/core';
import { combineLatest } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AppDto, AppsState, AuthService, DialogModel, FeatureDto, LocalStoreService, NewsService, OnboardingService, TeamDto, TeamsState, TemplateDto, TemplatesState, UIOptions, UIState } from '@app/shared';
import { AppDto, AppsState, AuthService, DialogModel, FeatureDto, LocalStoreService, NewsService, TeamDto, TeamsState, TemplateDto, TemplatesState, TourState, UIOptions, UIState } from '@app/shared';
import { Settings } from '@app/shared/state/settings';
type GroupedApps = { team?: TeamDto; apps: AppDto[] };
@ -63,9 +63,9 @@ export class AppsPageComponent implements OnInit {
private readonly appsState: AppsState,
private readonly localStore: LocalStoreService,
private readonly newsService: NewsService,
private readonly onboardingService: OnboardingService,
private readonly teamsState: TeamsState,
private readonly templatesState: TemplatesState,
private readonly tourState: TourState,
private readonly uiOptions: UIOptions,
) {
if (uiOptions.get('showInfo')) {
@ -74,14 +74,20 @@ export class AppsPageComponent implements OnInit {
}
public ngOnInit() {
const shouldShowOnboarding = this.onboardingService.shouldShow('dialog');
this.appsState.apps.pipe(take(1))
.subscribe(apps => {
if (shouldShowOnboarding && apps.length === 0) {
this.onboardingService.disable('dialog');
if (apps.length === 0 &&
this.tourState.snapshot.status !== 'Completed' &&
this.tourState.snapshot.status !== 'Started') {
this.onboardingDialog.show();
} else if (!this.uiOptions.get('hideNews')) {
return;
}
if (this.tourState.snapshot.status !== 'Started') {
this.tourState.complete();
}
if (!this.uiOptions.get('hideNews')) {
const newsVersion = this.localStore.getInt(Settings.Local.NEWS_VERSION);
this.newsService.getFeatures(newsVersion)
@ -95,7 +101,7 @@ export class AppsPageComponent implements OnInit {
this.localStore.setInt(Settings.Local.NEWS_VERSION, result.version);
}
});
}
}
});
this.templatesState.load(false, true);

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

@ -9,7 +9,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FeatureDto } from '@app/shared';
@Component({
selector: 'sqx-news-dialog[features]',
selector: 'sqx-news-dialog',
styleUrls: ['./news-dialog.component.scss'],
templateUrl: './news-dialog.component.html',
})
@ -17,7 +17,7 @@ export class NewsDialogComponent {
@Output()
public close = new EventEmitter();
@Input()
@Input({ required: true })
public features!: ReadonlyArray<FeatureDto>;
public trackByFeature(_index: number, feature: FeatureDto) {

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

@ -1,4 +1,4 @@
<sqx-modal-dialog [showHeader]="false">
<sqx-modal-dialog showHeader="false">
<ng-container tabs>
<div class="squid d-flex align-items-center justify-content-center">
<img src="./images/squid.svg">
@ -17,7 +17,9 @@
<div [innerHTML]="'tour.stepIntroText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="mt-4">
<button (click)="next()" class="btn btn-success">{{ 'tour.stepIntroNext' | sqxTranslate }}</button>
<button (click)="next()" class="btn btn-success">
{{ 'tour.stepIntroNext' | sqxTranslate }}
</button>
</div>
</div>
</div>
@ -36,6 +38,7 @@
<select class="form-select" id="companyRole" formControlName="companyRole">
<option [ngValue]="'RoleEmployee'">{{ 'tour.roleEmployee' | sqxTranslate }}</option>
<option [ngValue]="'RoleBusinessOwner'">{{ 'tour.roleBusinessOwner' | sqxTranslate }}</option>
<option [ngValue]="'RoleProductManager'">{{ 'tour.roleProductManager' | sqxTranslate }}</option>
<option [ngValue]="'RoleContentCreator'">{{ 'tour.roleContentCreator' | sqxTranslate }}</option>
<option [ngValue]="'RoleSoftwareDeveloper'">{{ 'tour.roleSoftwareDeveloper' | sqxTranslate }}</option>
@ -75,86 +78,21 @@
</div>
<div class="onboarding-step" *ngIf="step === 2">
<div class="row g-0" @slide>
<div class="col">
<h2>{{ 'common.apps' | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepAppText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="footer">
<button (click)="next()" class="btn btn-success">{{ 'tour.stepAppNext' | sqxTranslate }}</button>
</div>
</div>
<div class="col col-image">
<img src="./images/tour-apps.png">
</div>
</div>
</div>
<div class="onboarding-step" *ngIf="step === 3">
<div class="row g-0" @slide>
<div class="col">
<h2>{{ 'common.schemas' | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepSchemasText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="footer">
<button (click)="next()" class="btn btn-success">{{ 'tour.stepSchemasNext' | sqxTranslate }}</button>
</div>
</div>
<div class="col col-image">
<img src="./images/tour-schema.png">
</div>
</div>
</div>
<div class="onboarding-step" *ngIf="step === 4">
<div class="row g-0" @slide>
<div class="col">
<h2>{{ 'common.contents' | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepContentText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="footer">
<button (click)="next()" class="btn btn-success">{{ 'tour.stepContentNext' | sqxTranslate }}</button>
</div>
</div>
<div class="col col-image">
<img src="./images/tour-content.png">
</div>t
</div>
</div>
<div class="onboarding-step" *ngIf="step === 5">
<div class="row g-0" @slide>
<div class="col">
<h2>{{ 'common.assets' | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepAssetsText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="footer">
<button (click)="next()" class="btn btn-success">{{ 'tour.stepAssetsNext' | sqxTranslate }}</button>
</div>
</div>
<div class="col col-image">
<img src="./images/tour-assets.png">
</div>
</div>
</div>
<div class="onboarding-step" *ngIf="step === 6">
<img @fade class="header-left" src="./images/logo-white-small.png">
<div @slide class="onboarding-enter-leave text-center">
<h2>{{ 'tour.stepOutroTitle' | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepOutroText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<h2>{{ 'tour.stepTourTitle' | sqxTranslate }}</h2>
<div [innerHTML]="'tour.stepTourText' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<div class="mt-4">
<a class="btn btn-success me-2" href="https://support.squidex.io" sqxExternalLink>
{{ 'tour.joinForum' | sqxTranslate }}
</a>
<button (click)="start()" class="btn btn-success">
{{ 'tour.startYes' | sqxTranslate }}
</button>
<a class="btn btn-success" href="https://github.com/squidex/squidex" sqxExternalLink>
{{ 'tour.joinGithub' | sqxTranslate }}
</a>
<button (click)="cancel()" class="btn btn-outline-secondary ms-2">
{{ 'tour.startNo' | sqxTranslate }}
</button>
</div>
</div>
</div>

4
frontend/src/app/features/apps/pages/onboarding-dialog.component.scss

@ -42,6 +42,10 @@ p {
position: relative;
}
&-body {
overflow: hidden;
}
&-footer {
border: 0;
}

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

@ -8,7 +8,7 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { fadeAnimation, slideAnimation } from '@app/framework';
import { UsersService } from '@app/shared';
import { TourState, UsersService } from '@app/shared';
@Component({
selector: 'sqx-onboarding-dialog',
@ -34,6 +34,7 @@ export class OnboardingDialogComponent {
constructor(
private readonly formBuilder: FormBuilder,
private readonly tourState: TourState,
private readonly usersService: UsersService,
) {
}
@ -43,6 +44,16 @@ export class OnboardingDialogComponent {
this.next();
}
public start() {
this.tourState.start();
this.close.emit();
}
public cancel() {
this.tourState.complete();
this.close.emit();
}
public next() {
this.step += 1;
}

20
frontend/src/app/features/apps/pages/team.component.html

@ -11,17 +11,15 @@
<i class="icon-dots"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonOptions" [scrollY]="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="leave.emit(team)"
confirmTitle="i18n:teams.leaveConfirmTitle"
confirmText="i18n:teams.leaveConfirmText"
confirmRememberKey="leaveApp">
{{ 'teams.leave' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="leave.emit(team)"
confirmTitle="i18n:teams.leaveConfirmTitle"
confirmText="i18n:teams.leaveConfirmText"
confirmRememberKey="leaveApp">
{{ 'teams.leave' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</div>
</div>
</div>

4
frontend/src/app/features/apps/pages/team.component.ts

@ -9,13 +9,13 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
import { ModalModel, TeamDto } from '@app/shared';
@Component({
selector: 'sqx-team[team]',
selector: 'sqx-team',
styleUrls: ['./team.component.scss'],
templateUrl: './team.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TeamComponent {
@Input()
@Input({ required: true })
public team!: TeamDto;
@Output()

4
frontend/src/app/features/assets/pages/asset-tag-dialog.component.html

@ -1,5 +1,5 @@
<form [formGroup]="editForm.form" (ngSubmit)="renameAssetTag()">
<sqx-modal-dialog (close)="emitComplete()">
<sqx-modal-dialog (close)="emitClose()">
<ng-container title>
{{ 'common.renameTag' | sqxTranslate }}
</ng-container>
@ -17,7 +17,7 @@
</ng-container>
<ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="emitComplete()">
<button type="button" class="btn btn-text-secondary" (click)="emitClose()">
{{ 'common.cancel' | sqxTranslate }}
</button>

14
frontend/src/app/features/assets/pages/asset-tag-dialog.component.ts

@ -9,15 +9,15 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AssetsState, RenameAssetTagForm } from '@app/shared/internal';
@Component({
selector: 'sqx-asset-tag-dialog[tagName]',
selector: 'sqx-asset-tag-dialog',
styleUrls: ['./asset-tag-dialog.component.scss'],
templateUrl: './asset-tag-dialog.component.html',
})
export class AssetTagDialogComponent implements OnInit {
@Output()
public complete = new EventEmitter();
public close = new EventEmitter();
@Input()
@Input({ required: true })
public tagName!: string;
public editForm = new RenameAssetTagForm();
@ -31,8 +31,8 @@ export class AssetTagDialogComponent implements OnInit {
this.editForm.load({ tagName: this.tagName });
}
public emitComplete() {
this.complete.emit();
public emitClose() {
this.close.emit();
}
public renameAssetTag() {
@ -43,13 +43,13 @@ export class AssetTagDialogComponent implements OnInit {
}
if (value.tagName === this.tagName) {
this.emitComplete();
this.emitClose();
}
this.assetsState.renameTag(this.tagName, value?.tagName)
.subscribe({
next: () => {
this.emitComplete();
this.emitClose();
},
error: error => {
this.editForm.submitFailed(error);

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

@ -23,6 +23,6 @@
</div>
</div>
<ng-container *sqxModal="tagRenameDialog">
<sqx-asset-tag-dialog [tagName]="tagRenaming!.name" (complete)="tagRenameDialog.hide()"></sqx-asset-tag-dialog>
</ng-container>
<sqx-asset-tag-dialog *sqxModal="tagRenameDialog"
(close)="tagRenameDialog.hide()" [tagName]="tagRenaming!.name">
</sqx-asset-tag-dialog>

10
frontend/src/app/features/assets/pages/asset-tags.component.ts

@ -7,11 +7,11 @@
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { DialogModel, TagItem, TagsSelected } from '@app/shared';
@Component({
selector: 'sqx-asset-tags[tags][tagsSelected]',
selector: 'sqx-asset-tags',
styleUrls: ['./asset-tags.component.scss'],
templateUrl: './asset-tags.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -23,13 +23,13 @@ export class AssetTagsComponent {
@Output()
public toggle = new EventEmitter<string>();
@Input()
@Input({ required: true })
public tags!: ReadonlyArray<TagItem>;
@Input()
@Input({ required: true })
public tagsSelected!: TagsSelected;
@Input()
@Input({ transform: booleanAttribute })
public canRename = false;
public tagRenaming?: TagItem;

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

@ -1,4 +1,4 @@
<sqx-layout titleText="i18n:common.filters" [width]="20" [white]="true" [padding]="true" [overflow]="true">
<sqx-layout titleText="i18n:common.filters" width="20" white="true" padding="true" overflow="true">
<h3>{{ 'common.tags' | sqxTranslate }}</h3>
<sqx-asset-tags (reset)="resetTags()"

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

@ -15,13 +15,13 @@
[itemsSource]="assetsState.tagsNames | async"
[ngModel]="assetsState.selectedTagNames | async"
(ngModelChange)="selectTags($event)"
[styleScrollable]="true"
[undefinedWhenEmpty]="false">
styleScrollable="true"
undefinedWhenEmpty="false">
</sqx-tag-editor>
</div>
<div class="col-6">
<sqx-search-form formClass="form" placeholder="{{ 'assets.searchByName' | sqxTranslate }}" fieldExample="fileSize"
[enableShortcut]="true"
enableShortcut="true"
[queries]="listQueries"
[queriesTypes]="'common.assets' | sqxTranslate"
[query]="assetsState.query | async"
@ -73,7 +73,13 @@
<ng-container sidebarMenu>
<div class="panel-nav">
<a class="panel-link" routerLink="filters" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.filters" titlePosition="left-center">
<a class="panel-link"
routerLink="filters"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.filters"
titlePosition="left"
sqxTourStep="filter">
<i class="icon-filter"></i>
</a>
</div>
@ -82,13 +88,13 @@
<router-outlet></router-outlet>
<ng-container *sqxModal="addAssetFolderDialog">
<sqx-asset-folder-dialog (complete)="addAssetFolderDialog.hide()"></sqx-asset-folder-dialog>
</ng-container>
<sqx-asset-folder-dialog *sqxModal="addAssetFolderDialog"
(close)="addAssetFolderDialog.hide()">
</sqx-asset-folder-dialog>
<sqx-asset-dialog *sqxModal="editAsset;isDialog:true"
[asset]="editAsset!"
(assetUpdated)="replaceAsset($event)"
(assetReplaced)="replaceAsset($event)"
(complete)="editDone()">
(close)="editDone()">
</sqx-asset-dialog>

2
frontend/src/app/features/content/module.ts

@ -7,7 +7,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { VirtualScrollerModule } from 'ngx-virtual-scroller';
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSchemasGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, ChatDialogComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesPageComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations';

170
frontend/src/app/features/content/pages/calendar/calendar-page.component.html

@ -1,6 +1,6 @@
<sqx-title message="i18n:contents.calendar"></sqx-title>
<sqx-layout layout="main" titleText="i18n:contents.calendar" [hideSidebar]="true">
<sqx-layout layout="main" titleText="i18n:contents.calendar" hideSidebar="true">
<ng-container menu>
{{title}}
@ -23,108 +23,106 @@
</ng-container>
</sqx-layout>
<ng-container *sqxModal="contentDialog">
<sqx-modal-dialog (close)="contentDialog.hide()">
<ng-container title>
{{ 'common.content' | sqxTranslate }}
</ng-container>
<sqx-modal-dialog *sqxModal="contentDialog" (close)="contentDialog.hide()">
<ng-container title>
{{ 'common.content' | sqxTranslate }}
</ng-container>
<ng-container content>
<div *ngIf="content && content.scheduleJob">
<div class="form-group row">
<label class="col-4 col-form-label">{{ 'common.id' | sqxTranslate }}</label>
<div class="col-8">
<div class="input-group">
<input readonly class="form-control" name="id" id="id" value="{{content.id}}" #inputId>
<ng-container content>
<div *ngIf="content && content.scheduleJob">
<div class="form-group row">
<label class="col-4 col-form-label">{{ 'common.id' | sqxTranslate }}</label>
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputId">
<i class="icon-copy"></i>
</button>
</div>
<div class="col-8">
<div class="input-group">
<input readonly class="form-control" name="id" id="id" value="{{content.id}}" #inputId>
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputId">
<i class="icon-copy"></i>
</button>
</div>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.content' | sqxTranslate }}</label>
<div class="col-8">
<a class="truncate" [routerLink]="['../', content.schemaName, content.id]">
{{createContentName(content)}}
</a>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.content' | sqxTranslate }}</label>
<div class="col-8">
<a class="truncate" [routerLink]="['../', content.schemaName, content.id]">
{{createContentName(content)}}
</a>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.schema' | sqxTranslate }}</label>
<div class="col-8">
<a class="truncate" [routerLink]="['../', content.schemaName]">
{{content.schemaDisplayName}}
</a>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.schema' | sqxTranslate }}</label>
<div class="col-8">
<a class="truncate" [routerLink]="['../', content.schemaName]">
{{content.schemaDisplayName}}
</a>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.status' | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
[status]="content.status"
[statusColor]="content.statusColor"
[small]="true">
</sqx-content-status>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.status' | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
[status]="content.status"
[statusColor]="content.statusColor"
small="true">
</sqx-content-status>
</div>
</div>
<hr />
<hr />
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledToLabel' | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
[status]="content.scheduleJob.status"
[statusColor]="content.scheduleJob.color"
[small]="true">
</sqx-content-status>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledToLabel' | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
[status]="content.scheduleJob.status"
[statusColor]="content.scheduleJob.color"
small="true">
</sqx-content-status>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledAt' | sqxTranslate }}</label>
<div class="col-8">
{{content.scheduleJob.dueTime | sqxFullDateTime}}
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledAt' | sqxTranslate }}</label>
<div class="col-8">
{{content.scheduleJob.dueTime | sqxFullDateTime}}
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledBy' | sqxTranslate }}</label>
<div class="col-8">
<img class="user-picture" [src]="content.scheduleJob.scheduledBy | sqxUserPictureRef"> {{content.scheduleJob.scheduledBy | sqxUserNameRef}}
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledBy' | sqxTranslate }}</label>
<div class="col-8">
<img class="user-picture" [src]="content.scheduleJob.scheduledBy | sqxUserPictureRef"> {{content.scheduleJob.scheduledBy | sqxUserNameRef}}
</div>
</div>
<ng-container *ngIf="content.canCancelStatus">
<hr />
<ng-container *ngIf="content.canCancelStatus">
<hr />
<div class="row">
<div class="col-8 offset-4">
<button type="button" class="btn btn-outline-danger" [class.disabled]="!content.canCancelStatus"
(sqxConfirmClick)="cancelStatus()"
confirmTitle="i18n:contents.cancelStatusConfirmTitle"
confirmText="i18n:contents.cancelStatusConfirmText"
confirmRememberKey="cancelStatus">
{{ 'contents.cancelStatus' | sqxTranslate }}
</button>
</div>
<div class="row">
<div class="col-8 offset-4">
<button type="button" class="btn btn-outline-danger" [class.disabled]="!content.canCancelStatus"
(sqxConfirmClick)="cancelStatus()"
confirmTitle="i18n:contents.cancelStatusConfirmTitle"
confirmText="i18n:contents.cancelStatusConfirmText"
confirmRememberKey="cancelStatus">
{{ 'contents.cancelStatus' | sqxTranslate }}
</button>
</div>
</ng-container>
</div>
</ng-container>
</sqx-modal-dialog>
</ng-container>
</div>
</ng-container>
</div>
</ng-container>
</sqx-modal-dialog>

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

@ -1,3 +1,3 @@
<sqx-layout layout="right" titleText="i18n:comments.title" [width]="20" [white]="true">
<sqx-layout layout="right" titleText="i18n:comments.title" width="20" white="true">
<sqx-comments [commentsId]="commentsId | async"></sqx-comments>
</sqx-layout>

6
frontend/src/app/features/content/pages/content/content-event.component.ts

@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
import { ContentDto, HistoryEventDto, TypedSimpleChanges } from '@app/shared';
@Component({
selector: 'sqx-content-event[content][event]',
selector: 'sqx-content-event',
styleUrls: ['./content-event.component.scss'],
templateUrl: './content-event.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -21,10 +21,10 @@ export class ContentEventComponent {
@Output()
public dataCompare = new EventEmitter();
@Input()
@Input({ required: true })
public event!: HistoryEventDto;
@Input()
@Input({ required: true })
public content!: ContentDto;
public canLoadOrCompare = false;

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

@ -1,4 +1,4 @@
<sqx-layout layout="right" titleText="i18n:common.workflow" [width]="20" [white]="true" [overflow]="true" [padding]="true">
<sqx-layout layout="right" titleText="i18n:common.workflow" width="20" white="true" overflow="true" padding="true">
<ng-container>
<div class="section mb-4">
<label for="id">{{ 'common.id' | sqxTranslate }}</label>
@ -22,7 +22,7 @@
<ng-template #newVersion>
<label>{{ 'contents.draftStatus' | sqxTranslate }}</label>
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" #buttonOptions>
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdownNew.toggle()" #buttonOptions sqxTourStep="status">
<sqx-content-status
layout="multiline"
[status]="content.newStatus!"
@ -31,35 +31,33 @@
</sqx-content-status>
</button>
<ng-container *sqxModal="dropdownNew;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonOptions" [scrollY]="true">
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
{{ 'common.statusChangeTo' | sqxTranslate }} <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a>
<div class="dropdown-divider"></div>
</ng-container>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDraftDelete"
(sqxConfirmClick)="deleteDraft()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteVersionConfirmText"
confirmRememberKey="deleteDraft">
{{ 'contents.versionDelete' | sqxTranslate }}
<sqx-dropdown-menu *sqxModal="dropdownNew;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
{{ 'common.statusChangeTo' | sqxTranslate }} <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
</ng-container>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDraftDelete"
(sqxConfirmClick)="deleteDraft()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteVersionConfirmText"
confirmRememberKey="deleteDraft">
{{ 'contents.versionDelete' | sqxTranslate }}
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-template>
</div>
@ -67,50 +65,50 @@
<label>{{ 'contents.currentStatusLabel' | sqxTranslate }}</label>
<div *ngIf="!content.newStatus; else newStatusOld">
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" #buttonOptions>
<sqx-content-status [small]="true"
<button type="button" class="btn btn-outline-secondary btn-block btn-status" (click)="dropdown.toggle()" #buttonOptions sqxTourStep="status">
<sqx-content-status
layout="multiline"
[status]="content.status"
[statusColor]="content.statusColor"
[scheduled]="content.scheduleJob">
[scheduled]="content.scheduleJob"
small="true">
</sqx-content-status>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonOptions" [scrollY]="true">
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
{{ 'common.statusChangeTo' | sqxTranslate }}
<sqx-content-status [small]="true"
layout="text"
[status]="info.status"
[statusColor]="info.color">
</sqx-content-status>
</a>
<div class="dropdown-divider"></div>
</ng-container>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canCancelStatus"
(sqxConfirmClick)="cancelStatus()"
confirmTitle="i18n:contents.cancelStatusConfirmTitle"
confirmText="i18n:contents.cancelStatusConfirmText"
confirmRememberKey="cancelStatus">
{{ 'contents.cancelStatus' | sqxTranslate }}
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<ng-container *ngIf="content.statusUpdates.length > 0">
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="changeStatus(info.status)">
{{ 'common.statusChangeTo' | sqxTranslate }}
<sqx-content-status
layout="text"
[status]="info.status"
[statusColor]="info.color"
small="true">
</sqx-content-status>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
<div class="dropdown-divider"></div>
</ng-container>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canCancelStatus"
(sqxConfirmClick)="cancelStatus()"
confirmTitle="i18n:contents.cancelStatusConfirmTitle"
confirmText="i18n:contents.cancelStatusConfirmText"
confirmRememberKey="cancelStatus">
{{ 'contents.cancelStatus' | sqxTranslate }}
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</div>
<ng-template #newStatusOld>

71
frontend/src/app/features/content/pages/content/content-page.component.html

@ -67,17 +67,15 @@
<i class="icon-dots"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonOptions" [scrollY]="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
<sqx-dropdown-menu *sqxModal="dropdown;closeAlways:true" [sqxAnchoredTo]="buttonOptions" scrollY="true">
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</ng-container>
<ng-container *ngIf="contentTab | async; let tab">
@ -87,7 +85,7 @@
<sqx-preview-button [schema]="schema" [content]="content" [confirm]="confirmPreview"></sqx-preview-button>
<ng-container *ngIf="content?.canUpdate">
<button type="submit" class="btn btn-primary ms-2" shortcut="CTRL + SHIFT + S">
<button type="submit" class="btn btn-primary ms-2" shortcut="CTRL + SHIFT + S" sqxTourStep="saveContent">
{{ 'common.save' | sqxTranslate }}
</button>
</ng-container>
@ -108,13 +106,15 @@
[percents]="contentForm.translationStatus | async">
</sqx-language-selector>
<button type="button" class="btn btn-primary ms-2" (click)="save()" *ngIf="contentsState.canCreate | async">
{{ 'common.save' | sqxTranslate }}
</button>
<button type="submit" class="btn btn-success ms-2" shortcut="CTRL + SHIFT + S" *ngIf="contentsState.canCreateAndPublish | async">
{{ 'contents.saveAndPublish' | sqxTranslate }}
</button>
<div sqxTourStep="saveContent">
<button type="button" class="btn btn-primary ms-2" (click)="save()" *ngIf="contentsState.canCreate | async">
{{ 'common.save' | sqxTranslate }}
</button>
<button type="submit" class="btn btn-success ms-2" shortcut="CTRL + SHIFT + S" *ngIf="contentsState.canCreateAndPublish | async">
{{ 'contents.saveAndPublish' | sqxTranslate }}
</button>
</div>
</ng-template>
</div>
</ng-container>
@ -174,21 +174,40 @@
<ng-container sidebarMenu>
<div class="panel-nav">
<a class="panel-link" routerLink="history" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.workflow" titlePosition="left-center" #linkHistory>
<a class="panel-link"
routerLink="history"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.workflow"
titlePosition="left"
sqxTourStep="history"
#linkHistory>
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="comments" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.comments" titlePosition="left-center">
<a class="panel-link"
routerLink="comments"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.comments"
titlePosition="left"
hintText="common.sidebarTour"
hintAfter="120000"
sqxTourStep="comments">
<i class="icon-comments"></i>
</a>
<a class="panel-link" routerLink="sidebar" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.sidebar" titlePosition="left-center" *ngIf="schema.properties.contentSidebarUrl">
<a class="panel-link"
routerLink="sidebar"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.sidebar"
titlePosition="left"
sqxTourStep="plugin"
*ngIf="schema.properties.contentSidebarUrl">
<i class="icon-plugin"></i>
</a>
<sqx-onboarding-tooltip helpId="history" [for]="linkHistory" position="left-top" [after]="120000">
{{ 'common.sidebarTour' | sqxTranslate }}
</sqx-onboarding-tooltip>
</div>
</ng-container>
</sqx-layout>

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

@ -1,4 +1,4 @@
<sqx-form-error [bubble]="true" [closeable]="true" [error]="(contentForm.error | async)"></sqx-form-error>
<sqx-form-error bubble="true" closeable="true" [error]="(contentForm.error | async)"></sqx-form-error>
<sqx-list-view>
<ng-container topHeader>

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

@ -5,11 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, RootFieldDto, SchemaDto, Version } from '@app/shared';
@Component({
selector: 'sqx-content-editor[contentId][contentForm][formContext][language][languages][schema]',
selector: 'sqx-content-editor',
styleUrls: ['./content-editor.component.scss'],
templateUrl: './content-editor.component.html',
})
@ -23,19 +23,19 @@ export class ContentEditorComponent {
@Output()
public contentIdChange = new EventEmitter<string>();
@Input()
@Input({ transform: booleanAttribute })
public isNew = false;
@Input()
@Input({ transform: booleanAttribute })
public isDeleted?: boolean;
@Input()
@Input({ transform: booleanAttribute })
public showIdInput = false;
@Input()
@Input({ required: true })
public contentId!: string;
@Input()
@Input({ required: true })
public contentForm!: EditContentForm;
@Input()
@ -44,16 +44,16 @@ export class ContentEditorComponent {
@Input()
public contentFormCompare?: EditContentForm | null;
@Input()
@Input({ required: true })
public schema!: SchemaDto;
@Input()
@Input({ required: true })
public formContext!: any;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
public trackBySection(_index: number, section: FieldSection<RootFieldDto, FieldForm>) {

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

@ -5,12 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
import { booleanAttribute, Component, EventEmitter, HostBinding, Input, numberAttribute, Output } from '@angular/core';
import { Observable } from 'rxjs';
import { AppLanguageDto, AppsState, changed$, disabled$, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, TranslationsService, TypedSimpleChanges } from '@app/shared';
@Component({
selector: 'sqx-content-field[form][formContext][formLevel][formModel][language][languages][schema]',
selector: 'sqx-content-field',
styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html',
})
@ -18,7 +18,7 @@ export class ContentFieldComponent {
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Input()
@Input({ transform: booleanAttribute })
public isCompact?: boolean | null;
@Input()
@ -27,25 +27,25 @@ export class ContentFieldComponent {
@Input()
public formCompare?: EditContentForm | null;
@Input()
@Input({ required: true })
public formContext!: any;
@Input()
@Input({ required: true, transform: numberAttribute })
public formLevel!: number;
@Input()
@Input({ required: true })
public formModel!: FieldForm;
@Input()
public formModelCompare?: FieldForm;
@Input()
@Input({ required: true })
public schema!: SchemaDto;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
public showAllControls = false;

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

@ -10,7 +10,7 @@
<h3>{{separator.displayName}}</h3>
<sqx-form-hint *ngIf="separator.properties.hints && separator.properties.hints.length > 0">
<span [sqxMarkdown]="separator.properties.hints" [optional]="true" [inline]="true"></span>
<span [sqxMarkdown]="separator.properties.hints" optional="true" inline="true"></span>
</sqx-form-hint>
</div>
</div>

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output } from '@angular/core';
import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto, Settings, StatefulComponent, TypedSimpleChanges } from '@app/shared';
interface State {
@ -14,7 +14,7 @@ interface State {
}
@Component({
selector: 'sqx-content-section[form][formContext][formLevel][formSection][language][languages][schema]',
selector: 'sqx-content-section',
styleUrls: ['./content-section.component.scss'],
templateUrl: './content-section.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -23,43 +23,41 @@ export class ContentSectionComponent extends StatefulComponent<State> {
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Input()
@Input({ transform: booleanAttribute })
public isCompact?: boolean | null;
@Input()
@Input({ required: true })
public form!: EditContentForm;
@Input()
public formCompare?: EditContentForm | null;
@Input()
@Input({ required: true, transform: numberAttribute })
public formLevel!: number;
@Input()
@Input({ required: true })
public formContext!: any;
@Input()
@Input({ required: true })
public formSection!: FieldSection<RootFieldDto, FieldForm>;
@Input()
@Input({ required: true })
public schema!: SchemaDto;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
constructor(changeDetector: ChangeDetectorRef,
constructor(
private readonly localStore: LocalStoreService,
) {
super(changeDetector, {
isCollapsed: false,
});
super({ isCollapsed: false });
this.changes.subscribe(state => {
this.project(x => x.isCollapsed).subscribe(isCollapsed => {
if (this.formSection?.separator && this.schema) {
this.localStore.setBoolean(this.isCollapsedKey(), state.isCollapsed);
this.localStore.setBoolean(this.isCollapsedKey(), isCollapsed);
}
});
}

52
frontend/src/app/features/content/pages/content/editor/field-copy-button.component.html

@ -3,37 +3,35 @@
<i class="icon-copy"></i>
</button>
<ng-container *sqxModal="dropdown">
<sqx-dropdown-menu [sqxAnchoredTo]="button" [scrollY]="true">
<div class="section d-flex justify-content-end">
<button type="button" class="btn btn-primary" (click)="copy()" tabindex="-1">
{{ 'common.copy' | sqxTranslate }}
</button>
</div>
<sqx-dropdown-menu *sqxModal="dropdown" [sqxAnchoredTo]="button" scrollY="true">
<div class="section d-flex justify-content-end">
<button type="button" class="btn btn-primary" (click)="copy()" tabindex="-1">
{{ 'common.copy' | sqxTranslate }}
</button>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-divider"></div>
<div class="section">
<div class="row">
<label class="col-auto col-form-label" for="languageSource">{{ 'common.from' | sqxTranslate }}</label>
<div class="col">
<select class="form-select" id="languagesSource"
[ngModel]="copySource"
(ngModelChange)="setCopySource($event)">
<option *ngFor="let language of languages" [ngValue]="language.iso2Code">{{language.iso2Code}}</option>
</select>
</div>
<div class="section">
<div class="row">
<label class="col-auto col-form-label" for="languageSource">{{ 'common.from' | sqxTranslate }}</label>
<div class="col">
<select class="form-select" id="languagesSource"
[ngModel]="copySource"
(ngModelChange)="setCopySource($event)">
<option *ngFor="let language of languages" [ngValue]="language.iso2Code">{{language.iso2Code}}</option>
</select>
</div>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-divider"></div>
<div class="section">
<label>{{ 'common.to' | sqxTranslate }}</label>
<sqx-checkbox-group [(ngModel)]="copyTargets" [values]="languageCodes" layout="Multiline"></sqx-checkbox-group>
</div>
</sqx-dropdown-menu>
</ng-container>
<div class="section">
<label>{{ 'common.to' | sqxTranslate }}</label>
<sqx-checkbox-group [(ngModel)]="copyTargets" [values]="languageCodes" layout="Multiline"></sqx-checkbox-group>
</div>
</sqx-dropdown-menu>
</ng-container>

6
frontend/src/app/features/content/pages/content/editor/field-copy-button.component.ts

@ -9,15 +9,15 @@ import { Component, Input } from '@angular/core';
import { AppLanguageDto, FieldForm, ModalModel, TypedSimpleChanges } from '@app/shared';
@Component({
selector: 'sqx-field-copy-button[formModel][languages]',
selector: 'sqx-field-copy-button',
styleUrls: ['./field-copy-button.component.scss'],
templateUrl: './field-copy-button.component.html',
})
export class FieldCopyButtonComponent {
@Input()
@Input({ required: true })
public formModel!: FieldForm;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
public languageCodes: ReadonlyArray<string> = [];

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

@ -11,16 +11,17 @@
<ng-container *ngIf="formModel.field.properties.isComplexUI || !showAllControls">
<div class="button-container ms-1">
<sqx-language-selector size="sm" #buttonLanguages
<sqx-language-selector
size="sm"
[exists]="formModel.translationStatus | async"
(languageChange)="languageChange.emit($event)"
[language]="language"
[languages]="languages">
[languages]="languages"
hintText="contents.validationHint"
hintAfter="120000"
hintPosition="top-end"
sqxTourStep="languages">
</sqx-language-selector>
</div>
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="top-right" [after]="120000">
{{ 'contents.validationHint' | sqxTranslate }}
</sqx-onboarding-tooltip>
</ng-container>
</ng-container>

12
frontend/src/app/features/content/pages/content/editor/field-languages.component.ts

@ -5,11 +5,11 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppLanguageDto, FieldForm } from '@app/shared';
@Component({
selector: 'sqx-field-languages[formModel][language][languages]',
selector: 'sqx-field-languages',
styleUrls: ['./field-languages.component.scss'],
templateUrl: './field-languages.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -18,19 +18,19 @@ export class FieldLanguagesComponent {
@Output()
public showAllControlsChange = new EventEmitter<boolean>();
@Input()
@Input({ transform: booleanAttribute })
public showAllControls?: boolean | null;
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
@Input({ required: true })
public formModel!: FieldForm;
public toggleShowAllControls() {

4
frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.html

@ -1,4 +1,4 @@
<sqx-form-error [bubble]="true" [closeable]="true" [error]="contentError"></sqx-form-error>
<sqx-form-error bubble="true" closeable="true" [error]="contentError"></sqx-form-error>
<div class="inner-menu">
<ul class="nav nav-tabs2" *ngIf="mode | async; let currentMode">
@ -22,7 +22,7 @@
<div class="inner-main">
<sqx-code-editor
[borderless]="true"
borderless="true"
[ngModel]="actualData | async"
(ngModelChange)="setData($event)"
valueMode="Json">

10
frontend/src/app/features/content/pages/content/inspecting/content-inspection.component.ts

@ -13,23 +13,23 @@ import { AppLanguageDto, ContentDto, ContentsService, ContentsState, ErrorDto, T
type Mode = 'Content' | 'Data' | 'FlatData';
@Component({
selector: 'sqx-content-inspection[appName][content][language][languages]',
selector: 'sqx-content-inspection',
styleUrls: ['./content-inspection.component.scss'],
templateUrl: './content-inspection.component.html',
})
export class ContentInspectionComponent implements OnDestroy {
private languageChanges$ = new BehaviorSubject<AppLanguageDto | null>(null);
@Input()
@Input({ required: true })
public appName!: string;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
@Input({ required: true })
public content!: ContentDto;
public mode = new BehaviorSubject<Mode>('Content');

2
frontend/src/app/features/content/pages/content/references/content-references.component.html

@ -1,4 +1,4 @@
<sqx-list-view [isLoading]="contentsState.isLoading | async" [table]="true">
<sqx-list-view [isLoading]="contentsState.isLoading | async" table="true">
<ng-container>
<table class="table table-items table-fixed" *ngIf="contentsState.contents | async; let contents">
<tbody *ngFor="let content of contents; trackBy: trackByContent"

8
frontend/src/app/features/content/pages/content/references/content-references.component.ts

@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy
import { AppLanguageDto, ComponentContentsState, ContentDto, QuerySynchronizer, Router2State, ToolbarService, TypedSimpleChanges } from '@app/shared';
@Component({
selector: 'sqx-content-references[content][language][languages]',
selector: 'sqx-content-references',
styleUrls: ['./content-references.component.scss'],
templateUrl: './content-references.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -18,13 +18,13 @@ import { AppLanguageDto, ComponentContentsState, ContentDto, QuerySynchronizer,
],
})
export class ContentReferencesComponent implements OnInit, OnDestroy {
@Input()
@Input({ required: true })
public content!: ContentDto;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()

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

@ -1,4 +1,4 @@
<sqx-layout titleText="i18n:common.filters" [width]="20" [white]="true" [padding]="true" [overflow]="true">
<sqx-layout titleText="i18n:common.filters" width="20" white="true" padding="true" overflow="true">
<ng-container *ngIf="schemaQueries | async; let queries">
<sqx-query-list
[types]="'common.contents' | sqxTranslate"

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

@ -4,7 +4,7 @@
<ng-container menu>
<div class="row flex-nowrap flex-grow-1 gx-2">
<div class="col-auto ms-8">
<sqx-notifo topic="apps/{{contentsState.appId}}/schemas/{{schema.id}}/contents"></sqx-notifo>
<sqx-notifo topic="apps/{{contentsState.appId}}/schemas/{{schema.id}}/contents" position="bottom-left"></sqx-notifo>
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="i18n:contents.refreshTooltip" shortcut="CTRL + B">
<i class="icon-reset"></i> {{ 'common.refresh' | sqxTranslate }}
@ -12,7 +12,7 @@
</div>
<div class="col">
<sqx-search-form formClass="form" placeholder="{{ 'contents.searchPlaceholder' | sqxTranslate }}"
[enableShortcut]="true"
enableShortcut="true"
[language]="(languagesState.isoMasterLanguage | async)!"
[languages]="languages"
[queries]="queries | async"
@ -32,7 +32,7 @@
</sqx-language-selector>
</div>
<div class="col-auto">
<button type="button" class="btn btn-success" routerLink="new" title="i18n:contents.createContentTooltip" shortcut="CTRL + U" [disabled]="(contentsState.canCreateAny | async) === false">
<button type="button" class="btn btn-success" routerLink="new" title="i18n:contents.createContentTooltip" shortcut="CTRL + U" [disabled]="(contentsState.canCreateAny | async) === false" sqxTourStep="addContent">
<i class="icon-plus"></i> {{ 'contents.create' | sqxTranslate }}
</button>
</div>
@ -42,7 +42,7 @@
<ng-container>
<ng-container *ngIf="tableSettings | async; let tableSettings">
<ng-container *ngIf="tableSettings.listFields | async; let tableFields">
<sqx-list-view [isLoading]="contentsState.isLoading | async" [syncedHeader]="true" [tableNoPadding]="true">
<sqx-list-view [isLoading]="contentsState.isLoading | async" syncedHeader="true" tableNoPadding="true">
<ng-container topHeader>
<div class="selection" *ngIf="selectionCount > 0">
{{ 'contents.selectionCount' | sqxTranslate: { count: selectionCount } }}&nbsp;&nbsp;
@ -68,16 +68,14 @@
<i class="icon-settings"></i>
</button>
<ng-container *sqxModal="tableViewModal">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonSettings" [scrollY]="true" position="bottom-right">
<sqx-custom-view-editor
[allFields]="tableSettings.schemaFields"
(listFieldsChange)="tableSettings.updateFields($event)"
[listFields]="$any(tableFields)"
(reset)="tableSettings.reset()">
</sqx-custom-view-editor>
</sqx-dropdown-menu>
</ng-container>
<sqx-dropdown-menu *sqxModal="tableViewModal" [sqxAnchoredTo]="buttonSettings" scrollY="true" position="bottom-end">
<sqx-custom-view-editor
[allFields]="tableSettings.schemaFields"
(listFieldsChange)="tableSettings.updateFields($event)"
[listFields]="$any(tableFields)"
(reset)="tableSettings.reset()">
</sqx-custom-view-editor>
</sqx-dropdown-menu>
</div>
</ng-container>
@ -147,11 +145,24 @@
<ng-container sidebarMenu>
<div class="panel-nav">
<a class="panel-link" routerLink="filters" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.filters" titlePosition="left-center">
<a class="panel-link"
routerLink="filters"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.filters"
titlePosition="left"
sqxTourStep="filters">
<i class="icon-filter"></i>
</a>
<a class="panel-link" routerLink="sidebar" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.sidebar" titlePosition="left-center" *ngIf="schema.properties.contentsSidebarUrl">
<a class="panel-link"
routerLink="sidebar"
routerLinkActive="active"
queryParamsHandling="preserve"
title="i18n:common.sidebar"
titlePosition="left"
sqxTourStep="plugin"
*ngIf="schema.properties.contentsSidebarUrl">
<i class="icon-plugin"></i>
</a>
</div>

6
frontend/src/app/features/content/pages/contents/custom-view-editor.component.ts

@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
import { TableField } from '@app/shared';
@Component({
selector: 'sqx-custom-view-editor[allFields][listFields]',
selector: 'sqx-custom-view-editor',
styleUrls: ['./custom-view-editor.component.scss'],
templateUrl: './custom-view-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -22,10 +22,10 @@ export class CustomViewEditorComponent {
@Output()
public listFieldsChange = new EventEmitter<ReadonlyArray<TableField>>();
@Input()
@Input({ required: true })
public listFields!: TableField[];
@Input()
@Input({ required: true })
public allFields!: ReadonlyArray<TableField>;
public fieldsNotAdded!: ReadonlyArray<TableField>;

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

@ -19,7 +19,7 @@
</ng-container>
<ng-container>
<sqx-list-view [isLoading]="contentsState.isLoading | async" [table]="true">
<sqx-list-view [isLoading]="contentsState.isLoading | async" table="true">
<ng-container>
<table class="table table-items table-fixed" *ngIf="contentsState.contents | async; let contents">
<tbody *ngFor="let content of contents; trackBy: trackByContent"

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

@ -1,6 +1,6 @@
<sqx-title message="i18n:contents.schemasPageTitle"></sqx-title>
<sqx-layout layout="left" titleCollapsed="i18n:common.schemas" [width]="18" [white]="true" [padding]="true" [overflow]="true" *ngIf="!isEmbedded">
<sqx-layout layout="left" titleCollapsed="i18n:common.schemas" width="18" white="true" padding="true" overflow="true" *ngIf="!isEmbedded">
<ng-container menu>
<div class="search-form">
<input class="form-control" [formControl]="schemasFilter" placeholder="{{ 'contents.searchSchemasPlaceholder' | sqxTranslate }}">

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

@ -1,4 +1,4 @@
<sqx-layout titleText="i18n:common.sidebar" [width]="20" [white]="true" [padding]="true" [overflow]="true">
<sqx-layout titleText="i18n:common.sidebar" width="20" white="true" padding="true" overflow="true">
<sqx-content-extension
[url]="url | async"
[content]="contentsState.selectedContent | async"

6
frontend/src/app/features/content/shared/content-extension.component.ts

@ -11,7 +11,7 @@ import { ApiUrlConfig, ResourceOwner, Types } from '@app/framework/internal';
import { AppsState, AuthService, computeEditorUrl, ContentDto, SchemaDto, TypedSimpleChanges } from '@app/shared';
@Component({
selector: 'sqx-content-extension[content][contentSchema]',
selector: 'sqx-content-extension',
styleUrls: ['./content-extension.component.scss'],
templateUrl: './content-extension.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -23,10 +23,10 @@ export class ContentExtensionComponent extends ResourceOwner {
@ViewChild('iframe', { static: false })
public iframe!: ElementRef<HTMLIFrameElement>;
@Input()
@Input({ required: true })
public content?: ContentDto | null;
@Input()
@Input({ required: true })
public contentSchema!: SchemaDto;
@Input()

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

@ -1,35 +1,33 @@
<ng-container *sqxModal="dueTimeDialog">
<sqx-modal-dialog (close)="cancelStatusChange()">
<ng-container title>
{{ 'contents.changeStatusTo' | sqxTranslate: { action: dueTimeAction } }}
</ng-container>
<ng-container content>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode">
<label class="form-check-label" for="immediately">
{{ 'contents.changeStatusToImmediately' | sqxTranslate: { action: dueTimeAction } }}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode">
<label class="form-check-label" for="scheduled">
{{ 'contents.changeStatusToLater' | sqxTranslate: { action: dueTimeAction } }}
</label>
</div>
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" [enforceTime]="true" mode="DateTime" [hideClear]="true" [(ngModel)]="dueTime"></sqx-date-time-editor>
</ng-container>
<ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="cancelStatusChange()">
{{ 'common.cancel' | sqxTranslate }}
</button>
<button type="button" class="btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit>
{{ 'common.confirm' | sqxTranslate }}
</button>
</ng-container>
</sqx-modal-dialog>
</ng-container>
<sqx-modal-dialog *sqxModal="dueTimeDialog" (close)="cancelStatusChange()">
<ng-container title>
{{ 'contents.changeStatusTo' | sqxTranslate: { action: dueTimeAction } }}
</ng-container>
<ng-container content>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately" name="dueTimeMode">
<label class="form-check-label" for="immediately">
{{ 'contents.changeStatusToImmediately' | sqxTranslate: { action: dueTimeAction } }}
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled" name="dueTimeMode">
<label class="form-check-label" for="scheduled">
{{ 'contents.changeStatusToLater' | sqxTranslate: { action: dueTimeAction } }}
</label>
</div>
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" enforceTime="true" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor>
</ng-container>
<ng-container footer>
<button type="button" class="btn btn-text-secondary" (click)="cancelStatusChange()">
{{ 'common.cancel' | sqxTranslate }}
</button>
<button type="button" class="btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()" sqxFocusOnInit>
{{ 'common.confirm' | sqxTranslate }}
</button>
</ng-container>
</sqx-modal-dialog>

4
frontend/src/app/features/content/shared/due-time-selector.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input } from '@angular/core';
import { booleanAttribute, Component, Input } from '@angular/core';
import { Observable, of, Subject } from 'rxjs';
import { DialogModel } from '@app/shared';
@ -19,7 +19,7 @@ const OPTION_IMMEDIATELY = 'Immediately';
export class DueTimeSelectorComponent {
private dueTimeResult?: Subject<string | null>;
@Input()
@Input({ transform: booleanAttribute })
public disabled?: boolean | null;
public dueTimeDialog = new DialogModel();

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

@ -81,13 +81,11 @@
{{ 'contents.addComponent' | sqxTranslate}}
</button>
<ng-container *sqxModal="schemasDropdown;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonSelect" [scrollY]="true">
<a class="dropdown-item" *ngFor="let schema of schemasList" (click)="addComponent(schema)">
{{schema.displayName}}
</a>
</sqx-dropdown-menu>
</ng-container>
<sqx-dropdown-menu *sqxModal="schemasDropdown;closeAlways:true" [sqxAnchoredTo]="buttonSelect" scrollY="true">
<a class="dropdown-item" *ngFor="let schema of schemasList" (click)="addComponent(schema)">
{{schema.displayName}}
</a>
</sqx-dropdown-menu>
</ng-container>
<ng-container *ngIf="schemasList.length === 1">
<button type="button" class="btn btn-outline-success" [disabled]="isDisabledOrFull | async" (click)="addComponent(schemasList[0])">

28
frontend/src/app/features/content/shared/forms/array-editor.component.ts

@ -6,45 +6,45 @@
*/
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core';
import { VirtualScrollerComponent } from 'ngx-virtual-scroller';
import { booleanAttribute, ChangeDetectionStrategy, Component, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core';
import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppLanguageDto, ComponentsFieldPropertiesDto, disabled$, EditContentForm, FieldArrayForm, LocalStoreService, ModalModel, ObjectFormBase, SchemaDto, Settings, sorted, TypedSimpleChanges, Types } from '@app/shared';
import { ArrayItemComponent } from './array-item.component';
@Component({
selector: 'sqx-array-editor[form][formContext][formLevel][formModel][language][languages][isComparing]',
selector: 'sqx-array-editor',
styleUrls: ['./array-editor.component.scss'],
templateUrl: './array-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArrayEditorComponent {
@Input()
@Input({ required: true })
public form!: EditContentForm;
@Input()
@Input({ required: true })
public formContext!: any;
@Input()
@Input({ required: true, transform: numberAttribute })
public formLevel!: number;
@Input()
@Input({ required: true })
public formModel!: FieldArrayForm;
@Input()
@Input({ required: true, transform: booleanAttribute })
public isComparing = false;
@Input()
@Input({ transform: booleanAttribute })
public isExpanded = false;
@Input()
@Input({ transform: booleanAttribute })
public canUnset?: boolean | null;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@ViewChildren(ArrayItemComponent)
@ -63,7 +63,7 @@ export class ArrayEditorComponent {
public isCollapsedInitial = false;
public get hasField() {
return this.formModel.field['nested']?.length > 0;
return (this.formModel.field as any)['nested']?.length > 0;
}
constructor(
@ -73,7 +73,7 @@ export class ArrayEditorComponent {
public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes.formModel) {
const maxItems = this.formModel.field.properties['maxItems'] || Number.MAX_VALUE;
const maxItems = (this.formModel.field.properties as any)['maxItems'] || Number.MAX_VALUE;
if (Types.is(this.formModel.field.properties, ComponentsFieldPropertiesDto)) {
this.schemasList = this.formModel.field.properties.schemaIds?.map(x => this.formModel.globals.schemas[x]).defined().sortedByString(x => x.displayName) || [];

30
frontend/src/app/features/content/shared/forms/array-item.component.ts

@ -5,14 +5,14 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, EventEmitter, Input, numberAttribute, Output, QueryList, ViewChildren } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppLanguageDto, ComponentForm, EditContentForm, FieldDto, FieldFormatter, FieldSection, invalid$, ObjectFormBase, RootFieldDto, TypedSimpleChanges, Types, valueProjection$ } from '@app/shared';
import { ComponentSectionComponent } from './component-section.component';
@Component({
selector: 'sqx-array-item[form][formContext][formLevel][formModel][index][isComparing][language][languages]',
selector: 'sqx-array-item',
styleUrls: ['./array-item.component.scss'],
templateUrl: './array-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -30,43 +30,43 @@ export class ArrayItemComponent {
@Output()
public clone = new EventEmitter();
@Input()
@Input({ required: true })
public form!: EditContentForm;
@Input()
@Input({ required: true })
public formContext!: any;
@Input()
@Input({ required: true, transform: numberAttribute })
public formLevel!: number;
@Input()
@Input({ required: true })
public formModel!: ObjectFormBase;
@Input()
@Input({ transform: booleanAttribute })
public canUnset?: boolean | null;
@Input()
@Input({ required: true, transform: booleanAttribute })
public isComparing = false;
@Input()
@Input({ transform: booleanAttribute })
public isCollapsedInitial = false;
@Input()
@Input({ transform: booleanAttribute })
public isFirst?: boolean | null;
@Input()
@Input({ transform: booleanAttribute })
public isLast?: boolean | null;
@Input()
@Input({ transform: booleanAttribute })
public isDisabled?: boolean | null;
@Input()
@Input({ required: true, transform: numberAttribute })
public index!: number;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@ViewChildren(ComponentSectionComponent)

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

@ -23,7 +23,7 @@
</div>
</div>
<div class="body" (sqxResizeCondition)="setCompact($event)" [sqxResizeMinWidth]="600" [sqxResizeMaxWidth]="0">
<div class="body" (sqxResizeCondition)="setCompact($event)" sqxResizeMinWidth="600" sqxResizeMaxWidth="0">
<ng-container *ngIf="!snapshot.isListView; else listTemplate">
<div class="row g-0">
<sqx-asset *ngFor="let file of snapshot.assetFiles"
@ -40,7 +40,7 @@
(edit)="editStart($event)"
[isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact"
[removeMode]="true"
removeMode="true"
(remove)="removeLoadedAsset(asset)"
(update)="notifyOthers(asset)">
</sqx-asset>
@ -54,7 +54,7 @@
[folderId]="folderId"
[isCompact]="snapshot.isCompact"
[isDisabled]="snapshot.isDisabled"
[isListView]="true"
isListView="true"
(loadDone)="addAsset(file, $event)"
(loadError)="removeLoadingAsset(file)">
</sqx-asset>
@ -67,11 +67,11 @@
<sqx-asset
[asset]="asset"
(edit)="editStart($event)"
[isListView]="true"
isListView="true"
[isDisabled]="snapshot.isDisabled"
[isCompact]="snapshot.isCompact"
(update)="notifyOthers(asset)"
[removeMode]="true"
removeMode="true"
(remove)="removeLoadedAsset(asset)">
</sqx-asset>
</div>
@ -81,13 +81,13 @@
</div>
</div>
<ng-container *sqxModal="assetsDialog">
<sqx-asset-selector (select)="selectAssets($event)"></sqx-asset-selector>
</ng-container>
<sqx-asset-selector *sqxModal="assetsDialog"
(select)="selectAssets($event)">
</sqx-asset-selector>
<sqx-asset-dialog *sqxModal="snapshot.editAsset;isDialog:true"
[asset]="snapshot.editAsset!"
(assetReplaced)="notifyOthers($event)"
(assetUpdated)="notifyOthers($event)"
(complete)="editDone()">
(close)="editDone()">
</sqx-asset-dialog>

15
frontend/src/app/features/content/shared/forms/assets-editor.component.ts

@ -6,9 +6,8 @@
*/
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { AssetDto, DialogModel, LocalStoreService, MessageBus, ResolveAssets, Settings, sorted, StatefulControlComponent, Types } from '@app/shared';
export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
@ -53,28 +52,28 @@ export class AssetsEditorComponent extends StatefulControlComponent<State, Reado
@Input()
public folderId?: string;
@Input()
@Input({ transform: booleanAttribute })
public isExpanded = false;
@Input()
@Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) {
this.setDisabledState(value === true);
}
public assetsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef, localStore: LocalStoreService,
constructor(localStore: LocalStoreService,
private readonly assetsResolver: ResolveAssets,
private readonly messageBus: MessageBus,
) {
super(changeDetector, {
super({
assets: [],
assetFiles: [],
isListView: localStore.getBoolean(Settings.Local.ASSETS_MODE),
});
this.changes.pipe(map(x => x.isListView), distinctUntilChanged()).subscribe(value => {
localStore.setBoolean(Settings.Local.ASSETS_MODE, value);
this.project(x => x.isListView).subscribe(isListView => {
localStore.setBoolean(Settings.Local.ASSETS_MODE, isListView);
});
}

6
frontend/src/app/features/content/shared/forms/chat-dialog.component.html

@ -4,8 +4,8 @@
</ng-container>
<ng-container content>
<sqx-form-alert [marginBottom]="4">
<span [sqxMarkdown]="'chat.description' | sqxTranslate" [inline]="true"></span>
<sqx-form-alert marginBottom="4">
<span [sqxMarkdown]="'chat.description' | sqxTranslate" inline="true"></span>
</sqx-form-alert>
<form (ngSubmit)="ask()">
@ -39,7 +39,7 @@
<textarea class="form-control" readonly [ngModel]="answer"></textarea>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary" (click)="complete.emit(answer)">
<button type="submit" class="btn btn-primary" (click)="select.emit(answer)">
{{ 'chat.use' | sqxTranslate }}
</button>
</div>

8
frontend/src/app/features/content/shared/forms/chat-dialog.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectorRef, Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Output } from '@angular/core';
import { AppsState, StatefulComponent, TranslationsService } from '@app/shared';
interface State {
@ -29,13 +29,13 @@ export class ChatDialogComponent extends StatefulComponent<State> {
public close = new EventEmitter();
@Output()
public complete = new EventEmitter<string>();
public select = new EventEmitter<string>();
constructor(changeDetector: ChangeDetectorRef,
constructor(
private readonly appsState: AppsState,
private readonly translator: TranslationsService,
) {
super(changeDetector, {
super({
isRunning: false,
chatQuestion: '',
chatAnswers: undefined,

2
frontend/src/app/features/content/shared/forms/component-section.component.html

@ -3,7 +3,7 @@
<h3>{{separator!.displayName}}</h3>
<sqx-form-hint *ngIf="separator.properties.hints && separator.properties.hints.length > 0">
<span [sqxMarkdown]="separator.properties.hints" [optional]="true" [inline]="true"></span>
<span [sqxMarkdown]="separator.properties.hints" optional="true" inline="true"></span>
</sqx-form-hint>
</div>

22
frontend/src/app/features/content/shared/forms/component-section.component.ts

@ -5,42 +5,42 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core';
import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, FieldSection } from '@app/shared';
import { FieldEditorComponent } from './field-editor.component';
@Component({
selector: 'sqx-component-section[form][formContext][formLevel][formSection][isComparing][language][languages]',
selector: 'sqx-component-section',
styleUrls: ['./component-section.component.scss'],
templateUrl: './component-section.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComponentSectionComponent {
@Input()
@Input({ required: true })
public form!: EditContentForm;
@Input()
@Input({ required: true })
public formContext!: any;
@Input()
@Input({ required: true, transform: numberAttribute })
public formLevel!: number;
@Input()
@Input({ required: true })
public formSection!: FieldSection<FieldDto, any>;
@Input()
@Input({ required: true, transform: booleanAttribute })
public isComparing = false;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
@Input({ transform: numberAttribute })
public index: number | null | undefined;
@Input()
@Input({ transform: booleanAttribute })
public canUnset?: boolean | null;
@ViewChildren(FieldEditorComponent)

12
frontend/src/app/features/content/shared/forms/component.component.html

@ -24,13 +24,11 @@
{{ 'contents.addComponent' | sqxTranslate}}
</button>
<ng-container *sqxModal="schemasDropdown;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="buttonSelect" [scrollY]="true">
<a class="dropdown-item" *ngFor="let schema of schemasList" (click)="setSchema(schema)">
{{schema.displayName}}
</a>
</sqx-dropdown-menu>
</ng-container>
<sqx-dropdown-menu *sqxModal="schemasDropdown;closeAlways:true" [sqxAnchoredTo]="buttonSelect" scrollY="true">
<a class="dropdown-item" *ngFor="let schema of schemasList" (click)="setSchema(schema)">
{{schema.displayName}}
</a>
</sqx-dropdown-menu>
</ng-container>
<ng-container *ngIf="schemasList.length === 1">
<button type="button" class="btn btn-outline-success" [disabled]="isDisabled | async" (click)="setSchema(schemasList[0])">

20
frontend/src/app/features/content/shared/forms/component.component.ts

@ -5,40 +5,40 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, QueryList, ViewChildren } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, numberAttribute, QueryList, ViewChildren } from '@angular/core';
import { Observable } from 'rxjs';
import { AppLanguageDto, ComponentFieldPropertiesDto, ComponentForm, disabled$, EditContentForm, FieldDto, FieldSection, ModalModel, ResourceOwner, SchemaDto, TypedSimpleChanges, Types } from '@app/shared';
import { ComponentSectionComponent } from './component-section.component';
@Component({
selector: 'sqx-component[form][formContext][formLevel][formModel][isComparing][language][languages]',
selector: 'sqx-component',
styleUrls: ['./component.component.scss'],
templateUrl: './component.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComponentComponent extends ResourceOwner {
@Input()
@Input({ transform: booleanAttribute })
public canUnset?: boolean | null;
@Input()
@Input({ required: true })
public form!: EditContentForm;
@Input()
@Input({ required: true })
public formContext!: any;
@Input()
@Input({ required: true, transform: numberAttribute })
public formLevel!: number;
@Input()
@Input({ required: true })
public formModel!: ComponentForm;
@Input()
@Input({ required: true, transform: booleanAttribute })
public isComparing = false;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@ViewChildren(ComponentSectionComponent)

19
frontend/src/app/features/content/shared/forms/field-editor.component.html

@ -100,13 +100,13 @@
</sqx-array-editor>
</ng-container>
<ng-container *ngSwitchCase="'DateTime'">
<sqx-date-time-editor [formControl]="$any(fieldForm)" [mode]="field.rawProperties.editor" [enforceTime]="true"></sqx-date-time-editor>
<sqx-date-time-editor [formControl]="$any(fieldForm)" [mode]="field.rawProperties.editor" enforceTime="true"></sqx-date-time-editor>
</ng-container>
<ng-container *ngSwitchCase="'Geolocation'">
<sqx-geolocation-editor [formControl]="$any(fieldForm)"></sqx-geolocation-editor>
</ng-container>
<ng-container *ngSwitchCase="'Json'">
<sqx-code-editor [formControl]="$any(fieldForm)" valueMode="Json" [height]="350"></sqx-code-editor>
<sqx-code-editor [formControl]="$any(fieldForm)" valueMode="Json" height="350"></sqx-code-editor>
</ng-container>
<ng-container *ngSwitchCase="'Number'">
<ng-container [ngSwitch]="field.rawProperties.editor">
@ -117,7 +117,7 @@
<sqx-stars [formControl]="$any(fieldForm)" [maximumStars]="field.rawProperties.maxValue"></sqx-stars>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<sqx-radio-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues" [unsorted]="true"></sqx-radio-group>
<sqx-radio-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues" unsorted="true"></sqx-radio-group>
</ng-container>
<ng-container *ngSwitchCase="'Dropdown'">
<select class="form-select" [formControl]="$any(fieldForm)">
@ -195,7 +195,7 @@
</sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Html'">
<sqx-code-editor [formControl]="$any(fieldForm)" #editor mode="ace/mode/html" [height]="350" ></sqx-code-editor>
<sqx-code-editor [formControl]="$any(fieldForm)" #editor mode="ace/mode/html" height="350" ></sqx-code-editor>
</ng-container>
<ng-container *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="$any(fieldForm)" #editor
@ -215,7 +215,7 @@
</select>
</ng-container>
<ng-container *ngSwitchCase="'Radio'">
<sqx-radio-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues" [unsorted]="true"></sqx-radio-group>
<sqx-radio-group [formControl]="$any(fieldForm)" [values]="field.rawProperties.allowedValues" unsorted="true"></sqx-radio-group>
</ng-container>
<ng-container *ngSwitchCase="'Color'">
<sqx-color-picker [formControl]="$any(fieldForm)" [placeholder]="field.displayPlaceholder"></sqx-color-picker>
@ -245,11 +245,10 @@
</div>
<sqx-form-hint *ngIf="field.properties.hints && field.properties.hints.length > 0">
<span [sqxMarkdown]="field.properties.hints" [optional]="true" [inline]="true"></span>
<span [sqxMarkdown]="field.properties.hints" optional="true" inline="true"></span>
</sqx-form-hint>
</div>
<ng-container *sqxModal="chatDialog">
<sqx-chat-dialog (close)="chatDialog.hide()" (complete)="setValue($event)"></sqx-chat-dialog>
</ng-container>
<sqx-chat-dialog *sqxModal="chatDialog"
(close)="chatDialog.hide()" (select)="setValue($event)">
</sqx-chat-dialog>

30
frontend/src/app/features/content/shared/forms/field-editor.component.ts

@ -5,13 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { booleanAttribute, Component, ElementRef, EventEmitter, Input, numberAttribute, Output, ViewChild } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { AbstractContentForm, AppLanguageDto, DialogModel, EditContentForm, FieldDto, hasNoValue$, MathHelper, TypedSimpleChanges, Types } from '@app/shared';
@Component({
selector: 'sqx-field-editor[form][formContext][formLevel][formModel][isComparing][language][languages]',
selector: 'sqx-field-editor',
styleUrls: ['./field-editor.component.scss'],
templateUrl: './field-editor.component.html',
})
@ -21,31 +21,31 @@ export class FieldEditorComponent {
@Output()
public expandedChange = new EventEmitter();
@Input()
@Input({ required: true })
public form!: EditContentForm;
@Input()
@Input({ required: true })
public formContext!: any;
@Input()
@Input({ required: true, transform: numberAttribute })
public formLevel!: number;
@Input()
@Input({ required: true })
public formModel!: AbstractContentForm<FieldDto, AbstractControl>;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
@Input({ transform: numberAttribute })
public index: number | null | undefined;
@Input()
@Input({ required: true, transform: booleanAttribute })
public isComparing = false;
@Input()
@Input({ transform: booleanAttribute })
public canUnset?: boolean | null;
@Input()
@ -74,15 +74,17 @@ export class FieldEditorComponent {
}
public reset() {
if (this.editor) {
const editor = this.editor as any;
if (editor) {
const nativeElement = this.editor.nativeElement;
if (nativeElement && Types.isFunction(nativeElement['reset'])) {
nativeElement['reset']();
}
if (this.editor && Types.isFunction(this.editor['reset'])) {
this.editor['reset']();
if (this.editor && Types.isFunction(editor['reset'])) {
editor['reset']();
}
}
}

22
frontend/src/app/features/content/shared/forms/iframe-editor.component.html

@ -4,17 +4,13 @@
</div>
</div>
<ng-container *sqxModal="assetsDialog">
<sqx-asset-selector
(select)="pickAssets($event)">
</sqx-asset-selector>
</ng-container>
<sqx-asset-selector *sqxModal="assetsDialog"
(select)="pickAssets($event)">
</sqx-asset-selector>
<ng-container *sqxModal="contentsDialog">
<sqx-content-selector
(select)="pickContents($event)"
[language]="language"
[languages]="languages"
[schemaNames]="contentsSchemas">
</sqx-content-selector>
</ng-container>
<sqx-content-selector *sqxModal="contentsDialog"
(select)="pickContents($event)"
[language]="language"
[languages]="languages"
[schemaNames]="contentsSchemas">
</sqx-content-selector>

28
frontend/src/app/features/content/shared/forms/iframe-editor.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, Output, Renderer2, ViewChild } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, numberAttribute, OnDestroy, Output, Renderer2, ViewChild } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogModel, DialogService, disabled$, StatefulComponent, TypedSimpleChanges, Types, value$ } from '@app/framework';
@ -17,7 +17,7 @@ interface State {
}
@Component({
selector: 'sqx-iframe-editor[context][formField][formIndex][formValue][formControlBinding][language][languages]',
selector: 'sqx-iframe-editor',
styleUrls: ['./iframe-editor.component.scss'],
templateUrl: './iframe-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -39,31 +39,31 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements
@Output()
public isExpandedChange = new EventEmitter();
@Input()
@Input({ transform: booleanAttribute })
public isExpanded = false;
@Input()
@Input({ required: true })
public context: any = {};
@Input()
@Input({ required: true })
public formValue!: any;
@Input()
@Input({ required: true })
public formField = '';
@Input()
@Input({ required: true, transform: numberAttribute })
public formIndex?: number | null;
@Input()
@Input({ required: true })
public language!: AppLanguageDto;
@Input()
@Input({ required: true })
public languages!: ReadonlyArray<AppLanguageDto>;
@Input()
@Input({ required: true })
public formControlBinding!: AbstractControl;
@Input()
@Input({ transform: booleanAttribute })
public set disabled(value: boolean | undefined | null) {
this.updatedisabled(value === true);
}
@ -82,15 +82,13 @@ export class IFrameEditorComponent extends StatefulComponent<State> implements
public contentsSchemas?: string[];
public contentsDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
constructor(
private readonly appsState: AppsState,
private readonly dialogs: DialogService,
private readonly renderer: Renderer2,
private readonly router: Router,
) {
super(changeDetector, {
isFullscreen: false,
});
super({ isFullscreen: false });
}
public ngOnDestroy() {

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

@ -17,36 +17,34 @@
<sqx-loader color="white" *ngIf="snapshot.thumbnailStatus !== 'Loaded'"></sqx-loader>
</div>
<ng-container *sqxModal="searchDialog">
<sqx-modal-dialog size="lg" [fullHeight]="true" (close)="searchDialog.hide()">
<ng-container title>
<input class="form-control search" [formControl]="stockPhotoSearch" sqxFocusOnInit placeholder="{{ 'contents.stockPhotoSearch' | sqxTranslate }}">
<sqx-loader *ngIf="snapshot.isLoading"></sqx-loader>
</ng-container>
<ng-container content>
<div class="photos">
<div *ngFor="let photo of snapshot.stockPhotos; trackBy: trackByPhoto" class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)">
<img [src]="photo.thumbUrl">
<div class="photo-user">
<a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick>
{{photo.user}}
</a>
</div>
<sqx-modal-dialog *sqxModal="searchDialog" size="lg" fullHeight="true" (close)="searchDialog.hide()">
<ng-container title>
<input class="form-control search" [formControl]="stockPhotoSearch" sqxFocusOnInit placeholder="{{ 'contents.stockPhotoSearch' | sqxTranslate }}">
<sqx-loader *ngIf="snapshot.isLoading"></sqx-loader>
</ng-container>
<ng-container content>
<div class="photos">
<div *ngFor="let photo of snapshot.stockPhotos; trackBy: trackByPhoto" class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)">
<img [src]="photo.thumbUrl">
<div class="photo-user">
<a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick>
{{photo.user}}
</a>
</div>
</div>
<div class="empty small text-muted text-center" *ngIf="snapshot.stockPhotos.length === 0">
{{ 'contents.stockPhotoSearchEmpty' | sqxTranslate }}
</div>
<div class="mt-4 text-center" *ngIf="snapshot.hasMore">
<button class="btn btn-outline-secondary" type="button" (click)="loadMore()" [disabled]="snapshot.isLoading">
{{ 'common.loadMore' | sqxTranslate }} <sqx-loader *ngIf="snapshot.isLoading"></sqx-loader>
</button>
</div>
</ng-container>
</sqx-modal-dialog>
</ng-container>
</div>
<div class="empty small text-muted text-center" *ngIf="snapshot.stockPhotos.length === 0">
{{ 'contents.stockPhotoSearchEmpty' | sqxTranslate }}
</div>
<div class="mt-4 text-center" *ngIf="snapshot.hasMore">
<button class="btn btn-outline-secondary" type="button" (click)="loadMore()" [disabled]="snapshot.isLoading">
{{ 'common.loadMore' | sqxTranslate }} <sqx-loader *ngIf="snapshot.isLoading"></sqx-loader>
</button>
</div>
</ng-container>
</sqx-modal-dialog>

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

Loading…
Cancel
Save