diff --git a/app/frontend/src/components/admin/AdminFormEmbed.vue b/app/frontend/src/components/admin/AdminFormEmbed.vue new file mode 100644 index 000000000..ba6720de4 --- /dev/null +++ b/app/frontend/src/components/admin/AdminFormEmbed.vue @@ -0,0 +1,483 @@ + + + diff --git a/app/frontend/src/components/admin/AdminPage.vue b/app/frontend/src/components/admin/AdminPage.vue index 63da3919e..80f3aac36 100644 --- a/app/frontend/src/components/admin/AdminPage.vue +++ b/app/frontend/src/components/admin/AdminPage.vue @@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n'; import AdminFormsTable from '~/components/admin/AdminFormsTable.vue'; import AdminUsersTable from '~/components/admin/AdminUsersTable.vue'; import AdminAPIsTable from '~/components/admin/AdminAPIsTable.vue'; +import AdminFormEmbed from '~/components/admin/AdminFormEmbed.vue'; import Dashboard from '~/components/admin/Dashboard.vue'; import Developer from '~/components/admin/Developer.vue'; import FormComponentsProactiveHelp from '~/components/admin/FormComponentsProactiveHelp.vue'; @@ -39,6 +40,9 @@ watch(isRTL, () => { $t('trans.adminPage.users') }} {{ $t('trans.adminPage.apis') }} + {{ + $t('trans.adminPage.formEmbedding') + }} {{ $t('trans.adminPage.developer') }} @@ -61,6 +65,9 @@ watch(isRTL, () => { + + + diff --git a/app/frontend/src/components/designer/FormViewer.vue b/app/frontend/src/components/designer/FormViewer.vue index 65c6ff9b3..90f771681 100644 --- a/app/frontend/src/components/designer/FormViewer.vue +++ b/app/frontend/src/components/designer/FormViewer.vue @@ -29,6 +29,7 @@ import { getDisposition, } from '~/utils/transformUtils'; import { FormPermissions, NotificationTypes } from '~/utils/constants'; +import { initFormEmbed, isFormEmbedded } from '~/utils/embedUtils'; const { t, locale } = useI18n({ useScope: 'global' }); @@ -199,6 +200,10 @@ onMounted(async () => { } window.addEventListener('beforeunload', beforeWindowUnload); + if (isFormEmbedded()) { + initFormEmbed(properties.formId); + } + reRenderFormIo.value += 1; }); diff --git a/app/frontend/src/components/forms/manage/Embed.vue b/app/frontend/src/components/forms/manage/Embed.vue new file mode 100644 index 000000000..a84c09d89 --- /dev/null +++ b/app/frontend/src/components/forms/manage/Embed.vue @@ -0,0 +1,339 @@ + + +/* c8 ignore start */ + +/* c8 ignore end */ diff --git a/app/frontend/src/components/forms/manage/ManageForm.vue b/app/frontend/src/components/forms/manage/ManageForm.vue index 013b388f1..d23e185c7 100644 --- a/app/frontend/src/components/forms/manage/ManageForm.vue +++ b/app/frontend/src/components/forms/manage/ManageForm.vue @@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n'; import ApiKey from '~/components/forms/manage/ApiKey.vue'; import DocumentTemplate from '~/components/forms/manage/DocumentTemplate.vue'; import ExternalAPIs from '~/components/forms/manage/ExternalAPIs.vue'; +import Embed from '~/components/forms/manage/Embed.vue'; import FormSettings from '~/components/designer/FormSettings.vue'; import ManageVersions from '~/components/forms/manage/ManageVersions.vue'; import Subscription from '~/components/forms/manage/Subscription.vue'; @@ -25,6 +26,7 @@ const settingsPanel = ref(1); const subscription = ref(false); const subscriptionsPanel = ref(0); const versionsPanel = ref(0); +const embedPanel = ref(1); const formStore = useFormStore(); const notificationStore = useNotificationStore(); @@ -311,6 +313,24 @@ defineExpose({ + + + + +
+ {{ $t('trans.manageForm.formEmbed') }} +
+
+ + + +
+
+ diff --git a/app/frontend/src/internationalization/trans/chefs/ar/ar.json b/app/frontend/src/internationalization/trans/chefs/ar/ar.json index 0b514c9a8..c2e30450a 100644 --- a/app/frontend/src/internationalization/trans/chefs/ar/ar.json +++ b/app/frontend/src/internationalization/trans/chefs/ar/ar.json @@ -72,7 +72,8 @@ "eventSubscription": "اشتراك الحدث", "cdogsTemplate": "قالب CDOGS", "externalAPIs": "واجهات برمجة التطبيقات الخارجية", - "eventStreamConfig": "تكوين تدفق الحدث" + "eventStreamConfig": "تكوين تدفق الحدث", + "formEmbed": "تضمين النماذج" }, "documentTemplate": { "uploadTemplate": "تحميل قالب CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "تم تحديث مفتاح التشفير بواسطة", "encryptionKeyCopySnackbar": "تم نسخ مفتاح التشفير إلى الحافظة", "encryptionKeyCopyTooltip": "نسخ مفتاح التشفير إلى الحافظة", - "encryptionKeyGenerate": "إنشاء مفتاح التشفير" + "encryptionKeyGenerate": "إنشاء مفتاح التشفير", + "formEmbed": "تضمين النماذج" }, "formProfile": { "message": "تقوم فرق CHEFS بجمع وتنظيم المعلومات لتكون مدخلات حاسمة لصياغة حالات أعمال شاملة. ستلعب هذه الحالات دورًا حيويًا في توجيه العمليات الاستراتيجية وتحسين CHEFS المستمر في السنوات القادمة. هذه المبادرة لجمع البيانات ضرورية لإعلام القرارات الحاسمة وتشكيل مسار CHEFS ، مما يضمن قابليتها للتكيف وفعاليتها في التعامل مع الاحتياجات والتحديات المتطورة.", @@ -389,7 +391,8 @@ "developer": "مطور", "infoLinks": "روابط المعلومات", "metrics": "المقاييس", - "apis": "واجهات برمجة التطبيقات" + "apis": "واجهات برمجة التطبيقات", + "formEmbedding": "تضمين النماذج" }, "adminUsersTable": { "search": "يبحث", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "خطأ في تحديث واجهة برمجة التطبيقات الخارجية.", "updateAPIsConsErrMsg": "خطأ في تحديث واجهة برمجة التطبيقات الخارجية: {error}", "getAPICodesErrMsg": "حدث خطأ أثناء جلب قائمة رموز حالة واجهة برمجة التطبيقات الخارجية.", - "getAPICodesConsErrMsg": "حدث خطأ أثناء جلب قائمة رموز حالة واجهة برمجة التطبيقات الخارجية: {error}" + "getAPICodesConsErrMsg": "حدث خطأ أثناء جلب قائمة رموز حالة واجهة برمجة التطبيقات الخارجية: {error}", + "getFormEmbedDomainStatusCodesErr": "فشل في جلب رموز حالة النطاق.", + "getFormEmbedDomainStatusCodesConsErr": "فشل في جلب رموز حالة النطاق: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "خطأ في تحميل قوالب البريد الإلكتروني لـ {formId}: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "تحديث حالة واجهة برمجة التطبيقات الخارجية", "allowSendUserToken": "السماح بـ "إرسال رمز المستخدم"" }, + "adminFormEmbed": { + "search": "بحث", + "loadingText": "جاري التحميل... يرجى الانتظار", + "ministry": "الوزارة", + "ministryName": "اسم الوزارة", + "formName": "النموذج", + "domain": "النطاق", + "status": "الحالة", + "reason": "السبب", + "requestedBy": "طلب بواسطة", + "requestedAt": "تاريخ الطلب", + "actions": "إجراءات", + "edit": "تعديل", + "editTitle": "تحديث حالة طلب نطاق النموذج", + "save": "حفظ", + "delete": "حذف", + "previousStatus": "الحالة السابقة", + "newStatus": "الحالة الجديدة", + "createdAt": "تم الإنشاء في", + "createdBy": "تم الإنشاء بواسطة", + "viewHistory": "عرض التاريخ" + }, + "formEmbed": { + "disclaimer": "تأكد من حصولك على إذن لتضمين نموذج CHEFS هذا في نطاقك.", + "domain": "النطاق", + "status": "الحالة", + "reason": "السبب", + "requestedAt": "تاريخ الطلب", + "actions": "إجراءات", + "delete": "حذف", + "previousStatus": "الحالة السابقة", + "newStatus": "الحالة الجديدة", + "createdAt": "تم الإنشاء في", + "createdBy": "تم الإنشاء بواسطة", + "viewHistory": "عرض التاريخ", + "requestDomain": "طلب نطاق", + "domainRules": "أدخل نطاقًا صالحًا (مثل example.com)", + "emptyFieldRules": "النطاق مطلوب", + "newDomainHint": "أدخل النطاق بدون بروتوكول (مثل example.com)", + "listDomainsErr": "حدث خطأ أثناء جلب قائمة النطاقات.", + "listDomainsConsErr": "خطأ في جلب النطاقات: {error}", + "requestDomainSuccess": "تم تقديم طلب النطاق بنجاح.", + "requestDomainErr": "فشل في تقديم طلب النطاق.", + "requestDomainConsErr": "خطأ في تقديم طلب النطاق: {error}", + "removeDomainSuccess": "تم إزالة النطاق بنجاح.", + "removeDomainErr": "فشل في إزالة النطاق.", + "removeDomainConsErr": "خطأ في إزالة النطاق: {error}", + "getDomainHistoryErr": "فشل في جلب تاريخ النطاق.", + "getDomainHistoryConsErr": "فشل في جلب تاريخ النطاق: {error}", + "getStatusCodesErr": "فشل في جلب رموز حالة النطاق.", + "getStatusCodesConsErr": "فشل في جلب رموز حالة النطاق: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/de/de.json b/app/frontend/src/internationalization/trans/chefs/de/de.json index 5982a8e52..883f18c0e 100644 --- a/app/frontend/src/internationalization/trans/chefs/de/de.json +++ b/app/frontend/src/internationalization/trans/chefs/de/de.json @@ -72,7 +72,8 @@ "eventSubscription": "Ereignisabonnement", "cdogsTemplate": "CDOGS-Vorlage", "externalAPIs": "Externe APIs", - "eventStreamConfig": "Event Stream-Konfiguration" + "eventStreamConfig": "Event Stream-Konfiguration", + "formEmbed": "Formulareinbettung" }, "documentTemplate": { "uploadTemplate": "CDOGS-Vorlage hochladen", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Verschlüsselungsschlüssel aktualisiert von", "encryptionKeyCopySnackbar": "Verschlüsselungsschlüssel in die Zwischenablage kopiert", "encryptionKeyCopyTooltip": "Verschlüsselungsschlüssel in die Zwischenablage kopieren", - "encryptionKeyGenerate": "Verschlüsselungsschlüssel generieren" + "encryptionKeyGenerate": "Verschlüsselungsschlüssel generieren", + "formEmbed": "Formulareinbettung" }, "formProfile": { "message": "Das CHEFS-Team sammelt und organisiert Informationen, um als entscheidende Grundlage für die Erstellung umfassender Geschäftsfälle zu dienen. Diese Fälle werden eine Schlüsselrolle dabei spielen, den strategischen Betrieb und die laufende Verbesserung von CHEFS in den kommenden Jahren zu leiten. Diese Initiative zur Datensammlung ist entscheidend, um kritische Entscheidungen zu informieren und die Trajektorie von CHEFS zu formen, um seine Anpassungsfähigkeit und Wirksamkeit bei der Bewältigung sich wandelnder Bedürfnisse und Herausforderungen zu gewährleisten.", @@ -389,7 +391,8 @@ "developer": "Entwickler", "infoLinks": "Info-Links", "metrics": "Metriken", - "apis": "APIs" + "apis": "APIs", + "formEmbedding": "Formulareinbettung" }, "adminUsersTable": { "search": "Suchen", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "Fehler beim Aktualisieren der externen API.", "updateAPIsConsErrMsg": "Fehler beim Aktualisieren der externen API: {error}", "getAPICodesErrMsg": "Fehler beim Abrufen der Liste mit externen API-Statuscodes.", - "getAPICodesConsErrMsg": "Fehler beim Abrufen der Liste mit externen API-Statuscodes: {error}" + "getAPICodesConsErrMsg": "Fehler beim Abrufen der Liste mit externen API-Statuscodes: {error}", + "getFormEmbedDomainStatusCodesErr": "Abrufen der Domain-Statuscodes fehlgeschlagen.", + "getFormEmbedDomainStatusCodesConsErr": "Abrufen der Domain-Statuscodes fehlgeschlagen: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Fehler beim Laden der E-Mail-Vorlagen für {formId}: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "Externen API-Status aktualisieren", "allowSendUserToken": "„Benutzertoken senden“ zulassen" }, + "adminFormEmbed": { + "search": "Suchen", + "loadingText": "Wird geladen... Bitte warten", + "ministry": "Ministerium", + "ministryName": "Name des Ministeriums", + "formName": "Formular", + "domain": "Domain", + "status": "Status", + "reason": "Grund", + "requestedBy": "Angefragt von", + "requestedAt": "Angefragt am", + "actions": "Aktionen", + "edit": "Bearbeiten", + "editTitle": "Status der Formular-Domain-Anfrage aktualisieren", + "save": "Speichern", + "delete": "Löschen", + "previousStatus": "Vorheriger Status", + "newStatus": "Neuer Status", + "createdAt": "Erstellt am", + "createdBy": "Erstellt von", + "viewHistory": "Verlauf anzeigen" + }, + "formEmbed": { + "disclaimer": "Stellen Sie sicher, dass Sie die Berechtigung haben, dieses CHEFS-Formular in Ihre Domain einzubetten.", + "domain": "Domain", + "status": "Status", + "reason": "Grund", + "requestedAt": "Angefragt am", + "actions": "Aktionen", + "delete": "Löschen", + "previousStatus": "Vorheriger Status", + "newStatus": "Neuer Status", + "createdAt": "Erstellt am", + "createdBy": "Erstellt von", + "viewHistory": "Verlauf anzeigen", + "requestDomain": "Domain anfragen", + "domainRules": "Geben Sie eine gültige Domain ein (z.B. example.com)", + "emptyFieldRules": "Domain ist erforderlich", + "newDomainHint": "Geben Sie die Domain ohne Protokoll ein (z.B. example.com)", + "listDomainsErr": "Beim Abrufen der Domain-Liste ist ein Fehler aufgetreten.", + "listDomainsConsErr": "Fehler beim Abrufen der Domains: {error}", + "requestDomainSuccess": "Domain-Anfrage erfolgreich übermittelt.", + "requestDomainErr": "Domain-Anfrage konnte nicht übermittelt werden.", + "requestDomainConsErr": "Fehler bei der Übermittlung der Domain-Anfrage: {error}", + "removeDomainSuccess": "Domain erfolgreich entfernt.", + "removeDomainErr": "Domain konnte nicht entfernt werden.", + "removeDomainConsErr": "Fehler beim Entfernen der Domain: {error}", + "getDomainHistoryErr": "Domain-Verlauf konnte nicht abgerufen werden.", + "getDomainHistoryConsErr": "Fehler beim Abrufen des Domain-Verlaufs: {error}", + "getStatusCodesErr": "Abrufen der Domain-Statuscodes fehlgeschlagen.", + "getStatusCodesConsErr": "Abrufen der Domain-Statuscodes fehlgeschlagen: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/en/en.json b/app/frontend/src/internationalization/trans/chefs/en/en.json index f7d031314..5434238b4 100644 --- a/app/frontend/src/internationalization/trans/chefs/en/en.json +++ b/app/frontend/src/internationalization/trans/chefs/en/en.json @@ -68,7 +68,8 @@ "eventSubscription": "Event Subscription", "cdogsTemplate": "CDOGS Template", "externalAPIs": "External APIs", - "eventStreamConfig": "Event Stream Configuration" + "eventStreamConfig": "Event Stream Configuration", + "formEmbed": "Form Embedding" }, "documentTemplate": { "uploadTemplate": "Upload CDOGS Template", @@ -266,7 +267,8 @@ "encryptionKeyUpdatedBy": "Encryption Key Updated By", "encryptionKeyCopySnackbar": "Encryption Key copied to clipboard", "encryptionKeyCopyTooltip": "Copy Encryption Key to clipboard", - "encryptionKeyGenerate": "Generate Encryption Key" + "encryptionKeyGenerate": "Generate Encryption Key", + "formEmbed": "Form Embedding" }, "formProfile": { "message": "The CHEFS team is collecting and organizing information to serve as crucial input for crafting comprehensive business cases. These cases will play a pivotal role in guiding the strategic operation and ongoing improvement of CHEFS in the coming years. This initiative to gather data is essential for informing critical decisions and molding the trajectory of CHEFS, ensuring its adaptability and effectiveness in addressing evolving needs and challenges.", @@ -452,7 +454,8 @@ "apis": "APIs", "developer": "Developer", "infoLinks": "Info Links", - "metrics": "Metrics" + "metrics": "Metrics", + "formEmbedding": "Form Embedding" }, "adminAPIsTable": { "search": "Search", @@ -1107,7 +1110,17 @@ "updateAPIsErrMsg": "Error updating External API.", "updateAPIsConsErrMsg": "Error updating External API: {error}", "getAPICodesErrMsg": "Error fetching External API Status Codes list.", - "getAPICodesConsErrMsg": "Error fetching External API Status Codes list: {error}" + "getAPICodesConsErrMsg": "Error fetching External API Status Codes list: {error}", + "getFormEmbedDomainsErrMsg": "An error occurred while fetching Form Embed Domains.", + "getFormEmbedDomainsConsErrMsg": "Error fetching Form Embed Domains: {error}", + "getFormEmbedDomainHistoryErrMsg": "An error occurred while fetching Form Embed Domain History.", + "getFormEmbedDomainHistoryConsErrMsg": "Error fetching Form Embed Domain History: {error}", + "updateFormEmbedDomainRequestErrMsg": "Error updating Form Embed Domain.", + "updateFormEmbedDomainRequestConsErrMsg": "Error updating Form Embed Domain: {error}", + "removeFormEmbedDomainRequestErrMsg": "Error removing Form Embed Domain.", + "removeFormEmbedDomainRequestConsErrMsg": "Error removing Form Embed Domain: {error}", + "getFormEmbedDomainStatusCodesErr": "Failed to fetch domain status codes.", + "getFormEmbedDomainStatusCodesConsErr": "Failed to fetch domain status codes: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Error loading email templates for {formId}: {error}", @@ -1246,5 +1259,57 @@ "shuttingDown": "CHEFS is shutting down.", "notReady": "CHEFS cannot connect to all required services.", "unableToReachBackend": "Unable to reach backend service." + }, + "formEmbed": { + "disclaimer": "Ensure that you have permission to embed this CHEFS form in your domain.", + "domain": "Domain", + "status": "Status", + "reason": "Reason", + "requestedAt": "Requested At", + "actions": "Actions", + "delete": "Delete", + "previousStatus": "Previous Status", + "newStatus": "New Status", + "createdAt": "Created At", + "createdBy": "Created By", + "viewHistory": "View History", + "requestDomain": "Request Domain", + "domainRules": "Enter a valid domain (e.g., example.com)", + "emptyFieldRules": "Domain is required", + "newDomainHint": "Enter the domain without protocol (e.g., example.com)", + "listDomainsErr": "An error occurred while fetching the list of domains.", + "listDomainsConsErr": "Error fetching domains: {error}", + "requestDomainSuccess": "Domain request submitted successfully.", + "requestDomainErr": "Failed to submit domain request.", + "requestDomainConsErr": "Error submitting domain request: {error}", + "removeDomainSuccess": "Domain removed successfully.", + "removeDomainErr": "Failed to remove domain.", + "removeDomainConsErr": "Error removing domain: {error}", + "getDomainHistoryErr": "Failed to fetch domain history.", + "getDomainHistoryConsErr": "Failed to fetch domain history: {error}", + "getStatusCodesErr": "Failed to fetch domain status codes.", + "getStatusCodesConsErr": "Failed to fetch domain status codes: {error}" + }, + "adminFormEmbed": { + "search": "Search", + "loadingText": "Loading... Please wait", + "ministry": "Ministry", + "ministryName": "Ministry Name", + "formName": "Form", + "domain": "Domain", + "status": "Status", + "reason": "Reason", + "requestedBy": "Requested By", + "requestedAt": "Requested At", + "actions": "Actions", + "edit": "Edit", + "editTitle": "Update Form Domain Request Status", + "save": "Save", + "delete": "Delete", + "previousStatus": "Previous Status", + "newStatus": "New Status", + "createdAt": "Created At", + "createdBy": "Created By", + "viewHistory": "View History" } } diff --git a/app/frontend/src/internationalization/trans/chefs/es/es.json b/app/frontend/src/internationalization/trans/chefs/es/es.json index 921fa271a..d4bbc2b84 100644 --- a/app/frontend/src/internationalization/trans/chefs/es/es.json +++ b/app/frontend/src/internationalization/trans/chefs/es/es.json @@ -72,7 +72,8 @@ "eventSubscription": "Suscripción a eventos", "cdogsTemplate": "Plantilla CDDOGS", "externalAPIs": "API externas", - "eventStreamConfig": "Configuración de la secuencia de eventos" + "eventStreamConfig": "Configuración de la secuencia de eventos", + "formEmbed": "Incrustación de formulario" }, "documentTemplate": { "uploadTemplate": "Subir plantilla de CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Clave de cifrado actualizada por", "encryptionKeyCopySnackbar": "Clave de cifrado copiada al portapapeles", "encryptionKeyCopyTooltip": "Copiar la clave de cifrado al portapapeles", - "encryptionKeyGenerate": "Generar clave de cifrado" + "encryptionKeyGenerate": "Generar clave de cifrado", + "formEmbed": "Incrustación de formulario" }, "formProfile": { "message": "El equipo de CHEFS está recopilando y organizando información para servir como entrada crucial para la elaboración de casos de negocio integrales. Estos casos jugarán un papel fundamental en la guía de la operación estratégica y la mejora continua de CHEFS en los próximos años. Esta iniciativa de recopilación de datos es esencial para informar decisiones críticas y dar forma a la trayectoria de CHEFS, asegurando su adaptabilidad y efectividad para abordar necesidades y desafíos en constante evolución.", @@ -389,7 +391,8 @@ "developer": "Desarrollador", "infoLinks": "Enlaces de información", "metrics": "Métrica", - "apis": "API" + "apis": "API", + "formEmbedding": "Incrustación de formulario" }, "adminUsersTable": { "search": "Buscar", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "Error al actualizar la API externa.", "updateAPIsConsErrMsg": "Error al actualizar la API externa: {error}", "getAPICodesErrMsg": "Error al obtener la lista de códigos de estado de API externas.", - "getAPICodesConsErrMsg": "Error al obtener la lista de códigos de estado de API externas: {error}" + "getAPICodesConsErrMsg": "Error al obtener la lista de códigos de estado de API externas: {error}", + "getFormEmbedDomainStatusCodesErr": "Error al obtener los códigos de estado del dominio.", + "getFormEmbedDomainStatusCodesConsErr": "Error al obtener los códigos de estado del dominio: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Error al cargar plantillas de correo electrónico para {formId}: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "Actualizar el estado de la API externa", "allowSendUserToken": "Permitir 'Enviar token de usuario'" }, + "adminFormEmbed": { + "search": "Buscar", + "loadingText": "Cargando... Por favor espere", + "ministry": "Ministerio", + "ministryName": "Nombre del ministerio", + "formName": "Formulario", + "domain": "Dominio", + "status": "Estado", + "reason": "Razón", + "requestedBy": "Solicitado por", + "requestedAt": "Solicitado el", + "actions": "Acciones", + "edit": "Editar", + "editTitle": "Actualizar estado de solicitud de dominio", + "save": "Guardar", + "delete": "Eliminar", + "previousStatus": "Estado anterior", + "newStatus": "Nuevo estado", + "createdAt": "Creado el", + "createdBy": "Creado por", + "viewHistory": "Ver historial" + }, + "formEmbed": { + "disclaimer": "Asegúrese de tener permiso para incrustar este formulario CHEFS en su dominio.", + "domain": "Dominio", + "status": "Estado", + "reason": "Razón", + "requestedAt": "Solicitado el", + "actions": "Acciones", + "delete": "Eliminar", + "previousStatus": "Estado anterior", + "newStatus": "Nuevo estado", + "createdAt": "Creado el", + "createdBy": "Creado por", + "viewHistory": "Ver historial", + "requestDomain": "Solicitar dominio", + "domainRules": "Introduzca un dominio válido (ej: example.com)", + "emptyFieldRules": "El dominio es obligatorio", + "newDomainHint": "Introduzca el dominio sin protocolo (ej: example.com)", + "listDomainsErr": "Se produjo un error al obtener la lista de dominios.", + "listDomainsConsErr": "Error al obtener dominios: {error}", + "requestDomainSuccess": "Solicitud de dominio enviada con éxito.", + "requestDomainErr": "No se pudo enviar la solicitud de dominio.", + "requestDomainConsErr": "Error al enviar la solicitud de dominio: {error}", + "removeDomainSuccess": "Dominio eliminado con éxito.", + "removeDomainErr": "No se pudo eliminar el dominio.", + "removeDomainConsErr": "Error al eliminar el dominio: {error}", + "getDomainHistoryErr": "No se pudo obtener el historial del dominio.", + "getDomainHistoryConsErr": "No se pudo obtener el historial del dominio: {error}", + "getStatusCodesErr": "Error al obtener los códigos de estado del dominio.", + "getStatusCodesConsErr": "Error al obtener los códigos de estado del dominio: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/fa/fa.json b/app/frontend/src/internationalization/trans/chefs/fa/fa.json index 418019be0..b66d9e2eb 100644 --- a/app/frontend/src/internationalization/trans/chefs/fa/fa.json +++ b/app/frontend/src/internationalization/trans/chefs/fa/fa.json @@ -72,7 +72,8 @@ "eventSubscription": "اشتراک رویداد", "cdogsTemplate": "قالب CDOGS", "externalAPIs": "API های خارجی", - "eventStreamConfig": "پیکربندی جریان رویداد" + "eventStreamConfig": "پیکربندی جریان رویداد", + "formEmbed": "جاسازی فرم" }, "documentTemplate": { "uploadTemplate": "بارگذاری قالب CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "کلید رمزگذاری به روز شده توسط", "encryptionKeyCopySnackbar": "کلید رمزگذاری در کلیپ بورد کپی شد", "encryptionKeyCopyTooltip": "کلید رمزگذاری را در کلیپ بورد کپی کنید", - "encryptionKeyGenerate": "ایجاد کلید رمزگذاری" + "encryptionKeyGenerate": "ایجاد کلید رمزگذاری", + "formEmbed": "جاسازی فرم" }, "formProfile": { "message": "تیم CHEFS اطلاعات را جمع آوری و سازماندهی می‌کند تا به عنوان ورودی حیاتی برای ساخت مورد کارهای تجاری جامع عمل کند. این موارد نقش کلیدی در راهنمایی عملیات استراتژیک و بهبود مستمر CHEFS در سال‌های آینده خواهند داشت. این اقدام برای جمع‌آوری داده‌ها برای اطلاع از تصمیمات حیاتی و شکل‌دهی مسیر CHEFS جهت اطمینان از قابلیت تطبیق و کارایی آن در مواجهه با نیازها و چالش‌های در حال تحول حیاتی است.", @@ -389,7 +391,8 @@ "developer": "توسعه دهنده", "infoLinks": "لینک های اطلاعات", "metrics": "معیارهای", - "apis": "API ها" + "apis": "API ها", + "formEmbedding": "جاسازی فرم" }, "adminUsersTable": { "search": "جستجو کردن", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "خطا در به‌روزرسانی API خارجی.", "updateAPIsConsErrMsg": "خطا در به‌روزرسانی API خارجی: {error}", "getAPICodesErrMsg": "خطا در واکشی لیست کدهای وضعیت API خارجی.", - "getAPICodesConsErrMsg": "خطا در واکشی لیست کدهای وضعیت API خارجی: {error}" + "getAPICodesConsErrMsg": "خطا در واکشی لیست کدهای وضعیت API خارجی: {error}", + "getFormEmbedDomainStatusCodesErr": "دریافت کدهای وضعیت دامنه ناموفق بود.", + "getFormEmbedDomainStatusCodesConsErr": "دریافت کدهای وضعیت دامنه ناموفق بود: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "خطا در بارگیری الگوهای ایمیل برای {formId}: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "به روز رسانی وضعیت API خارجی", "allowSendUserToken": "اجازه دادن به "Send User Token"" }, + "adminFormEmbed": { + "search": "جستجو", + "loadingText": "در حال بارگذاری... لطفا صبر کنید", + "ministry": "وزارتخانه", + "ministryName": "نام وزارتخانه", + "formName": "فرم", + "domain": "دامنه", + "status": "وضعیت", + "reason": "دلیل", + "requestedBy": "درخواست شده توسط", + "requestedAt": "درخواست شده در", + "actions": "عملیات", + "edit": "ویرایش", + "editTitle": "به‌روزرسانی وضعیت درخواست دامنه فرم", + "save": "ذخیره", + "delete": "حذف", + "previousStatus": "وضعیت قبلی", + "newStatus": "وضعیت جدید", + "createdAt": "ایجاد شده در", + "createdBy": "ایجاد شده توسط", + "viewHistory": "مشاهده تاریخچه" + }, + "formEmbed": { + "disclaimer": "اطمینان حاصل کنید که مجوز جاسازی این فرم CHEFS را در دامنه خود دارید.", + "domain": "دامنه", + "status": "وضعیت", + "reason": "دلیل", + "requestedAt": "درخواست شده در", + "actions": "عملیات", + "delete": "حذف", + "previousStatus": "وضعیت قبلی", + "newStatus": "وضعیت جدید", + "createdAt": "ایجاد شده در", + "createdBy": "ایجاد شده توسط", + "viewHistory": "مشاهده تاریخچه", + "requestDomain": "درخواست دامنه", + "domainRules": "یک دامنه معتبر وارد کنید (مثلاً example.com)", + "emptyFieldRules": "دامنه الزامی است", + "newDomainHint": "دامنه را بدون پروتکل وارد کنید (مثلاً example.com)", + "listDomainsErr": "خطایی هنگام دریافت لیست دامنه‌ها رخ داد.", + "listDomainsConsErr": "خطا در دریافت دامنه‌ها: {error}", + "requestDomainSuccess": "درخواست دامنه با موفقیت ارسال شد.", + "requestDomainErr": "ارسال درخواست دامنه ناموفق بود.", + "requestDomainConsErr": "خطا در ارسال درخواست دامنه: {error}", + "removeDomainSuccess": "دامنه با موفقیت حذف شد.", + "removeDomainErr": "حذف دامنه ناموفق بود.", + "removeDomainConsErr": "خطا در حذف دامنه: {error}", + "getDomainHistoryErr": "دریافت تاریخچه دامنه ناموفق بود.", + "getDomainHistoryConsErr": "خطا در دریافت تاریخچه دامنه: {error}", + "getStatusCodesErr": "دریافت کدهای وضعیت دامنه ناموفق بود.", + "getStatusCodesConsErr": "دریافت کدهای وضعیت دامنه ناموفق بود: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/fr/fr.json b/app/frontend/src/internationalization/trans/chefs/fr/fr.json index 696f49af8..9e8c0ec19 100644 --- a/app/frontend/src/internationalization/trans/chefs/fr/fr.json +++ b/app/frontend/src/internationalization/trans/chefs/fr/fr.json @@ -72,7 +72,8 @@ "eventSubscription": "Abonnement à l'événement", "cdogsTemplate": "Modèle CDOGS", "externalAPIs": "API externes", - "eventStreamConfig": "Configuration du flux d'événements" + "eventStreamConfig": "Configuration du flux d'événements", + "formEmbed": "Intégration de formulaire" }, "documentTemplate": { "uploadTemplate": "Télécharger le modèle CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Clé de cryptage mise à jour par", "encryptionKeyCopySnackbar": "Clé de chiffrement copiée dans le presse-papiers", "encryptionKeyCopyTooltip": "Copier la clé de chiffrement dans le presse-papiers", - "encryptionKeyGenerate": "Générer une clé de cryptage" + "encryptionKeyGenerate": "Générer une clé de cryptage", + "formEmbed": "Intégration de formulaire" }, "formProfile": { "message": "L'équipe CHEFS collecte et organise des informations pour servir de contribution cruciale à l'élaboration de cas d'affaires complets. Ces cas joueront un rôle central dans la direction des opérations stratégiques et l'amélioration continue de CHEFS au cours des prochaines années. Cette initiative de collecte de données est essentielle pour éclairer les décisions critiques et façonner la trajectoire de CHEFS, assurant son adaptabilité et son efficacité face aux besoins et aux défis en constante évolution.", @@ -389,7 +391,8 @@ "developer": "Développeur", "infoLinks": "Liens d'information", "metrics": "Métrique", - "apis": "Apis" + "apis": "Apis", + "formEmbedding": "Intégration de formulaire" }, "adminUsersTable": { "search": "Recherche", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "Erreur lors de la mise à jour de l'API externe.", "updateAPIsConsErrMsg": "Erreur lors de la mise à jour de l'API externe : {error}", "getAPICodesErrMsg": "Erreur lors de la récupération de la liste des codes d'état de l'API externe.", - "getAPICodesConsErrMsg": "Erreur lors de la récupération de la liste des codes d'état de l'API externe : {error}" + "getAPICodesConsErrMsg": "Erreur lors de la récupération de la liste des codes d'état de l'API externe : {error}", + "getFormEmbedDomainStatusCodesErr": "دریافت کدهای وضعیت دامنه ناموفق بود.", + "getFormEmbedDomainStatusCodesConsErr": "دریافت کدهای وضعیت دامنه ناموفق بود: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Erreur lors du chargement des modèles d'e-mail pour {formId} : {error}", @@ -1220,6 +1225,59 @@ "editTitle": "Mettre à jour le statut de l'API externe", "allowSendUserToken": "Autoriser « Envoyer un jeton utilisateur »" }, + + "adminFormEmbed": { + "search": "Rechercher", + "loadingText": "Chargement... Veuillez patienter", + "ministry": "Ministère", + "ministryName": "Nom du ministère", + "formName": "Formulaire", + "domain": "Domaine", + "status": "Statut", + "reason": "Raison", + "requestedBy": "Demandé par", + "requestedAt": "Demandé le", + "actions": "Actions", + "edit": "Modifier", + "editTitle": "Mettre à jour le statut de la demande de domaine", + "save": "Enregistrer", + "delete": "Supprimer", + "previousStatus": "Statut précédent", + "newStatus": "Nouveau statut", + "createdAt": "Créé le", + "createdBy": "Créé par", + "viewHistory": "Voir l'historique" + }, + "formEmbed": { + "disclaimer": "Assurez-vous d'avoir l'autorisation d'intégrer ce formulaire CHEFS dans votre domaine.", + "domain": "Domaine", + "status": "Statut", + "reason": "Raison", + "requestedAt": "Demandé le", + "actions": "Actions", + "delete": "Supprimer", + "previousStatus": "Statut précédent", + "newStatus": "Nouveau statut", + "createdAt": "Créé le", + "createdBy": "Créé par", + "viewHistory": "Voir l'historique", + "requestDomain": "Demander un domaine", + "domainRules": "Entrez un domaine valide (ex: example.com)", + "emptyFieldRules": "Le domaine est requis", + "newDomainHint": "Entrez le domaine sans protocole (ex: example.com)", + "listDomainsErr": "Une erreur s'est produite lors de la récupération de la liste des domaines.", + "listDomainsConsErr": "Erreur lors de la récupération des domaines: {error}", + "requestDomainSuccess": "Demande de domaine soumise avec succès.", + "requestDomainErr": "Échec de la soumission de la demande de domaine.", + "requestDomainConsErr": "Erreur lors de la soumission de la demande de domaine: {error}", + "removeDomainSuccess": "Domaine supprimé avec succès.", + "removeDomainErr": "Échec de la suppression du domaine.", + "removeDomainConsErr": "Erreur lors de la suppression du domaine: {error}", + "getDomainHistoryErr": "Échec de la récupération de l'historique du domaine.", + "getDomainHistoryConsErr": "Échec de la récupération de l'historique du domaine: {error}", + "getStatusCodesErr": "Échec de la récupération des codes de statut de domaine.", + "getStatusCodesConsErr": "Échec de la récupération des codes de statut de domaine: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/hi/hi.json b/app/frontend/src/internationalization/trans/chefs/hi/hi.json index 82f3a341c..167178e07 100644 --- a/app/frontend/src/internationalization/trans/chefs/hi/hi.json +++ b/app/frontend/src/internationalization/trans/chefs/hi/hi.json @@ -72,7 +72,8 @@ "eventSubscription": "इवेंट सदस्यता", "cdogsTemplate": "सीडीओजीएस टेम्पलेट", "externalAPIs": "बाह्य एपीआई", - "eventStreamConfig": "इवेंट स्ट्रीम कॉन्फ़िगरेशन" + "eventStreamConfig": "इवेंट स्ट्रीम कॉन्फ़िगरेशन", + "formEmbed": "फॉर्म एम्बेडिंग" }, "documentTemplate": { "uploadTemplate": "CDOGS टेम्पलेट अपलोड करें", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "एन्क्रिप्शन कुंजी अपडेट किया गया", "encryptionKeyCopySnackbar": "एन्क्रिप्शन कुंजी क्लिपबोर्ड पर कॉपी की गई", "encryptionKeyCopyTooltip": "एन्क्रिप्शन कुंजी को क्लिपबोर्ड पर कॉपी करें", - "encryptionKeyGenerate": "एन्क्रिप्शन कुंजी उत्पन्न करें" + "encryptionKeyGenerate": "एन्क्रिप्शन कुंजी उत्पन्न करें", + "formEmbed": "फॉर्म एम्बेडिंग" }, "formProfile": { "message": "CHEFS टीम सूचना एकत्र कर रही है और उसे समृद्धिकारी व्यापक व्यापार मामलों के लिए महत्वपूर्ण इनपुट के रूप में सेवा करने के लिए। ये मामले CHEFS के आगामी वर्षों में रणनीतिक संचालन और उन्नति में महत्वपूर्ण भूमिका निभाएंगे। डेटा इकट्ठा करने का यह पहल सूचना को सूचित करने, महत्वपूर्ण निर्णयों को सूचित करने और CHEFS की यात्रा को मोल्डिंग के लिए आवश्यक है, इसे सुनिश्चित करना है कि यह आवश्यकताओं और चुनौतियों को समाप्त करने में अपनी योग्यता और प्रभावकारिता में बदलते समय का सामना कर सकता है।", @@ -389,7 +391,8 @@ "developer": "डेवलपर", "infoLinks": "जानकारी लिंक", "metrics": "मेट्रिक्स", - "apis": "शहद की मक्खी" + "apis": "शहद की मक्खी", + "formEmbedding": "फॉर्म एम्बेडिंग" }, "adminUsersTable": { "search": "खोज", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "बाह्य API अद्यतन करने में त्रुटि.", "updateAPIsConsErrMsg": "बाहरी API अपडेट करते समय त्रुटि: {error}", "getAPICodesErrMsg": "बाह्य API स्थिति कोड सूची प्राप्त करने में त्रुटि हुई.", - "getAPICodesConsErrMsg": "बाहरी API स्थिति कोड सूची प्राप्त करने में त्रुटि: {त्रुटि}" + "getAPICodesConsErrMsg": "बाहरी API स्थिति कोड सूची प्राप्त करने में त्रुटि: {त्रुटि}", + "getFormEmbedDomainStatusCodesErr": "डोमेन स्थिति कोड प्राप्त करने में विफल।", + "getFormEmbedDomainStatusCodesConsErr": "डोमेन स्थिति कोड प्राप्त करने में विफल: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "{formId} के लिए ईमेल टेम्पलेट लोड करने में त्रुटि: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "बाहरी API स्थिति अपडेट करें", "allowSendUserToken": "'उपयोगकर्ता टोकन भेजें' की अनुमति दें" }, + "adminFormEmbed": { + "search": "खोज", + "loadingText": "लोड हो रहा है... कृपया प्रतीक्षा करें", + "ministry": "मंत्रालय", + "ministryName": "मंत्रालय का नाम", + "formName": "फॉर्म", + "domain": "डोमेन", + "status": "स्थिति", + "reason": "कारण", + "requestedBy": "अनुरोधकर्ता", + "requestedAt": "अनुरोध का समय", + "actions": "कार्यवाहियां", + "edit": "संपादित करें", + "editTitle": "फॉर्म डोमेन अनुरोध की स्थिति अपडेट करें", + "save": "सहेजें", + "delete": "हटाएं", + "previousStatus": "पिछली स्थिति", + "newStatus": "नई स्थिति", + "createdAt": "बनाया गया", + "createdBy": "बनाने वाला", + "viewHistory": "इतिहास देखें" + }, + "formEmbed": { + "disclaimer": "सुनिश्चित करें कि आपके पास अपने डोमेन में इस CHEFS फॉर्म को एम्बेड करने की अनुमति है।", + "domain": "डोमेन", + "status": "स्थिति", + "reason": "कारण", + "requestedAt": "अनुरोध का समय", + "actions": "कार्यवाहियां", + "delete": "हटाएं", + "previousStatus": "पिछली स्थिति", + "newStatus": "नई स्थिति", + "createdAt": "बनाया गया", + "createdBy": "बनाने वाला", + "viewHistory": "इतिहास देखें", + "requestDomain": "डोमेन का अनुरोध करें", + "domainRules": "एक वैध डोमेन दर्ज करें (जैसे, example.com)", + "emptyFieldRules": "डोमेन आवश्यक है", + "newDomainHint": "प्रोटोकॉल के बिना डोमेन दर्ज करें (जैसे, example.com)", + "listDomainsErr": "डोमेन की सूची प्राप्त करते समय एक त्रुटि हुई।", + "listDomainsConsErr": "डोमेन प्राप्त करने में त्रुटि: {error}", + "requestDomainSuccess": "डोमेन अनुरोध सफलतापूर्वक जमा किया गया।", + "requestDomainErr": "डोमेन अनुरोध जमा करने में विफल।", + "requestDomainConsErr": "डोमेन अनुरोध जमा करने में त्रुटि: {error}", + "removeDomainSuccess": "डोमेन सफलतापूर्वक हटा दिया गया।", + "removeDomainErr": "डोमेन हटाने में विफल।", + "removeDomainConsErr": "डोमेन हटाने में त्रुटि: {error}", + "getDomainHistoryErr": "डोमेन इतिहास प्राप्त करने में विफल।", + "getDomainHistoryConsErr": "डोमेन इतिहास प्राप्त करने में त्रुटि: {error}", + "getStatusCodesErr": "डोमेन स्थिति कोड प्राप्त करने में विफल।", + "getStatusCodesConsErr": "डोमेन स्थिति कोड प्राप्त करने में विफल: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/it/it.json b/app/frontend/src/internationalization/trans/chefs/it/it.json index 13403d96b..faec55313 100644 --- a/app/frontend/src/internationalization/trans/chefs/it/it.json +++ b/app/frontend/src/internationalization/trans/chefs/it/it.json @@ -72,7 +72,8 @@ "eventSubscription": "Abbonamento all'evento", "cdogsTemplate": "Modello CDOGS", "externalAPIs": "API esterne", - "eventStreamConfig": "Configurazione del flusso di eventi" + "eventStreamConfig": "Configurazione del flusso di eventi", + "formEmbed": "Incorporazione del modulo" }, "documentTemplate": { "uploadTemplate": "Carica template CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Chiave di crittografia aggiornata da", "encryptionKeyCopySnackbar": "Chiave di crittografia copiata negli appunti", "encryptionKeyCopyTooltip": "Copia la chiave di crittografia negli appunti", - "encryptionKeyGenerate": "Genera chiave di crittografia" + "encryptionKeyGenerate": "Genera chiave di crittografia", + "formEmbed": "Incorporazione del modulo" }, "formProfile": { "message": "Il team CHEFS sta raccogliendo e organizzando informazioni per servire come input cruciale per la creazione di casi aziendali completi. Questi casi avranno un ruolo cruciale nella guida dell'operazione strategica e nell'ulteriore miglioramento di CHEFS nei prossimi anni. Questa iniziativa di raccolta dati è essenziale per informare decisioni critiche e plasmare la traiettoria di CHEFS, garantendone adattabilità ed efficacia nel affrontare esigenze e sfide in evoluzione.", @@ -389,7 +391,8 @@ "developer": "Sviluppatore", "infoLinks": "Collegamenti informativi", "metrics": "Metrica", - "apis": "API" + "apis": "API", + "formEmbedding": "Incorporazione del modulo" }, "adminUsersTable": { "search": "Ricerca", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "Errore durante l'aggiornamento dell'API esterna.", "updateAPIsConsErrMsg": "Errore durante l'aggiornamento dell'API esterna: {error}", "getAPICodesErrMsg": "Errore durante il recupero dell'elenco dei codici di stato dell'API esterna.", - "getAPICodesConsErrMsg": "Errore durante il recupero dell'elenco dei codici di stato dell'API esterna: {error}" + "getAPICodesConsErrMsg": "Errore durante il recupero dell'elenco dei codici di stato dell'API esterna: {error}", + "getFormEmbedDomainStatusCodesErr": "Impossibile recuperare i codici di stato del dominio.", + "getFormEmbedDomainStatusCodesConsErr": "Impossibile recuperare i codici di stato del dominio: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Errore durante il caricamento dei modelli di email per {formId}: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "Aggiorna lo stato dell'API esterna", "allowSendUserToken": "Consenti "Invia token utente"" }, + "adminFormEmbed": { + "search": "Cerca", + "loadingText": "Caricamento in corso... Attendere prego", + "ministry": "Ministero", + "ministryName": "Nome del ministero", + "formName": "Modulo", + "domain": "Dominio", + "status": "Stato", + "reason": "Motivo", + "requestedBy": "Richiesto da", + "requestedAt": "Richiesto il", + "actions": "Azioni", + "edit": "Modifica", + "editTitle": "Aggiorna stato della richiesta di dominio", + "save": "Salva", + "delete": "Elimina", + "previousStatus": "Stato precedente", + "newStatus": "Nuovo stato", + "createdAt": "Creato il", + "createdBy": "Creato da", + "viewHistory": "Visualizza cronologia" + }, + "formEmbed": { + "disclaimer": "Assicurati di avere l'autorizzazione per incorporare questo modulo CHEFS nel tuo dominio.", + "domain": "Dominio", + "status": "Stato", + "reason": "Motivo", + "requestedAt": "Richiesto il", + "actions": "Azioni", + "delete": "Elimina", + "previousStatus": "Stato precedente", + "newStatus": "Nuovo stato", + "createdAt": "Creato il", + "createdBy": "Creato da", + "viewHistory": "Visualizza cronologia", + "requestDomain": "Richiedi dominio", + "domainRules": "Inserisci un dominio valido (es. example.com)", + "emptyFieldRules": "Il dominio è obbligatorio", + "newDomainHint": "Inserisci il dominio senza protocollo (es. example.com)", + "listDomainsErr": "Si è verificato un errore durante il recupero dell'elenco dei domini.", + "listDomainsConsErr": "Errore nel recupero dei domini: {error}", + "requestDomainSuccess": "Richiesta di dominio inviata con successo.", + "requestDomainErr": "Impossibile inviare la richiesta di dominio.", + "requestDomainConsErr": "Errore nell'invio della richiesta di dominio: {error}", + "removeDomainSuccess": "Dominio rimosso con successo.", + "removeDomainErr": "Impossibile rimuovere il dominio.", + "removeDomainConsErr": "Errore nella rimozione del dominio: {error}", + "getDomainHistoryErr": "Impossibile recuperare la cronologia del dominio.", + "getDomainHistoryConsErr": "Errore nel recupero della cronologia del dominio: {error}", + "getStatusCodesErr": "Impossibile recuperare i codici di stato del dominio.", + "getStatusCodesConsErr": "Impossibile recuperare i codici di stato del dominio: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/ja/ja.json b/app/frontend/src/internationalization/trans/chefs/ja/ja.json index 9a367131d..565d6cdc0 100644 --- a/app/frontend/src/internationalization/trans/chefs/ja/ja.json +++ b/app/frontend/src/internationalization/trans/chefs/ja/ja.json @@ -72,7 +72,8 @@ "eventSubscription": "イベントのサブスクリプション", "cdogsTemplate": "CDOGS テンプレート", "externalAPIs": "外部API", - "eventStreamConfig": "イベントストリームの構成" + "eventStreamConfig": "イベントストリームの構成", + "formEmbed": "フォーム埋め込み" }, "documentTemplate": { "uploadTemplate": "CDOGSテンプレートをアップロード", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "暗号化キーの更新者", "encryptionKeyCopySnackbar": "暗号化キーがクリップボードにコピーされました", "encryptionKeyCopyTooltip": "暗号化キーをクリップボードにコピー", - "encryptionKeyGenerate": "暗号化キーの生成" + "encryptionKeyGenerate": "暗号化キーの生成", + "formEmbed": "フォーム埋め込み" }, "formProfile": { "message": "CHEFSチームは包括的なビジネスケースの作成に重要な入力となる情報を収集し、整理しています。これらのケースは、CHEFSの戦略的な運用と今後の改善を指南するうえで重要な役割を果たします。データを収集するこの取り組みは、重要な意思決定の情報提供やCHEFSの軌道を形作るために不可欠です。これにより、CHEFSが変化するニーズと課題に対応するための適応性と効果を確保します.", @@ -389,7 +391,8 @@ "developer": "デベロッパー", "infoLinks": "情報リンク", "metrics": "メトリクス", - "apis": "APIについて" + "apis": "APIについて", + "formEmbedding": "フォーム埋め込み" }, "adminUsersTable": { "search": "検索", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "外部 API の更新中にエラーが発生しました。", "updateAPIsConsErrMsg": "外部 API の更新中にエラーが発生しました: {error}", "getAPICodesErrMsg": "外部 API ステータス コード リストの取得中にエラーが発生しました。", - "getAPICodesConsErrMsg": "外部 API ステータス コード リストの取得中にエラーが発生しました: {error}" + "getAPICodesConsErrMsg": "外部 API ステータス コード リストの取得中にエラーが発生しました: {error}", + "getFormEmbedDomainStatusCodesErr": "ドメインステータスコードの取得に失敗しました。", + "getFormEmbedDomainStatusCodesConsErr": "ドメインステータスコードの取得に失敗しました: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "{formId} の電子メール テンプレートの読み込みエラー: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "外部APIステータスの更新", "allowSendUserToken": "「ユーザートークンの送信」を許可する" }, + "adminFormEmbed": { + "search": "検索", + "loadingText": "読み込み中... お待ちください", + "ministry": "省庁", + "ministryName": "省庁名", + "formName": "フォーム", + "domain": "ドメイン", + "status": "ステータス", + "reason": "理由", + "requestedBy": "リクエスト者", + "requestedAt": "リクエスト日時", + "actions": "アクション", + "edit": "編集", + "editTitle": "フォームドメインリクエストのステータスを更新", + "save": "保存", + "delete": "削除", + "previousStatus": "前のステータス", + "newStatus": "新しいステータス", + "createdAt": "作成日時", + "createdBy": "作成者", + "viewHistory": "履歴を表示" + }, + "formEmbed": { + "disclaimer": "このCHEFSフォームをあなたのドメインに埋め込む許可があることを確認してください。", + "domain": "ドメイン", + "status": "ステータス", + "reason": "理由", + "requestedAt": "リクエスト日時", + "actions": "アクション", + "delete": "削除", + "previousStatus": "前のステータス", + "newStatus": "新しいステータス", + "createdAt": "作成日時", + "createdBy": "作成者", + "viewHistory": "履歴を表示", + "requestDomain": "ドメインをリクエスト", + "domainRules": "有効なドメインを入力してください(例:example.com)", + "emptyFieldRules": "ドメインは必須です", + "newDomainHint": "プロトコルなしでドメインを入力してください(例:example.com)", + "listDomainsErr": "ドメインのリストの取得中にエラーが発生しました。", + "listDomainsConsErr": "ドメインの取得中にエラー: {error}", + "requestDomainSuccess": "ドメインリクエストが正常に送信されました。", + "requestDomainErr": "ドメインリクエストの送信に失敗しました。", + "requestDomainConsErr": "ドメインリクエスト送信中にエラー: {error}", + "removeDomainSuccess": "ドメインが正常に削除されました。", + "removeDomainErr": "ドメインの削除に失敗しました。", + "removeDomainConsErr": "ドメイン削除中にエラー: {error}", + "getDomainHistoryErr": "ドメイン履歴の取得に失敗しました。", + "getDomainHistoryConsErr": "ドメイン履歴の取得中にエラー: {error}", + "getStatusCodesErr": "ドメインステータスコードの取得に失敗しました。", + "getStatusCodesConsErr": "ドメインステータスコードの取得に失敗しました: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/ko/ko.json b/app/frontend/src/internationalization/trans/chefs/ko/ko.json index 48526fd28..cd31f849b 100644 --- a/app/frontend/src/internationalization/trans/chefs/ko/ko.json +++ b/app/frontend/src/internationalization/trans/chefs/ko/ko.json @@ -72,7 +72,8 @@ "eventSubscription": "イベントのサブスクリプション", "cdogsTemplate": "CDOGS 템플릿", "externalAPIs": "외부 API", - "eventStreamConfig": "이벤트 스트림 구성" + "eventStreamConfig": "이벤트 스트림 구성", + "formEmbed": "양식 임베딩" }, "documentTemplate": { "uploadTemplate": "CDOGS 템플릿 업로드", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "암호화 키 업데이트됨", "encryptionKeyCopySnackbar": "암호화 키가 클립보드에 복사되었습니다.", "encryptionKeyCopyTooltip": "암호화 키를 클립보드에 복사", - "encryptionKeyGenerate": "암호화 키 생성" + "encryptionKeyGenerate": "암호화 키 생성", + "formEmbed": "양식 임베딩" }, "formProfile": { "message": "CHEFS 팀은 포괄적인 비즈니스 케이스를 작성하는 데 중요한 입력으로 사용될 정보를 수집하고 조직하고 있습니다. 이러한 케이스는 향후 몇 년 동안 CHEFS의 전략적 운영과 지속적인 개선을 안내하는 데 중추적인 역할을 할 것입니다. 데이터 수집을 위한 이 이니셔티브는 중요한 결정에 정보를 제공하고 CHEFS의 궤도를 형성하는 데 필수적입니다. 이를 통해 CHEFS가 변화하는 필요와 도전에 대응하여 적응성과 효과를 보장합니다.", @@ -389,7 +391,8 @@ "developer": "개발자", "infoLinks": "정보 링크", "metrics": "측정항목", - "apis": "아피스" + "apis": "아피스", + "formEmbedding": "양식 임베딩" }, "adminUsersTable": { "search": "찾다", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "외부 API를 업데이트하는 중 오류가 발생했습니다.", "updateAPIsConsErrMsg": "외부 API 업데이트 중 오류 발생: {error}", "getAPICodesErrMsg": "외부 API 상태 코드 목록을 가져오는 중 오류가 발생했습니다.", - "getAPICodesConsErrMsg": "외부 API 상태 코드 목록을 가져오는 중 오류가 발생했습니다: {error}" + "getAPICodesConsErrMsg": "외부 API 상태 코드 목록을 가져오는 중 오류가 발생했습니다: {error}", + "getFormEmbedDomainStatusCodesErr": "도메인 상태 코드를 가져오는 데 실패했습니다.", + "getFormEmbedDomainStatusCodesConsErr": "도메인 상태 코드를 가져오는 데 실패했습니다: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "{formId}에 대한 이메일 템플릿을 로드하는 중에 오류가 발생했습니다. {error}", @@ -1220,6 +1225,58 @@ "editTitle": "외부 API 상태 업데이트", "allowSendUserToken": "'사용자 토큰 보내기' 허용" }, + "adminFormEmbed": { + "search": "검색", + "loadingText": "로딩 중... 잠시만 기다려주세요", + "ministry": "부처", + "ministryName": "부처명", + "formName": "양식", + "domain": "도메인", + "status": "상태", + "reason": "이유", + "requestedBy": "요청자", + "requestedAt": "요청 시간", + "actions": "작업", + "edit": "편집", + "editTitle": "양식 도메인 요청 상태 업데이트", + "save": "저장", + "delete": "삭제", + "previousStatus": "이전 상태", + "newStatus": "새 상태", + "createdAt": "생성일", + "createdBy": "생성자", + "viewHistory": "기록 보기" + }, + "formEmbed": { + "disclaimer": "이 CHEFS 양식을 귀하의 도메인에 임베드할 권한이 있는지 확인하세요.", + "domain": "도메인", + "status": "상태", + "reason": "이유", + "requestedAt": "요청 시간", + "actions": "작업", + "delete": "삭제", + "previousStatus": "이전 상태", + "newStatus": "새 상태", + "createdAt": "생성일", + "createdBy": "생성자", + "viewHistory": "기록 보기", + "requestDomain": "도메인 요청", + "domainRules": "유효한 도메인을 입력하세요 (예: example.com)", + "emptyFieldRules": "도메인은 필수입니다", + "newDomainHint": "프로토콜 없이 도메인을 입력하세요 (예: example.com)", + "listDomainsErr": "도메인 목록을 가져오는 중 오류가 발생했습니다.", + "listDomainsConsErr": "도메인 가져오기 오류: {error}", + "requestDomainSuccess": "도메인 요청이 성공적으로 제출되었습니다.", + "requestDomainErr": "도메인 요청 제출에 실패했습니다.", + "requestDomainConsErr": "도메인 요청 제출 중 오류: {error}", + "removeDomainSuccess": "도메인이 성공적으로 제거되었습니다.", + "removeDomainErr": "도메인 제거에 실패했습니다.", + "removeDomainConsErr": "도메인 제거 중 오류: {error}", + "getDomainHistoryErr": "도메인 기록을 가져오는데 실패했습니다.", + "getDomainHistoryConsErr": "도메인 기록 가져오기 오류: {error}", + "getStatusCodesErr": "도메인 상태 코드를 가져오는 데 실패했습니다.", + "getStatusCodesConsErr": "도메인 상태 코드를 가져오는 데 실패했습니다: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/pa/pa.json b/app/frontend/src/internationalization/trans/chefs/pa/pa.json index 5a31f3dde..57b47ed9f 100644 --- a/app/frontend/src/internationalization/trans/chefs/pa/pa.json +++ b/app/frontend/src/internationalization/trans/chefs/pa/pa.json @@ -72,7 +72,8 @@ "eventSubscription": "د پیښې ګډون", "cdogsTemplate": "CDOGS ਟੈਂਪਲੇਟ", "externalAPIs": "ਬਾਹਰੀ APIs", - "eventStreamConfig": "ਇਵੈਂਟ ਸਟ੍ਰੀਮ ਕੌਂਫਿਗਰੇਸ਼ਨ" + "eventStreamConfig": "ਇਵੈਂਟ ਸਟ੍ਰੀਮ ਕੌਂਫਿਗਰੇਸ਼ਨ", + "formEmbed": "ਫਾਰਮ ਇੰਬੈਡਿੰਗ" }, "documentTemplate": { "uploadTemplate": "CDOGS ਟੈਂਪਲੇਟ ਅੱਪਲੋਡ ਕਰੋ", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "ਏਨਕ੍ਰਿਪਸ਼ਨ ਕੁੰਜੀ ਦੁਆਰਾ ਅੱਪਡੇਟ ਕੀਤਾ ਗਿਆ", "encryptionKeyCopySnackbar": "ਇਨਕ੍ਰਿਪਸ਼ਨ ਕੁੰਜੀ ਕਲਿੱਪਬੋਰਡ 'ਤੇ ਕਾਪੀ ਕੀਤੀ ਗਈ", "encryptionKeyCopyTooltip": "ਐਨਕ੍ਰਿਪਸ਼ਨ ਕੁੰਜੀ ਨੂੰ ਕਲਿੱਪਬੋਰਡ ਵਿੱਚ ਕਾਪੀ ਕਰੋ", - "encryptionKeyGenerate": "ਐਨਕ੍ਰਿਪਸ਼ਨ ਕੁੰਜੀ ਬਣਾਓ" + "encryptionKeyGenerate": "ਐਨਕ੍ਰਿਪਸ਼ਨ ਕੁੰਜੀ ਬਣਾਓ", + "formEmbed": "ਫਾਰਮ ਇੰਬੈਡਿੰਗ" }, "formProfile": { "message": "CHEFS ਟੀਮ ਜਾਣਕਾਰੀ ਇਕੱਠੀ ਕਰ ਰਹੀ ਹੈ ਅਤੇ ਵਿਗਿਆਨ ਕੇਸਾਂ ਲਈ ਮੁਦਾਵਿਆਂ ਬਣਾਉਣ ਲਈ ਮੁਹਿਮ ਵਰਤ ਰਹੀ ਹੈ। ਇਹ ਕੇਸ ਸੀਧੇ CHEFS ਦੀ ਰਣਨੀਤਿਕ ਓਪਰੇਸ਼ਨ ਅਤੇ ਚਲਦੇ ਸਾਲਾਂ ਵਿੱਚ ਚੋਣ ਦੀ ਮੁਖਮਾਨੇ ਵਿੱਚ ਏਕ ਕੀ ਬੰਦੋਬਸਤੀ ਰੋਲ ਪਵੇਗਾ। ਡਾਟਾ ਇਕੱਠਾ ਕਰਨ ਦੀ ਇਹ ਪ੍ਰਯਾਸ਼ਾ ਜਰੂਰੀ ਹੈ ਜਿਸ ਨਾਲ ਕ੍ਰਿਟਿਕਲ ਫੈਸਲਿਆਂ ਨੂੰ ਸੂਚਿਤ ਕਰਨ ਲਈ ਅਤੇ CHEFS ਦੇ ਤਰੱਕੀਪੂਰਤ ਵਿੱਚ ਮੁਕਾਬਲਾ ਕਰਨ ਲਈ ਲੋੜੀਦਾ ਹੈ। ਇਸ ਨਾਲ ਯਹ ਸੁਨਿਸ਼ਚਿਤ ਹੋ ਜਾਂਦਾ ਹੈ ਕਿ ਇਹ ਆਪਣੇ ਅਨੁਕੂਲਨ ਅਤੇ ਚੁਣੇ ਗਏ ਚੁਣੌਤੀਆਂ ਅਤੇ ਜ਼ਰੂਰਾਤਾਂ ਦਾ ਇੱਜ਼ਤੀਕਾਰ ਅਤੇ ਯੋਜਨਾ ਵਿੱਚ ਇੱਕ ਬਦਲਾਵ ਅਤੇ ਕਾਰਗੁਜ਼ਾਰੀ ਵਿਚ ਇਹ ਗੁਣਤੀ ਹੈ।", @@ -389,7 +391,8 @@ "developer": "ਵਿਕਾਸਕਾਰ", "infoLinks": "ਜਾਣਕਾਰੀ ਲਿੰਕ", "metrics": "ਮੈਟ੍ਰਿਕਸ", - "apis": "APIs" + "apis": "APIs", + "formEmbedding": "ਫਾਰਮ ਇੰਬੈਡਿੰਗ" }, "adminUsersTable": { "search": "ਖੋਜ", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "ਬਾਹਰੀ API ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਵਿੱਚ ਤਰੁੱਟੀ।", "updateAPIsConsErrMsg": "ਬਾਹਰੀ API ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਵਿੱਚ ਤਰੁੱਟੀ: {error}", "getAPICodesErrMsg": "ਬਾਹਰੀ API ਸਥਿਤੀ ਕੋਡ ਸੂਚੀ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਤਰੁੱਟੀ।", - "getAPICodesConsErrMsg": "ਬਾਹਰੀ API ਸਥਿਤੀ ਕੋਡ ਸੂਚੀ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਤਰੁੱਟੀ: {error}" + "getAPICodesConsErrMsg": "ਬਾਹਰੀ API ਸਥਿਤੀ ਕੋਡ ਸੂਚੀ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਤਰੁੱਟੀ: {error}", + "getFormEmbedDomainStatusCodesErr": "ਡੋਮੇਨ ਸਥਿਤੀ ਕੋਡਾਂ ਨੂੰ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਅਸਫਲ।", + "getFormEmbedDomainStatusCodesConsErr": "ਡੋਮੇਨ ਸਥਿਤੀ ਕੋਡਾਂ ਨੂੰ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਅਸਫਲ: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "{formId} ਲਈ ਈਮੇਲ ਟੈਮਪਲੇਟ ਲੋਡ ਕਰਨ ਵਿੱਚ ਤਰੁੱਟੀ: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "ਬਾਹਰੀ API ਸਥਿਤੀ ਨੂੰ ਅੱਪਡੇਟ ਕਰੋ", "allowSendUserToken": "'ਯੂਜ਼ਰ ਟੋਕਨ ਭੇਜੋ' ਦੀ ਇਜਾਜ਼ਤ ਦਿਓ" }, + "adminFormEmbed": { + "search": "ਖੋਜ", + "loadingText": "ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ... ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕ ਕਰੋ", + "ministry": "ਮੰਤਰਾਲਾ", + "ministryName": "ਮੰਤਰਾਲੇ ਦਾ ਨਾਮ", + "formName": "ਫਾਰਮ", + "domain": "ਡੋਮੇਨ", + "status": "ਸਥਿਤੀ", + "reason": "ਕਾਰਨ", + "requestedBy": "ਬੇਨਤੀ ਕਰਨ ਵਾਲਾ", + "requestedAt": "ਬੇਨਤੀ ਦਾ ਸਮਾਂ", + "actions": "ਕਾਰਵਾਈਆਂ", + "edit": "ਸੋਧੋ", + "editTitle": "ਫਾਰਮ ਡੋਮੇਨ ਬੇਨਤੀ ਦੀ ਸਥਿਤੀ ਅੱਪਡੇਟ ਕਰੋ", + "save": "ਸੁਰੱਖਿਅਤ ਕਰੋ", + "delete": "ਮਿਟਾਓ", + "previousStatus": "ਪਿਛਲੀ ਸਥਿਤੀ", + "newStatus": "ਨਵੀਂ ਸਥਿਤੀ", + "createdAt": "ਬਣਾਇਆ ਗਿਆ", + "createdBy": "ਬਣਾਉਣ ਵਾਲਾ", + "viewHistory": "ਇਤਿਹਾਸ ਵੇਖੋ" + }, + "formEmbed": { + "disclaimer": "ਯਕੀਨੀ ਬਣਾਓ ਕਿ ਤੁਹਾਡੇ ਕੋਲ ਇਸ CHEFS ਫਾਰਮ ਨੂੰ ਆਪਣੇ ਡੋਮੇਨ ਵਿੱਚ ਇੰਬੈਡ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਹੈ।", + "domain": "ਡੋਮੇਨ", + "status": "ਸਥਿਤੀ", + "reason": "ਕਾਰਨ", + "requestedAt": "ਬੇਨਤੀ ਦਾ ਸਮਾਂ", + "actions": "ਕਾਰਵਾਈਆਂ", + "delete": "ਮਿਟਾਓ", + "previousStatus": "ਪਿਛਲੀ ਸਥਿਤੀ", + "newStatus": "ਨਵੀਂ ਸਥਿਤੀ", + "createdAt": "ਬਣਾਇਆ ਗਿਆ", + "createdBy": "ਬਣਾਉਣ ਵਾਲਾ", + "viewHistory": "ਇਤਿਹਾਸ ਵੇਖੋ", + "requestDomain": "ਡੋਮੇਨ ਲਈ ਬੇਨਤੀ ਕਰੋ", + "domainRules": "ਵੈਧ ਡੋਮੇਨ ਦਰਜ ਕਰੋ (ਉਦਾਹਰਨ ਲਈ, example.com)", + "emptyFieldRules": "ਡੋਮੇਨ ਜ਼ਰੂਰੀ ਹੈ", + "newDomainHint": "ਪ੍ਰੋਟੋਕੋਲ ਤੋਂ ਬਿਨਾਂ ਡੋਮੇਨ ਦਰਜ ਕਰੋ (ਉਦਾਹਰਨ ਲਈ, example.com)", + "listDomainsErr": "ਡੋਮੇਨਾਂ ਦੀ ਸੂਚੀ ਪ੍ਰਾਪਤ ਕਰਨ ਦੌਰਾਨ ਇੱਕ ਗਲਤੀ ਹੋਈ ਹੈ।", + "listDomainsConsErr": "ਡੋਮੇਨਾਂ ਨੂੰ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਗਲਤੀ: {error}", + "requestDomainSuccess": "ਡੋਮੇਨ ਬੇਨਤੀ ਸਫਲਤਾਪੂਰਵਕ ਜਮ੍ਹਾਂ ਕੀਤੀ ਗਈ।", + "requestDomainErr": "ਡੋਮੇਨ ਬੇਨਤੀ ਜਮ੍ਹਾਂ ਕਰਨ ਵਿੱਚ ਅਸਫਲ।", + "requestDomainConsErr": "ਡੋਮੇਨ ਬੇਨਤੀ ਜਮ੍ਹਾਂ ਕਰਨ ਵਿੱਚ ਗਲਤੀ: {error}", + "removeDomainSuccess": "ਡੋਮੇਨ ਸਫਲਤਾਪੂਰਵਕ ਹਟਾਇਆ ਗਿਆ।", + "removeDomainErr": "ਡੋਮੇਨ ਹਟਾਉਣ ਵਿੱਚ ਅਸਫਲ।", + "removeDomainConsErr": "ਡੋਮੇਨ ਹਟਾਉਣ ਵਿੱਚ ਗਲਤੀ: {error}", + "getDomainHistoryErr": "ਡੋਮੇਨ ਇਤਿਹਾਸ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਅਸਫਲ।", + "getDomainHistoryConsErr": "ਡੋਮੇਨ ਇਤਿਹਾਸ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਗਲਤੀ: {error}", + "getStatusCodesErr": "ਡੋਮੇਨ ਸਥਿਤੀ ਕੋਡਾਂ ਨੂੰ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਅਸਫਲ।", + "getStatusCodesConsErr": "ਡੋਮੇਨ ਸਥਿਤੀ ਕੋਡਾਂ ਨੂੰ ਪ੍ਰਾਪਤ ਕਰਨ ਵਿੱਚ ਅਸਫਲ: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/pt/pt.json b/app/frontend/src/internationalization/trans/chefs/pt/pt.json index 961124dbb..544fa3b8b 100644 --- a/app/frontend/src/internationalization/trans/chefs/pt/pt.json +++ b/app/frontend/src/internationalization/trans/chefs/pt/pt.json @@ -72,7 +72,8 @@ "eventSubscription": "Assinatura de evento", "cdogsTemplate": "modelo CDOGS", "externalAPIs": "APIs externas", - "eventStreamConfig": "Configuração do fluxo de eventos" + "eventStreamConfig": "Configuração do fluxo de eventos", + "formEmbed": "Incorporação de formulário" }, "documentTemplate": { "uploadTemplate": "Carregar modelo CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Chave de criptografia atualizada por", "encryptionKeyCopySnackbar": "Chave de criptografia copiada para a área de transferência", "encryptionKeyCopyTooltip": "Copiar chave de criptografia para a área de transferência", - "encryptionKeyGenerate": "Gerar chave de criptografia" + "encryptionKeyGenerate": "Gerar chave de criptografia", + "formEmbed": "Incorporação de formulário" }, "formProfile": { "message": "A equipe CHEFS está coletando e organizando informações para servir como entrada crucial para a criação de casos de negócios abrangentes. Esses casos desempenharão um papel fundamental na orientação da operação estratégica e na melhoria contínua do CHEFS nos próximos anos. Essa iniciativa de coleta de dados é essencial para informar decisões críticas e moldar a trajetória do CHEFS, garantindo sua adaptabilidade e eficácia em lidar com necessidades e desafios em evolução.", @@ -389,7 +391,8 @@ "developer": "Desenvolvedor", "infoLinks": "Links de informações", "metrics": "Métricas", - "apis": "APIs" + "apis": "APIs", + "formEmbedding": "Incorporação de formulário" }, "adminUsersTable": { "search": "Procurar", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "Erro ao atualizar API externa.", "updateAPIsConsErrMsg": "Erro ao atualizar API externa: {error}", "getAPICodesErrMsg": "Erro ao buscar a lista de códigos de status da API externa.", - "getAPICodesConsErrMsg": "Erro ao buscar a lista de códigos de status da API externa: {error}" + "getAPICodesConsErrMsg": "Erro ao buscar a lista de códigos de status da API externa: {error}", + "getFormEmbedDomainStatusCodesErr": "Falha ao buscar códigos de status de domínio.", + "getFormEmbedDomainStatusCodesConsErr": "Falha ao buscar códigos de status de domínio: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Erro ao carregar modelos de e-mail para {formId}: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "Atualizar status da API externa", "allowSendUserToken": "Permitir 'Enviar Token de Usuário'" }, + "adminFormEmbed": { + "search": "Pesquisar", + "loadingText": "Carregando... Por favor, aguarde", + "ministry": "Ministério", + "ministryName": "Nome do ministério", + "formName": "Formulário", + "domain": "Domínio", + "status": "Status", + "reason": "Motivo", + "requestedBy": "Solicitado por", + "requestedAt": "Solicitado em", + "actions": "Ações", + "edit": "Editar", + "editTitle": "Atualizar status da solicitação de domínio", + "save": "Salvar", + "delete": "Excluir", + "previousStatus": "Status anterior", + "newStatus": "Novo status", + "createdAt": "Criado em", + "createdBy": "Criado por", + "viewHistory": "Ver histórico" + }, + "formEmbed": { + "disclaimer": "Certifique-se de ter permissão para incorporar este formulário CHEFS em seu domínio.", + "domain": "Domínio", + "status": "Status", + "reason": "Motivo", + "requestedAt": "Solicitado em", + "actions": "Ações", + "delete": "Excluir", + "previousStatus": "Status anterior", + "newStatus": "Novo status", + "createdAt": "Criado em", + "createdBy": "Criado por", + "viewHistory": "Ver histórico", + "requestDomain": "Solicitar domínio", + "domainRules": "Digite um domínio válido (ex: exemplo.com)", + "emptyFieldRules": "O domínio é obrigatório", + "newDomainHint": "Digite o domínio sem protocolo (ex: exemplo.com)", + "listDomainsErr": "Ocorreu um erro ao buscar a lista de domínios.", + "listDomainsConsErr": "Erro ao buscar domínios: {error}", + "requestDomainSuccess": "Solicitação de domínio enviada com sucesso.", + "requestDomainErr": "Falha ao enviar solicitação de domínio.", + "requestDomainConsErr": "Erro ao enviar solicitação de domínio: {error}", + "removeDomainSuccess": "Domínio removido com sucesso.", + "removeDomainErr": "Falha ao remover domínio.", + "removeDomainConsErr": "Erro ao remover domínio: {error}", + "getDomainHistoryErr": "Falha ao buscar histórico do domínio.", + "getDomainHistoryConsErr": "Falha ao buscar histórico do domínio: {error}", + "getStatusCodesErr": "Falha ao buscar códigos de status de domínio.", + "getStatusCodesConsErr": "Falha ao buscar códigos de status de domínio: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/ru/ru.json b/app/frontend/src/internationalization/trans/chefs/ru/ru.json index 6538151e2..f985088ab 100644 --- a/app/frontend/src/internationalization/trans/chefs/ru/ru.json +++ b/app/frontend/src/internationalization/trans/chefs/ru/ru.json @@ -72,7 +72,8 @@ "eventSubscription": "Подписка на события", "cdogsTemplate": "Шаблон CDOGS", "externalAPIs": "Внешние API", - "eventStreamConfig": "Конфигурация потока событий" + "eventStreamConfig": "Конфигурация потока событий", + "formEmbed": "Встраивание формы" }, "documentTemplate": { "uploadTemplate": "Загрузить шаблон CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Ключ шифрования обновлен", "encryptionKeyCopySnackbar": "Ключ шифрования скопирован в буфер обмена", "encryptionKeyCopyTooltip": "Копировать ключ шифрования в буфер обмена", - "encryptionKeyGenerate": "Сгенерировать ключ шифрования" + "encryptionKeyGenerate": "Сгенерировать ключ шифрования", + "formEmbed": "Встраивание формы" }, "formProfile": { "message": "Команда CHEFS собирает и систематизирует информацию для создания ключевых аргументов в пользу формирования всесторонних деловых кейсов. Эти кейсы будут играть решающую роль в направлении стратегической операции и постоянного совершенствования CHEFS в ближайшие годы. Эта инициатива по сбору данных является необходимой для принятия важных решений и формирования траектории CHEFS, обеспечивая его адаптивность и эффективность в решении изменяющихся потребностей и вызовов.", @@ -389,7 +391,8 @@ "developer": "Разработчик", "infoLinks": "Информационные ссылки", "metrics": "Метрики", - "apis": "API-интерфейсы" + "apis": "API-интерфейсы", + "formEmbedding": "Встраивание формы" }, "adminUsersTable": { "search": "Поиск", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "Ошибка обновления внешнего API.", "updateAPIsConsErrMsg": "Ошибка обновления внешнего API: {error}", "getAPICodesErrMsg": "Ошибка при получении списка кодов состояния внешнего API.", - "getAPICodesConsErrMsg": "Ошибка при получении списка кодов состояния внешнего API: {error}" + "getAPICodesConsErrMsg": "Ошибка при получении списка кодов состояния внешнего API: {error}", + "getFormEmbedDomainStatusCodesErr": "Не удалось получить коды статуса домена.", + "getFormEmbedDomainStatusCodesConsErr": "Не удалось получить коды статуса домена: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Ошибка загрузки шаблонов электронной почты для {formId}: {error}.", @@ -1220,6 +1225,58 @@ "editTitle": "Обновить внешний статус API", "allowSendUserToken": "Разрешить «Отправить токен пользователя»" }, + "adminFormEmbed": { + "search": "Поиск", + "loadingText": "Загрузка... Пожалуйста, подождите", + "ministry": "Министерство", + "ministryName": "Название министерства", + "formName": "Форма", + "domain": "Домен", + "status": "Статус", + "reason": "Причина", + "requestedBy": "Запрошено", + "requestedAt": "Запрошено в", + "actions": "Действия", + "edit": "Редактировать", + "editTitle": "Обновить статус запроса домена", + "save": "Сохранить", + "delete": "Удалить", + "previousStatus": "Предыдущий статус", + "newStatus": "Новый статус", + "createdAt": "Создано", + "createdBy": "Создано", + "viewHistory": "Просмотр истории" + }, + "formEmbed": { + "disclaimer": "Убедитесь, что у вас есть разрешение на встраивание этой формы CHEFS в ваш домен.", + "domain": "Домен", + "status": "Статус", + "reason": "Причина", + "requestedAt": "Запрошено в", + "actions": "Действия", + "delete": "Удалить", + "previousStatus": "Предыдущий статус", + "newStatus": "Новый статус", + "createdAt": "Создано", + "createdBy": "Создано", + "viewHistory": "Просмотр истории", + "requestDomain": "Запросить домен", + "domainRules": "Введите действительный домен (например, example.com)", + "emptyFieldRules": "Домен обязателен", + "newDomainHint": "Введите домен без протокола (например, example.com)", + "listDomainsErr": "При получении списка доменов произошла ошибка.", + "listDomainsConsErr": "Ошибка при получении доменов: {error}", + "requestDomainSuccess": "Запрос домена успешно отправлен.", + "requestDomainErr": "Не удалось отправить запрос домена.", + "requestDomainConsErr": "Ошибка при отправке запроса домена: {error}", + "removeDomainSuccess": "Домен успешно удален.", + "removeDomainErr": "Не удалось удалить домен.", + "removeDomainConsErr": "Ошибка при удалении домена: {error}", + "getDomainHistoryErr": "Не удалось получить историю домена.", + "getDomainHistoryConsErr": "Не удалось получить историю домена: {error}", + "getStatusCodesErr": "Не удалось получить коды статуса домена.", + "getStatusCodesConsErr": "Не удалось получить коды статуса домена: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/tl/tl.json b/app/frontend/src/internationalization/trans/chefs/tl/tl.json index 236544a1e..807d5fcea 100644 --- a/app/frontend/src/internationalization/trans/chefs/tl/tl.json +++ b/app/frontend/src/internationalization/trans/chefs/tl/tl.json @@ -72,7 +72,8 @@ "eventSubscription": "Subscription sa Kaganapan", "cdogsTemplate": "template ng CDOGS", "externalAPIs": "Mga panlabas na API", - "eventStreamConfig": "Configuration ng Stream ng Event" + "eventStreamConfig": "Configuration ng Stream ng Event", + "formEmbed": "Pag-embed ng Form" }, "documentTemplate": { "uploadTemplate": "I-upload ang Template ng CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Encryption Key Na-update Ni", "encryptionKeyCopySnackbar": "Nakopya ang Encryption Key sa clipboard", "encryptionKeyCopyTooltip": "Kopyahin ang Encryption Key sa clipboard", - "encryptionKeyGenerate": "Bumuo ng Encryption Key" + "encryptionKeyGenerate": "Bumuo ng Encryption Key", + "formEmbed": "Pag-embed ng Form" }, "formProfile": { "message": "Ang CHEFS team ay nagkokolekta at nag-oorganisa ng impormasyon upang magsilbing mahalagang input para sa pagbuo ng kapsulang kaso ng negosyo. Ang mga kaso na ito ay magiging pangunahing bahagi sa paggabay sa pangangasiwa at patuloy na pagpapabuti ng CHEFS sa mga darating na taon. Ang inisyatibang ito sa pagsasama ng datos ay mahalaga para sa pagbibigay impormasyon sa mga kritikal na desisyon at pagsanay sa takbo ng CHEFS, tiyak na nagiging adaptable at epektibo sa pagsasaad sa mga nagbabagong pangangailangan at hamon.", @@ -389,7 +391,8 @@ "developer": "Developer", "infoLinks": "Mga Link ng Impormasyon", "metrics": "Mga sukatan", - "apis": "Mga API" + "apis": "Mga API", + "formEmbedding": "Pag-embed ng Form" }, "adminUsersTable": { "search": "Maghanap", @@ -1041,7 +1044,9 @@ "updateAPIsErrMsg": "Error sa pag-update ng External API.", "updateAPIsConsErrMsg": "Error sa pag-update ng External API: {error}", "getAPICodesErrMsg": "Error sa pagkuha ng listahan ng External API Status Codes.", - "getAPICodesConsErrMsg": "Error sa pagkuha ng listahan ng External API Status Codes: {error}" + "getAPICodesConsErrMsg": "Error sa pagkuha ng listahan ng External API Status Codes: {error}", + "getFormEmbedDomainStatusCodesErr": "Hindi makuha ang mga code ng katayuan ng domain.", + "getFormEmbedDomainStatusCodesConsErr": "Hindi makuha ang mga code ng katayuan ng domain: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Error sa paglo-load ng mga template ng email para sa {formId}: {error}", @@ -1218,6 +1223,58 @@ "editTitle": "I-update ang Katayuan ng Panlabas na API", "allowSendUserToken": "Payagan ang 'Ipadala ang Token ng User'" }, + "adminFormEmbed": { + "search": "Maghanap", + "loadingText": "Naglo-load... Mangyaring maghintay", + "ministry": "Ministeryo", + "ministryName": "Pangalan ng Ministeryo", + "formName": "Form", + "domain": "Domain", + "status": "Katayuan", + "reason": "Dahilan", + "requestedBy": "Hiniling Ni", + "requestedAt": "Hiniling Noong", + "actions": "Mga aksyon", + "edit": "I-edit", + "editTitle": "I-update ang Katayuan ng Kahilingan sa Domain ng Form", + "save": "I-save", + "delete": "Tanggalin", + "previousStatus": "Nakaraang Katayuan", + "newStatus": "Bagong Katayuan", + "createdAt": "Nilikha Noong", + "createdBy": "Nilikha Ni", + "viewHistory": "Tingnan ang Kasaysayan" + }, + "formEmbed": { + "disclaimer": "Siguraduhing may pahintulot kang i-embed ang CHEFS form na ito sa iyong domain.", + "domain": "Domain", + "status": "Katayuan", + "reason": "Dahilan", + "requestedAt": "Hiniling Noong", + "actions": "Mga aksyon", + "delete": "Tanggalin", + "previousStatus": "Nakaraang Katayuan", + "newStatus": "Bagong Katayuan", + "createdAt": "Nilikha Noong", + "createdBy": "Nilikha Ni", + "viewHistory": "Tingnan ang Kasaysayan", + "requestDomain": "Humiling ng Domain", + "domainRules": "Magpasok ng wastong domain (hal., example.com)", + "emptyFieldRules": "Kinakailangan ang domain", + "newDomainHint": "Ipasok ang domain nang walang protocol (hal., example.com)", + "listDomainsErr": "May naganap na error habang kinukuha ang listahan ng mga domain.", + "listDomainsConsErr": "Error sa pagkuha ng mga domain: {error}", + "requestDomainSuccess": "Matagumpay na naisumite ang kahilingan sa domain.", + "requestDomainErr": "Hindi maipasa ang kahilingan sa domain.", + "requestDomainConsErr": "Error sa pagsusumite ng kahilingan sa domain: {error}", + "removeDomainSuccess": "Matagumpay na natanggal ang domain.", + "removeDomainErr": "Hindi matanggal ang domain.", + "removeDomainConsErr": "Error sa pagtanggal ng domain: {error}", + "getDomainHistoryErr": "Hindi makuha ang kasaysayan ng domain.", + "getDomainHistoryConsErr": "Hindi nakuha ang kasaysayan ng domain: {error}", + "getStatusCodesErr": "Hindi makuha ang mga code ng katayuan ng domain.", + "getStatusCodesConsErr": "Hindi makuha ang mga code ng katayuan ng domain: {error}" + }, "download": { "chefsDataExport": "Pag-export ng Data ng CHEFS", "preparingForDownloading": "Naghahanda para sa pag-download...", diff --git a/app/frontend/src/internationalization/trans/chefs/uk/uk.json b/app/frontend/src/internationalization/trans/chefs/uk/uk.json index ad50dba1e..9a5b71b30 100644 --- a/app/frontend/src/internationalization/trans/chefs/uk/uk.json +++ b/app/frontend/src/internationalization/trans/chefs/uk/uk.json @@ -72,7 +72,8 @@ "eventSubscription": "Підписка на подію", "cdogsTemplate": "Шаблон CDOGS", "externalAPIs": "Зовнішні API", - "eventStreamConfig": "Конфігурація потоку подій" + "eventStreamConfig": "Конфігурація потоку подій", + "formEmbed": "Вбудовування форми" }, "documentTemplate": { "uploadTemplate": "Завантажити шаблон CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Ключ шифрування оновлено", "encryptionKeyCopySnackbar": "Ключ шифрування скопійовано в буфер обміну", "encryptionKeyCopyTooltip": "Копіювати ключ шифрування в буфер обміну", - "encryptionKeyGenerate": "Згенерувати ключ шифрування" + "encryptionKeyGenerate": "Згенерувати ключ шифрування", + "formEmbed": "Вбудовування форми" }, "formProfile": { "message": "Команда CHEFS збирає та організовує інформацію для надання ключового внеску у створення всебічних бізнес-кейсів. Ці кейси відіграють вирішальну роль у напрямку стратегічної операції та постійного вдосконалення CHEFS у наступні роки. Ця ініціатива зібрати дані є важливою для інформування критичних рішень та формування траєкторії CHEFS, забезпечуючи його адаптивність та ефективність у вирішенні змінюючихся потреб і викликів.", @@ -389,7 +391,8 @@ "developer": "Розробник", "infoLinks": "Інформаційні посилання", "metrics": "Метрики", - "apis": "API" + "apis": "API", + "formEmbedding": "Вбудовування форми" }, "adminUsersTable": { "search": "Пошук", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "Помилка оновлення зовнішнього API.", "updateAPIsConsErrMsg": "Помилка оновлення зовнішнього API: {error}", "getAPICodesErrMsg": "Помилка отримання списку кодів стану зовнішнього API.", - "getAPICodesConsErrMsg": "Помилка отримання списку кодів стану зовнішнього API: {error}" + "getAPICodesConsErrMsg": "Помилка отримання списку кодів стану зовнішнього API: {error}", + "getFormEmbedDomainStatusCodesErr": "Не вдалося отримати коди статусу домену.", + "getFormEmbedDomainStatusCodesConsErr": "Не вдалося отримати коди статусу домену: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Помилка завантаження шаблонів електронних листів для {formId}: {error}", @@ -1220,6 +1225,58 @@ "editTitle": "Оновити статус зовнішнього API", "allowSendUserToken": "Дозволити "Надіслати маркер користувача"" }, + "adminFormEmbed": { + "search": "Пошук", + "loadingText": "Завантаження... Будь ласка, зачекайте", + "ministry": "Міністерство", + "ministryName": "Назва міністерства", + "formName": "Форма", + "domain": "Домен", + "status": "Статус", + "reason": "Причина", + "requestedBy": "Запитано", + "requestedAt": "Запитано о", + "actions": "Дії", + "edit": "Редагувати", + "editTitle": "Оновити статус запиту домену форми", + "save": "Зберегти", + "delete": "Видалити", + "previousStatus": "Попередній статус", + "newStatus": "Новий статус", + "createdAt": "Створено", + "createdBy": "Створено", + "viewHistory": "Переглянути історію" + }, + "formEmbed": { + "disclaimer": "Переконайтесь, що у вас є дозвіл на вбудовування цієї форми CHEFS у ваш домен.", + "domain": "Домен", + "status": "Статус", + "reason": "Причина", + "requestedAt": "Запитано о", + "actions": "Дії", + "delete": "Видалити", + "previousStatus": "Попередній статус", + "newStatus": "Новий статус", + "createdAt": "Створено", + "createdBy": "Створено", + "viewHistory": "Переглянути історію", + "requestDomain": "Запитати домен", + "domainRules": "Введіть дійсний домен (наприклад, example.com)", + "emptyFieldRules": "Домен обов'язковий", + "newDomainHint": "Введіть домен без протоколу (наприклад, example.com)", + "listDomainsErr": "Сталася помилка при отриманні списку доменів.", + "listDomainsConsErr": "Помилка при отриманні доменів: {error}", + "requestDomainSuccess": "Запит домену успішно надіслано.", + "requestDomainErr": "Не вдалося надіслати запит домену.", + "requestDomainConsErr": "Помилка при надсиланні запиту домену: {error}", + "removeDomainSuccess": "Домен успішно видалено.", + "removeDomainErr": "Не вдалося видалити домен.", + "removeDomainConsErr": "Помилка при видаленні домену: {error}", + "getDomainHistoryErr": "Не вдалося отримати історію домену.", + "getDomainHistoryConsErr": "Помилка при отриманні історії домену: {error}", + "getStatusCodesErr": "Не вдалося отримати коди статусу домену.", + "getStatusCodesConsErr": "Не вдалося отримати коди статусу домену: {error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/vi/vi.json b/app/frontend/src/internationalization/trans/chefs/vi/vi.json index c778cfa21..79bd1cb0a 100644 --- a/app/frontend/src/internationalization/trans/chefs/vi/vi.json +++ b/app/frontend/src/internationalization/trans/chefs/vi/vi.json @@ -72,7 +72,8 @@ "eventSubscription": "Đăng ký sự kiện", "cdogsTemplate": "mẫu CDOGS", "externalAPIs": "API bên ngoài", - "eventStreamConfig": "Cấu hình luồng sự kiện" + "eventStreamConfig": "Cấu hình luồng sự kiện", + "formEmbed": "Nhúng biểu mẫu" }, "documentTemplate": { "uploadTemplate": "Tải lên mẫu CDOGS", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "Khóa mã hóa được cập nhật bởi", "encryptionKeyCopySnackbar": "Khóa mã hóa đã được sao chép vào bảng tạm", "encryptionKeyCopyTooltip": "Sao chép Khóa mã hóa vào clipboard", - "encryptionKeyGenerate": "Tạo khóa mã hóa" + "encryptionKeyGenerate": "Tạo khóa mã hóa", + "formEmbed": "Nhúng biểu mẫu" }, "formProfile": { "message": "Đội ngũ CHEFS đang thu thập và tổ chức thông tin để phục vụ như một đầu vào quan trọng cho việc xây dựng các trường hợp kinh doanh toàn diện. Những trường hợp này sẽ đóng một vai trò quan trọng trong hướng dẫn vận hành chiến lược và cải tiến liên tục của CHEFS trong những năm sắp tới. Sáng kiến này để thu thập dữ liệu là quan trọng để thông tin quyết định quan trọng và định hình quỹ đạo của CHEFS, đảm bảo tính linh hoạt và hiệu quả trong đối mặt với những nhu cầu và thách thức đang thay đổi.", @@ -389,7 +391,8 @@ "developer": "nhà phát triển", "infoLinks": "Liên kết thông tin", "metrics": "số liệu", - "apis": "API" + "apis": "API", + "formEmbedding": "Nhúng biểu mẫu" }, "adminUsersTable": { "search": "Tìm kiếm", @@ -1041,7 +1044,9 @@ "updateAPIsErrMsg": "Lỗi khi cập nhật API bên ngoài.", "updateAPIsConsErrMsg": "Lỗi khi cập nhật API bên ngoài: {error}", "getAPICodesErrMsg": "Có lỗi khi tải danh sách Mã trạng thái API bên ngoài.", - "getAPICodesConsErrMsg": "Lỗi khi tải danh sách Mã trạng thái API bên ngoài: {error}" + "getAPICodesConsErrMsg": "Lỗi khi tải danh sách Mã trạng thái API bên ngoài: {error}", + "getFormEmbedDomainStatusCodesErr": "Không thể lấy các mã trạng thái tên miền.", + "getFormEmbedDomainStatusCodesConsErr": "Không thể lấy các mã trạng thái tên miền: {error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "Lỗi tải mẫu email cho {formId}: {error}", @@ -1218,6 +1223,58 @@ "editTitle": "Cập nhật trạng thái API bên ngoài", "allowSendUserToken": "Cho phép 'Gửi mã thông báo người dùng'" }, + "adminFormEmbed": { + "search": "Tìm kiếm", + "loadingText": "Đang tải... Vui lòng đợi", + "ministry": "Bộ", + "ministryName": "Tên Bộ", + "formName": "Biểu mẫu", + "domain": "Tên miền", + "status": "Trạng thái", + "reason": "Lý do", + "requestedBy": "Yêu cầu bởi", + "requestedAt": "Yêu cầu lúc", + "actions": "Hành động", + "edit": "Chỉnh sửa", + "editTitle": "Cập nhật trạng thái yêu cầu tên miền biểu mẫu", + "save": "Lưu", + "delete": "Xóa", + "previousStatus": "Trạng thái trước", + "newStatus": "Trạng thái mới", + "createdAt": "Tạo lúc", + "createdBy": "Tạo bởi", + "viewHistory": "Xem lịch sử" + }, + "formEmbed": { + "disclaimer": "Đảm bảo rằng bạn được phép nhúng biểu mẫu CHEFS này vào tên miền của bạn.", + "domain": "Tên miền", + "status": "Trạng thái", + "reason": "Lý do", + "requestedAt": "Yêu cầu lúc", + "actions": "Hành động", + "delete": "Xóa", + "previousStatus": "Trạng thái trước", + "newStatus": "Trạng thái mới", + "createdAt": "Tạo lúc", + "createdBy": "Tạo bởi", + "viewHistory": "Xem lịch sử", + "requestDomain": "Yêu cầu tên miền", + "domainRules": "Nhập tên miền hợp lệ (ví dụ: example.com)", + "emptyFieldRules": "Tên miền là bắt buộc", + "newDomainHint": "Nhập tên miền không có giao thức (ví dụ: example.com)", + "listDomainsErr": "Đã xảy ra lỗi khi lấy danh sách tên miền.", + "listDomainsConsErr": "Lỗi khi lấy tên miền: {error}", + "requestDomainSuccess": "Yêu cầu tên miền đã được gửi thành công.", + "requestDomainErr": "Không thể gửi yêu cầu tên miền.", + "requestDomainConsErr": "Lỗi khi gửi yêu cầu tên miền: {error}", + "removeDomainSuccess": "Đã xóa tên miền thành công.", + "removeDomainErr": "Không thể xóa tên miền.", + "removeDomainConsErr": "Lỗi khi xóa tên miền: {error}", + "getDomainHistoryErr": "Không thể lấy lịch sử tên miền.", + "getDomainHistoryConsErr": "Lỗi khi lấy lịch sử tên miền: {error}", + "getStatusCodesErr": "Không thể lấy các mã trạng thái tên miền.", + "getStatusCodesConsErr": "Không thể lấy các mã trạng thái tên miền: {error}" + }, "download": { "chefsDataExport": "Xuất dữ liệu CHEFS", "preparingForDownloading": "Đang chuẩn bị tải xuống...", diff --git a/app/frontend/src/internationalization/trans/chefs/zh/zh.json b/app/frontend/src/internationalization/trans/chefs/zh/zh.json index a50077322..f96be9572 100644 --- a/app/frontend/src/internationalization/trans/chefs/zh/zh.json +++ b/app/frontend/src/internationalization/trans/chefs/zh/zh.json @@ -72,7 +72,8 @@ "eventSubscription": "E活动订阅", "cdogsTemplate": "CDOGS 模板", "externalAPIs": "外部 API", - "eventStreamConfig": "事件流配置" + "eventStreamConfig": "事件流配置", + "formEmbed": "表单嵌入" }, "documentTemplate": { "uploadTemplate": "上传CDOGS模板", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "加密密钥更新者", "encryptionKeyCopySnackbar": "加密密钥已复制到剪贴板", "encryptionKeyCopyTooltip": "将加密密钥复制到剪贴板", - "encryptionKeyGenerate": "生成加密密钥" + "encryptionKeyGenerate": "生成加密密钥", + "formEmbed": "表单嵌入" }, "formProfile": { "message": "CHEFS团队正在收集和组织信息,作为制定全面业务案例的关键输入。这些案例将在指导CHEFS未来几年的战略运作和持续改进中发挥关键作用。这一收集数据的倡议对于提供关键决策的信息和塑造CHEFS轨迹至关重要,确保其在应对不断变化的需求和挑战中的适应性和有效性。", @@ -389,7 +391,8 @@ "developer": "开发商", "infoLinks": "信息链接", "metrics": "指标", - "apis": "蜜蜂" + "apis": "蜜蜂", + "formEmbedding": "表单嵌入" }, "adminUsersTable": { "search": "搜索", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "更新外部 API 时出错。", "updateAPIsConsErrMsg": "更新外部 API 时出错:{error}", "getAPICodesErrMsg": "获取外部 API 状态代码列表时出错。", - "getAPICodesConsErrMsg": "获取外部 API 状态代码列表时出错:{error}" + "getAPICodesConsErrMsg": "获取外部 API 状态代码列表时出错:{error}", + "getFormEmbedDomainStatusCodesErr": "获取域名状态代码失败。", + "getFormEmbedDomainStatusCodesConsErr": "获取域名状态代码失败:{error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "加载 {formId} 的电子邮件模板时出错:{error}", @@ -1220,6 +1225,58 @@ "editTitle": "更新外部 API 状态", "allowSendUserToken": "允许‘发送用户令牌’" }, + "adminFormEmbed": { + "search": "搜索", + "loadingText": "加载中... 请稍候", + "ministry": "部门", + "ministryName": "部门名称", + "formName": "表单", + "domain": "域名", + "status": "状态", + "reason": "原因", + "requestedBy": "请求者", + "requestedAt": "请求时间", + "actions": "操作", + "edit": "编辑", + "editTitle": "更新表单域名请求状态", + "save": "保存", + "delete": "删除", + "previousStatus": "先前状态", + "newStatus": "新状态", + "createdAt": "创建时间", + "createdBy": "创建者", + "viewHistory": "查看历史" + }, + "formEmbed": { + "disclaimer": "确保您有权限在您的域名中嵌入此CHEFS表单。", + "domain": "域名", + "status": "状态", + "reason": "原因", + "requestedAt": "请求时间", + "actions": "操作", + "delete": "删除", + "previousStatus": "先前状态", + "newStatus": "新状态", + "createdAt": "创建时间", + "createdBy": "创建者", + "viewHistory": "查看历史", + "requestDomain": "请求域名", + "domainRules": "输入有效的域名(例如:example.com)", + "emptyFieldRules": "域名是必填项", + "newDomainHint": "输入域名,不含协议(例如:example.com)", + "listDomainsErr": "获取域名列表时发生错误。", + "listDomainsConsErr": "获取域名时出错:{error}", + "requestDomainSuccess": "域名请求已成功提交。", + "requestDomainErr": "提交域名请求失败。", + "requestDomainConsErr": "提交域名请求时出错:{error}", + "removeDomainSuccess": "域名已成功移除。", + "removeDomainErr": "移除域名失败。", + "removeDomainConsErr": "移除域名时出错:{error}", + "getDomainHistoryErr": "获取域名历史记录失败。", + "getDomainHistoryConsErr": "获取域名历史记录时出错:{error}", + "getStatusCodesErr": "获取域名状态代码失败。", + "getStatusCodesConsErr": "获取域名状态代码失败:{error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json b/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json index e0a3479b7..b2cd89221 100644 --- a/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json +++ b/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json @@ -72,7 +72,8 @@ "eventSubscription": "活動訂閱", "cdogsTemplate": "CDOGS 模板", "externalAPIs": "外部API", - "eventStreamConfig": "事件流配置" + "eventStreamConfig": "事件流配置", + "formEmbed": "表單嵌入" }, "documentTemplate": { "uploadTemplate": "上傳CDOGS範本", @@ -203,7 +204,8 @@ "encryptionKeyUpdatedBy": "加密金鑰更新者", "encryptionKeyCopySnackbar": "加密金鑰已複製到剪貼簿", "encryptionKeyCopyTooltip": "將加密金鑰複製到剪貼簿", - "encryptionKeyGenerate": "產生加密金鑰" + "encryptionKeyGenerate": "產生加密金鑰", + "formEmbed": "表單嵌入" }, "formProfile": { "message": "CHEFS團隊正在收集和組織信息,作為制定全面業務案例的關鍵輸入。這些案例將在指導CHEFS未來幾年的戰略運作和持續改進中發揮關鍵作用。這一收集數據的倡議對於提供關鍵決策的信息和塑造CHEFS軌跡至關重要,確保其在應對不斷變化的需求和挑戰中的適應性和有效性。", @@ -389,7 +391,8 @@ "developer": "開發商", "infoLinks": "信息鏈接", "metrics": "指標", - "apis": "蜜蜂" + "apis": "蜜蜂", + "formEmbedding": "表單嵌入" }, "adminUsersTable": { "search": "搜索", @@ -1043,7 +1046,9 @@ "updateAPIsErrMsg": "更新外部 API 時發生錯誤。", "updateAPIsConsErrMsg": "更新外部 API 時發生錯誤:{error}", "getAPICodesErrMsg": "取得外部 API 狀態代碼清單時發生錯誤。", - "getAPICodesConsErrMsg": "取得外部 API 狀態代碼清單時發生錯誤:{error}" + "getAPICodesConsErrMsg": "取得外部 API 狀態代碼清單時發生錯誤:{error}", + "getFormEmbedDomainStatusCodesErr": "獲取網域狀態碼失敗。", + "getFormEmbedDomainStatusCodesConsErr": "獲取網域狀態碼失敗:{error}" }, "form": { "fetchEmailTemplatesConsErrMsg": "加載 {formId} 的電子郵件模板時出錯:{error}", @@ -1220,6 +1225,58 @@ "editTitle": "更新外部 API 狀態", "allowSendUserToken": "允許“發送用戶令牌”" }, + "adminFormEmbed": { + "search": "搜尋", + "loadingText": "加載中... 請稍候", + "ministry": "部門", + "ministryName": "部門名稱", + "formName": "表單", + "domain": "網域", + "status": "狀態", + "reason": "原因", + "requestedBy": "請求者", + "requestedAt": "請求時間", + "actions": "操作", + "edit": "編輯", + "editTitle": "更新表單網域請求狀態", + "save": "儲存", + "delete": "刪除", + "previousStatus": "先前狀態", + "newStatus": "新狀態", + "createdAt": "建立時間", + "createdBy": "建立者", + "viewHistory": "查看歷史" + }, + "formEmbed": { + "disclaimer": "確保您有權限在您的網域中嵌入此CHEFS表單。", + "domain": "網域", + "status": "狀態", + "reason": "原因", + "requestedAt": "請求時間", + "actions": "操作", + "delete": "刪除", + "previousStatus": "先前狀態", + "newStatus": "新狀態", + "createdAt": "建立時間", + "createdBy": "建立者", + "viewHistory": "查看歷史", + "requestDomain": "請求網域", + "domainRules": "輸入有效的網域(例如:example.com)", + "emptyFieldRules": "網域是必填項", + "newDomainHint": "輸入網域,不含協議(例如:example.com)", + "listDomainsErr": "獲取網域列表時發生錯誤。", + "listDomainsConsErr": "獲取網域時出錯:{error}", + "requestDomainSuccess": "網域請求已成功提交。", + "requestDomainErr": "提交網域請求失敗。", + "requestDomainConsErr": "提交網域請求時出錯:{error}", + "removeDomainSuccess": "網域已成功移除。", + "removeDomainErr": "移除網域失敗。", + "removeDomainConsErr": "移除網域時出錯:{error}", + "getDomainHistoryErr": "獲取網域歷史記錄失敗。", + "getDomainHistoryConsErr": "獲取網域歷史記錄時出錯:{error}", + "getStatusCodesErr": "獲取網域狀態碼失敗。", + "getStatusCodesConsErr": "獲取網域狀態碼失敗:{error}" + }, "test": { "customError": "This is a custom error message for testing." }, diff --git a/app/frontend/src/services/adminService.js b/app/frontend/src/services/adminService.js index e90ec116d..a853a1b61 100644 --- a/app/frontend/src/services/adminService.js +++ b/app/frontend/src/services/adminService.js @@ -235,4 +235,78 @@ export default { `${ApiRoutes.ADMIN}/formcomponents/proactivehelp/list` ); }, + + // + // Form Embedding calls + // + + /** + * @function getFormEmbedDomainStatusCodes + * List all domain statuses + * @returns {Promise} An axios response + */ + getFormEmbedDomainStatusCodes() { + return appAxios().get(`${ApiRoutes.ADMIN}${ApiRoutes.EMBED}/statusCodes`); + }, + + /** + * @function getFormEmbedDomains + * Read all the form embed domains in the DB + * @param {boolean} paginationEnabled if pagination is enabled for this request + * @param {number} page the page for the request + * @param {number} itemsPerPage the number of items to be returned + * @param {boolean} searchEnabled if the results should be searched + * @param {string} search the search string for the query + * @returns {Promise} An axios response + */ + getFormEmbedDomains(params) { + return appAxios().get(`${ApiRoutes.ADMIN}${ApiRoutes.EMBED}`, { + params: params, + }); + }, + + /** + * @function getFormEmbedDomainHistory + * Read all the form embed domain history for a domain in the DB + * @param {formEmbedDomainId} formEmbedDomainId The form embed domain uuid + * @param {boolean} paginationEnabled if pagination is enabled for this request + * @param {number} page the page for the request + * @param {number} itemsPerPage the number of items to be returned + * @param {boolean} searchEnabled if the results should be searched + * @param {string} search the search string for the query + * @returns {Promise} An axios response + */ + getFormEmbedDomainHistory(formEmbedDomainId, params) { + return appAxios().get( + `${ApiRoutes.ADMIN}${ApiRoutes.EMBED}/${formEmbedDomainId}/history`, + { + params: params, + } + ); + }, + /** + * @function updateFormEmbedDomainRequest + * Review a Form Requested Domain record (status code only) + * @param {string} formEmbedDomainId The form embed domain uuid + * @param {Object} data An object containing an form requested domain record + * @returns {Promise} An axios response + */ + updateFormEmbedDomainRequest(formEmbedDomainId, data) { + return appAxios().put( + `${ApiRoutes.ADMIN}${ApiRoutes.EMBED}/${formEmbedDomainId}`, + data + ); + }, + /** + * @function removeFormEmbedDomainRequest + * Removes a Form Requested Domain record + * @param {string} formEmbedDomainId The form embed domain uuid + * @param {Object} data An object containing ssssssan form requested domain record + * @returns {Promise} An axios response + */ + removeFormEmbedDomainRequest(formEmbedDomainId) { + return appAxios().delete( + `${ApiRoutes.ADMIN}${ApiRoutes.EMBED}/${formEmbedDomainId}` + ); + }, }; diff --git a/app/frontend/src/services/embedService.js b/app/frontend/src/services/embedService.js new file mode 100644 index 000000000..742cc2b1f --- /dev/null +++ b/app/frontend/src/services/embedService.js @@ -0,0 +1,61 @@ +import { appAxios } from '~/services/interceptors'; +import { ApiRoutes } from '~/utils/constants'; + +export default { + /** + * @function getFormEmbedDomainStatusCodes + * List all domain statuses + * @param {string} formId The form uuid + * @returns {Promise} An axios response + */ + getFormEmbedDomainStatusCodes(formId) { + return appAxios().get(`${ApiRoutes.FORMS}/${formId}/embed/statusCodes`); + }, + + /** + * @function listDomains + * List all domains for a form + * @param {string} formId The form uuid + * @returns {Promise} An axios response + */ + listDomains(formId) { + return appAxios().get(`${ApiRoutes.FORMS}/${formId}/embed`); + }, + + /** + * @function requestDomain + * Request a domain to be added to allowed domains + * @param {string} formId The form uuid + * @param {Object} data The domain data + * @returns {Promise} An axios response + */ + requestDomain(formId, data) { + return appAxios().post(`${ApiRoutes.FORMS}/${formId}/embed`, data); + }, + + /** + * @function removeDomain + * Remove a domain from requested domains + * @param {string} formId The form uuid + * @param {string} formEmbedDomainId The form embed domain uuid + * @returns {Promise} An axios response + */ + removeDomain(formId, formEmbedDomainId) { + return appAxios().delete( + `${ApiRoutes.FORMS}/${formId}/embed/${formEmbedDomainId}` + ); + }, + + /** + * @function getDomainHistory + * List all domains for a form + * @param {string} formId The form uuid + * @param {string} formEmbedDomainId The form embed domain uuid + * @returns {Promise} An axios response + */ + getDomainHistory(formId, formEmbedDomainId) { + return appAxios().get( + `${ApiRoutes.FORMS}/${formId}/embed/${formEmbedDomainId}/history` + ); + }, +}; diff --git a/app/frontend/src/services/index.js b/app/frontend/src/services/index.js index 5dd1ff563..b6523cd0a 100755 --- a/app/frontend/src/services/index.js +++ b/app/frontend/src/services/index.js @@ -8,3 +8,4 @@ export { default as fileService } from './fileService'; export { default as utilsService } from './utilsService'; export { default as encryptionKeyService } from './encryptionKeyService'; export { default as eventStreamConfigService } from './eventStreamConfigService'; +export { default as embedService } from './embedService'; diff --git a/app/frontend/src/store/admin.js b/app/frontend/src/store/admin.js index a5648a626..3fa4aa818 100644 --- a/app/frontend/src/store/admin.js +++ b/app/frontend/src/store/admin.js @@ -21,6 +21,9 @@ export const useAdminStore = defineStore('admin', { fcProactiveHelp: {}, // Form Component Proactive Help fcProactiveHelpImageUrl: '', fcProactiveHelpGroupList: [], + formEmbedDomainsList: [], + formEmbedDomainsTotal: undefined, + formEmbedDomainStatusCodes: [], }), getters: {}, actions: { @@ -338,5 +341,103 @@ export const useAdminStore = defineStore('admin', { }); } }, + + // + // External APIs + // + async getFormEmbedDomainStatusCodes() { + try { + this.formEmbedDomainStatusCodes = []; + const response = await adminService.getFormEmbedDomainStatusCodes(); + this.formEmbedDomainStatusCodes = response.data; + } catch (error) { + const notificationStore = useNotificationStore(); + notificationStore.addNotification({ + text: i18n.t('trans.store.admin.getFormEmbedDomainStatusCodesErr'), + consoleError: i18n.t( + 'trans.store.admin.getFormEmbedDomainStatusCodesConsErrMsg', + { + error: error, + } + ), + }); + } + }, + async getFormEmbedDomains(params) { + try { + // Get all external apis + this.formEmbedDomainsList = []; + const response = await adminService.getFormEmbedDomains(params); + if (response.data.results) { + this.formEmbedDomainsList = response.data.results; + this.formEmbedDomainsTotal = response.data.total; + } else { + this.formEmbedDomainsList = response.data; + this.formEmbedDomainsTotal = response.data.length; + } + } catch (error) { + const notificationStore = useNotificationStore(); + notificationStore.addNotification({ + text: i18n.t('trans.store.admin.getFormEmbedDomainsErrMsg'), + consoleError: i18n.t( + 'trans.store.admin.getFormEmbedDomainsConsErrMsg', + { + error: error, + } + ), + }); + } + }, + async getFormEmbedDomainHistory(formEmbedDomainId) { + try { + const response = await adminService.getFormEmbedDomainHistory( + formEmbedDomainId + ); + return response.data; + } catch (error) { + const notificationStore = useNotificationStore(); + notificationStore.addNotification({ + text: i18n.t('trans.store.admin.getFormEmbedDomainHistoryErrMsg'), + consoleError: i18n.t( + 'trans.store.admin.getFormEmbedDomainHistoryConsErrMsg', + { + error: error, + } + ), + }); + } + }, + async updateFormEmbedDomainRequest(id, data) { + try { + await adminService.updateFormEmbedDomainRequest(id, data); + } catch (error) { + const notificationStore = useNotificationStore(); + notificationStore.addNotification({ + text: i18n.t('trans.store.admin.updateFormEmbedDomainRequestErrMsg'), + consoleError: i18n.t( + 'trans.store.admin.updateFormEmbedDomainRequestConsErrMsg', + { + error: error, + } + ), + }); + } + }, + async removeFormEmbedDomainRequest(formEmbedDomainId) { + try { + await adminService.removeFormEmbedDomainRequest(formEmbedDomainId); + } catch (error) { + const notificationStore = useNotificationStore(); + notificationStore.addNotification({ + text: i18n.t('trans.store.admin.removeFormEmbedDomainRequestErrMsg'), + consoleError: i18n.t( + 'trans.store.admin.removeFormEmbedDomainRequestConsErrMsg', + { + error: error, + } + ), + }); + } + }, }, }); diff --git a/app/frontend/src/utils/constants.js b/app/frontend/src/utils/constants.js index 2b9361700..4dd618abb 100755 --- a/app/frontend/src/utils/constants.js +++ b/app/frontend/src/utils/constants.js @@ -19,6 +19,7 @@ export const ApiRoutes = Object.freeze({ FORM_METADATA: '/formMetadata', EVENT_STREAM_CONFIG: '/eventStreamConfig', ENCRYPTION_KEY: '/encryptionKey', + EMBED: '/embed', }); /** Roles a user can have on a form. These are defined in the DB and sent from the API */ diff --git a/app/frontend/src/utils/embedUtils.js b/app/frontend/src/utils/embedUtils.js new file mode 100644 index 000000000..fe11cf884 --- /dev/null +++ b/app/frontend/src/utils/embedUtils.js @@ -0,0 +1,41 @@ +/** + * Initializes embed functionality on the form + * @param {string} formId The form ID + */ +export function initFormEmbed(formId) { + // Check if we're in an iframe and it's from an allowed domain + if (window.self !== window.top) { + // We're in an iframe + // Set up a resize observer to adjust iframe height + const resizeForm = () => { + const height = document.body.scrollHeight; + window.parent.postMessage( + { + type: 'resize', + formId: formId, + height: height, + }, + '*' + ); // Using * as a temporary measure; will be filtered by receiving side + }; + + // Create and observe body for size changes + const resizeObserver = new ResizeObserver(() => { + resizeForm(); + }); + + resizeObserver.observe(document.body); + + // Also resize on window load and resize + window.addEventListener('load', resizeForm); + window.addEventListener('resize', resizeForm); + } +} + +/** + * Checks if the current form is embedded + * @returns {boolean} True if form is embedded + */ +export function isFormEmbedded() { + return window.self !== window.top; +} diff --git a/app/frontend/tests/unit/utils/constants.spec.js b/app/frontend/tests/unit/utils/constants.spec.js index d8446657c..4fb146a58 100644 --- a/app/frontend/tests/unit/utils/constants.spec.js +++ b/app/frontend/tests/unit/utils/constants.spec.js @@ -19,6 +19,7 @@ describe('Constants', () => { FORM_METADATA: '/formMetadata', ENCRYPTION_KEY: '/encryptionKey', EVENT_STREAM_CONFIG: '/eventStreamConfig', + EMBED: '/embed', }); }); diff --git a/app/src/db/migrations/20250702151906_074_embedded-forms-support.js b/app/src/db/migrations/20250702151906_074_embedded-forms-support.js new file mode 100644 index 000000000..de1a1928c --- /dev/null +++ b/app/src/db/migrations/20250702151906_074_embedded-forms-support.js @@ -0,0 +1,94 @@ +const stamps = require('../stamps'); +const { FormEmbedDomainStatuses } = require('../../forms/common/constants'); +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +const CREATED_BY = 'migration-74'; + +const statusCodes = [ + { code: FormEmbedDomainStatuses.SUBMITTED, display: 'Submitted', createdBy: CREATED_BY }, + { code: FormEmbedDomainStatuses.PENDING, display: 'Pending', createdBy: CREATED_BY }, + { code: FormEmbedDomainStatuses.APPROVED, display: 'Approved', createdBy: CREATED_BY }, + { code: FormEmbedDomainStatuses.DENIED, display: 'Denied', createdBy: CREATED_BY }, +]; +exports.up = function (knex) { + return ( + Promise.resolve() + .then(() => + knex.schema.createTable('form_embed_domain_status_code', (table) => { + table.string('code').primary(); + table.string('display').notNullable(); + stamps(knex, table); + }) + ) + // seed the table + .then(() => { + return knex('form_embed_domain_status_code').insert(statusCodes); + }) + .then(() => + knex.schema.createTable('form_embed_domain', (table) => { + table.uuid('id').primary(); + table.uuid('formId').references('id').inTable('form').notNullable().index(); + table.string('domain'); + table.string('status').references('code').inTable('form_embed_domain_status_code').defaultTo('submitted'); // submitted, pending, approved, denied + table.timestamp('requestedAt', { useTz: true }).defaultTo(knex.fn.now()); + table.string('requestedBy').notNullable(); + table.unique(['formId', 'domain']); + }) + ) + .then(() => + knex.schema.createTable('form_embed_domain_history', (table) => { + table.uuid('id').primary(); + table.uuid('formEmbedDomainId').references('id').inTable('form_embed_domain').notNullable().index(); + table.string('previousStatus'); + table.string('newStatus').notNullable(); + table.text('reason').nullable(); + stamps(knex, table); + }) + ) + .then(() => + knex.schema.raw( + `create or replace view form_embed_domain_vw as + select frd.id, f.ministry, f.name as "formName", frd."domain", + frd.status, frd."requestedAt", frd."requestedBy" + from form_embed_domain frd + inner join form f on frd."formId" = f.id + order by f.ministry, "formName", frd."domain";` + ) + ) + .then(() => + knex.schema.raw( + `create or replace view form_embed_domain_history_vw as + select + frd.id as "formEmbedDomainId", + f.ministry, + f.name as "formName", + frd."domain", + h.id as "historyId", + h."previousStatus", + h."newStatus", + h.reason, + h."createdAt" as "statusChangedAt", + h."createdBy" as "statusChangedBy" + from form_embed_domain_history h + inner join form_embed_domain frd on h."formEmbedDomainId" = frd.id + inner join form f on frd."formId" = f.id + order by h."createdAt" desc;` + ) + ) + ); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return Promise.resolve() + .then(() => knex.schema.raw('DROP VIEW IF EXISTS form_embed_domain_history_vw')) + .then(() => knex.schema.raw('DROP VIEW IF EXISTS form_embed_domain_vw')) + .then(() => knex.schema.dropTableIfExists('form_embed_domain_history')) + .then(() => knex.schema.dropTableIfExists('form_embed_domain')) + .then(() => knex.schema.dropTableIfExists('form_embed_domain_status_code')); +}; diff --git a/app/src/forms/admin/controller.js b/app/src/forms/admin/controller.js index d25b1ea66..cc09be784 100644 --- a/app/src/forms/admin/controller.js +++ b/app/src/forms/admin/controller.js @@ -169,4 +169,91 @@ module.exports = { next(error); } }, + + // Form Embedding + + /** + * @function getFormEmbedDomains + * Gets all requested form embed domain status codes + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next Express next middleware function + * @returns {function} Express middleware function + */ + getFormEmbedDomainStatusCodes: async (req, res, next) => { + try { + const response = await service.getFormEmbedDomainStatusCodes(); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + /** + * @function getFormEmbedDomains + * Gets all requested domains with filtering and pagination + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next Express next middleware function + * @returns {function} Express middleware function + */ + getFormEmbedDomains: async (req, res, next) => { + try { + const response = await service.getFormEmbedDomains(req.query); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + /** + * @function getFormEmbedDomainHistory + * Gets history for a specific domain + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next Express next middleware function + * @returns {function} Express middleware function + */ + getFormEmbedDomainHistory: async (req, res, next) => { + try { + const response = await service.getFormEmbedDomainHistory(req.params.formEmbedDomainId); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + /** + * @function updateFormEmbedDomainRequest + * Updates a domain request (approve/deny) + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next Express next middleware function + * @returns {function} Express middleware function + */ + updateFormEmbedDomainRequest: async (req, res, next) => { + try { + const response = await service.updateFormEmbedDomainRequest(req.params.formEmbedDomainId, req.body, req.currentUser); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + /** + * @function removeFormEmbedDomainRequest + * Permanently removes a domain request + * @param {object} req Express request object + * @param {object} res Express response object + * @param {function} next Express next middleware function + * @returns {function} Express middleware function + */ + removeFormEmbedDomainRequest: async (req, res, next) => { + try { + await service.removeFormEmbedDomain(req.params.formEmbedDomainId); + res.status(204).send(); + } catch (error) { + next(error); + } + }, }; diff --git a/app/src/forms/admin/routes.js b/app/src/forms/admin/routes.js index 638075ac4..866d938a2 100644 --- a/app/src/forms/admin/routes.js +++ b/app/src/forms/admin/routes.js @@ -18,6 +18,7 @@ routes.param('externalApiId', validateParameter.validateExternalAPIId); routes.param('formId', validateParameter.validateFormId); routes.param('formVersionId', validateParameter.validateFormVersionId); routes.param('userId', validateParameter.validateUserId); +routes.param('formEmbedDomainId', validateParameter.validateFormEmbedDomainId); // // Forms @@ -103,4 +104,31 @@ routes.get('/formcomponents/proactivehelp/list', async (req, res, next) => { await controller.listFormComponentsProactiveHelp(req, res, next); }); +// +// Form Embedding +// + +// List domains for a form +routes.get('/embed/statusCodes', async (req, res, next) => { + await controller.getFormEmbedDomainStatusCodes(req, res, next); +}); + +routes.get('/embed', async (req, res, next) => { + await controller.getFormEmbedDomains(req, res, next); +}); + +// Get history for a specific domain +routes.get('/embed/:formEmbedDomainId/history', async (req, res, next) => { + await controller.getFormEmbedDomainHistory(req, res, next); +}); + +// Update a domain request (approve/deny) +routes.put('/embed/:formEmbedDomainId', async (req, res, next) => { + await controller.updateFormEmbedDomainRequest(req, res, next); +}); + +// Remove a domain request +routes.delete('/embed/:formEmbedDomainId', async (req, res, next) => { + await controller.removeFormEmbedDomainRequest(req, res, next); +}); module.exports = routes; diff --git a/app/src/forms/admin/service.js b/app/src/forms/admin/service.js index 737f5873d..b03655332 100644 --- a/app/src/forms/admin/service.js +++ b/app/src/forms/admin/service.js @@ -1,5 +1,20 @@ +const Problem = require('api-problem'); const { ExternalAPIStatuses } = require('../common/constants'); -const { Form, FormVersion, User, UserFormAccess, FormComponentsProactiveHelp, AdminExternalAPI, ExternalAPI, ExternalAPIStatusCode } = require('../common/models'); +const { + Form, + FormVersion, + User, + UserFormAccess, + FormComponentsProactiveHelp, + AdminExternalAPI, + ExternalAPI, + ExternalAPIStatusCode, + FormEmbedDomain, + FormEmbedDomainHistory, + FormEmbedDomainStatusCode, + FormEmbedDomainVw, + FormEmbedDomainHistoryVw, +} = require('../common/models'); const { queryUtils, typeUtils } = require('../common/utils'); const moment = require('moment'); const uuid = require('uuid'); @@ -421,6 +436,120 @@ const service = { } return {}; }, + + // + // Form Embedding + // + /** + * @function getFormEmbedDomainStatusCodes + * Gets all form embed domain status codes + * @returns {Promise} An array of requested domains + */ + getFormEmbedDomainStatusCodes: async () => { + return FormEmbedDomainStatusCode.query(); + }, + + /** + * @function getFormEmbedDomains + * Gets all requested domains with pagination and filtering + * @param {Object} params Query parameters + * @returns {Promise} Object with results and total count + */ + getFormEmbedDomains: async (params) => { + const query = FormEmbedDomainVw.query() + .modify('filterMinistry', params.ministry) + .modify('filterFormName', params.formName) + .modify('filterDomain', params.domain) + .modify('orderByRequestedAt'); + + if (params.paginationEnabled) { + return await service._processPagination(query, { + page: parseInt(params.page), + itemsPerPage: parseInt(params.itemsPerPage), + totalItems: params.totalItems, + search: params.search, + searchEnabled: params.searchEnabled, + }); + } + return query; + }, + + /** + * @function getFormEmbedDomainHistory + * Gets history for a specific domain + * @param {string} formEmbedDomainId The domain uuid + * @returns {Promise} Domain history records + */ + getFormEmbedDomainHistory: async (formEmbedDomainId) => { + return FormEmbedDomainHistoryVw.query().where('formEmbedDomainId', formEmbedDomainId).orderBy('statusChangedAt', 'desc'); + }, + + /** + * @function updateFormEmbedDomainRequest + * Reviews a domain request (approve/deny) + * @param {string} formEmbedDomainId The domain uuid + * @param {Object} data The review data + * @param {Object} currentUser The current user + * @returns {Promise} The updated domain + */ + updateFormEmbedDomainRequest: async (formEmbedDomainId, data, currentUser) => { + const domain = await FormEmbedDomain.query().findById(formEmbedDomainId); + if (!domain) { + throw new Problem(404, 'Domain request not found'); + } + + let trx; + try { + trx = await FormEmbedDomain.startTransaction(); + + // Add history record + await FormEmbedDomainHistory.query(trx).insert({ + id: uuid.v4(), + formEmbedDomainId: formEmbedDomainId, + previousStatus: domain.status, + newStatus: data.status, + reason: data.reason || null, + createdBy: currentUser.usernameIdp, + updatedBy: currentUser.usernameIdp, + }); + + // Update domain status + const updated = await FormEmbedDomain.query(trx).patchAndFetchById(formEmbedDomainId, { + status: data.status, + }); + + await trx.commit(); + return updated; + } catch (error) { + if (trx) await trx.rollback(); + throw error; + } + }, + + /** + * @function removeFormEmbedDomain + * Permanently removes a domain + * @param {string} formEmbedDomainId The domain uuid + * @returns {Promise} Number of deleted records + */ + removeFormEmbedDomain: async (formEmbedDomainId) => { + let trx; + try { + trx = await FormEmbedDomain.startTransaction(); + + // Delete history first + await FormEmbedDomainHistory.query(trx).where('formEmbedDomainId', formEmbedDomainId).delete(); + + // Then delete the domain + const deleted = await FormEmbedDomain.query(trx).deleteById(formEmbedDomainId); + + await trx.commit(); + return deleted; + } catch (error) { + if (trx) await trx.rollback(); + throw error; + } + }, }; module.exports = service; diff --git a/app/src/forms/common/constants.js b/app/src/forms/common/constants.js index cefeb49fa..9296b8b65 100644 --- a/app/src/forms/common/constants.js +++ b/app/src/forms/common/constants.js @@ -112,4 +112,10 @@ module.exports = Object.freeze({ APPROVED: 'APPROVED', DENIED: 'DENIED', }, + FormEmbedDomainStatuses: { + SUBMITTED: 'SUBMITTED', + PENDING: 'PENDING', + APPROVED: 'APPROVED', + DENIED: 'DENIED', + }, }); diff --git a/app/src/forms/common/middleware/embed.js b/app/src/forms/common/middleware/embed.js new file mode 100644 index 000000000..84eebe548 --- /dev/null +++ b/app/src/forms/common/middleware/embed.js @@ -0,0 +1,50 @@ +const embedService = require('../../embed/service'); +const log = require('../../../components/log'); + +/** + * Middleware to check if embedding is allowed + * @param {Object} req The request + * @param {Object} res The response + * @param {Function} next The next middleware + */ +const embedSecurityMiddleware = async (req, res, next) => { + try { + const formId = req.params.formId; + const origin = req.headers.origin; + + // Set default CSP to deny embedding + let csp = "frame-ancestors 'self';"; + let xFrameOptions = 'SAMEORIGIN'; + + // If we have an origin and formId, check if embedding is allowed + if (formId && origin) { + try { + const isAllowed = await embedService.isEmbedAllowed(formId, origin); + + if (isAllowed) { + // Allow embedding from the specific origin + csp = `frame-ancestors 'self' ${origin};`; + xFrameOptions = `ALLOW-FROM ${origin}`; + } + } catch (error) { + // Log that we're defaulting to restrictive headers due to error + log.error('Error checking embed permissions - defaulting to restrictive headers', { + error: error.message, + formId, + origin, + }); + // We intentionally continue with default restrictive headers + } + } + + // Set security headers + res.setHeader('Content-Security-Policy', csp); + res.setHeader('X-Frame-Options', xFrameOptions); + + next(); + } catch (error) { + next(error); + } +}; + +module.exports = embedSecurityMiddleware; diff --git a/app/src/forms/common/middleware/validateParameter.js b/app/src/forms/common/middleware/validateParameter.js index f9592ef5d..10fab8798 100644 --- a/app/src/forms/common/middleware/validateParameter.js +++ b/app/src/forms/common/middleware/validateParameter.js @@ -305,6 +305,24 @@ const validateFormEncryptionKeyId = async (req, _res, next, formEncryptionKeyId) } }; +/** + * Validates that the :formEmbedDomainId route parameter exists and is a UUID. + * + * @param {*} _req the Express object representing the HTTP request - unused. + * @param {*} _res the Express object representing the HTTP response - unused. + * @param {*} next the Express chaining function. + * @param {*} formEmbedDomainId the :formEmbedDomainId value from the route. + */ +const validateFormEmbedDomainId = async (_req, _res, next, formEmbedDomainId) => { + try { + _validateUuid(formEmbedDomainId, 'formEmbedDomainId'); + + next(); + } catch (error) { + next(error); + } +}; + module.exports = { validateComponentId, validateDocumentTemplateId, @@ -318,4 +336,5 @@ module.exports = { validateRoleCode, validateUserId, validateFormEncryptionKeyId, + validateFormEmbedDomainId, }; diff --git a/app/src/forms/common/models/index.js b/app/src/forms/common/models/index.js index 2a30db4e3..a37beae7d 100644 --- a/app/src/forms/common/models/index.js +++ b/app/src/forms/common/models/index.js @@ -30,6 +30,9 @@ module.exports = { FormMetadata: require('./tables/formMetadata'), FormEncryptionKey: require('./tables/formEncryptionKey'), FormEventStreamConfig: require('./tables/formEventStreamConfig'), + FormEmbedDomain: require('./tables/formEmbedDomain'), + FormEmbedDomainHistory: require('./tables/formEmbedDomainHistory'), + FormEmbedDomainStatusCode: require('./tables/formEmbedDomainStatusCode'), // Views FormSubmissionUserPermissions: require('./views/formSubmissionUserPermissions'), @@ -39,4 +42,6 @@ module.exports = { UserFormAccess: require('./views/userFormAccess'), UserSubmissions: require('./views/userSubmissions'), AdminExternalAPI: require('./views/adminExternalAPI'), + FormEmbedDomainVw: require('./views/formEmbedDomainVw'), + FormEmbedDomainHistoryVw: require('./views/formEmbedDomainHistoryVw'), }; diff --git a/app/src/forms/common/models/tables/formEmbedDomain.js b/app/src/forms/common/models/tables/formEmbedDomain.js new file mode 100644 index 000000000..b130fb137 --- /dev/null +++ b/app/src/forms/common/models/tables/formEmbedDomain.js @@ -0,0 +1,90 @@ +const { Model } = require('objection'); +const { Regex } = require('../../constants'); + +class FormEmbedDomain extends Model { + static get tableName() { + return 'form_embed_domain'; + } + + static get idColumn() { + return 'id'; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['formId', 'domain', 'status', 'requestedBy'], + properties: { + id: { type: 'string', pattern: Regex.UUID }, + formId: { type: 'string', pattern: Regex.UUID }, + domain: { type: 'string', minLength: 1, maxLength: 255 }, + status: { type: 'string', minLength: 1, maxLength: 50 }, + requestedAt: { type: ['string', 'null'] }, + requestedBy: { type: ['string', 'null'] }, + }, + }; + } + + static get relationMappings() { + const Form = require('./form'); + const FormEmbedDomainStatusCode = require('./formEmbedDomainStatusCode'); + const FormEmbedDomainHistory = require('./formEmbedDomainHistory'); + + return { + form: { + relation: Model.BelongsToOneRelation, + modelClass: Form, + join: { + from: 'form_embed_domain.formId', + to: 'form.id', + }, + }, + statusCode: { + relation: Model.BelongsToOneRelation, + modelClass: FormEmbedDomainStatusCode, + join: { + from: 'form_embed_domain.status', + to: 'form_embed_domain_status_code.code', + }, + }, + history: { + relation: Model.HasManyRelation, + modelClass: FormEmbedDomainHistory, + join: { + from: 'form_embed_domain.id', + to: 'form_embed_domain_history.formEmbedDomainId', + }, + }, + }; + } + + static get modifiers() { + return { + filterFormId(query, formId) { + if (formId) { + query.where('formId', formId); + } + }, + filterDomain(query, domain) { + if (domain) { + query.where('domain', 'ilike', `%${domain}%`); + } + }, + filterStatus(query, status) { + if (status) { + query.where('status', status); + } + }, + filterRequestedBy(query, requestedBy) { + if (requestedBy) { + query.where('requestedBy', requestedBy); + } + }, + orderByRequestDate(query, direction = 'desc') { + query.orderBy('requestedAt', direction); + }, + }; + } +} + +module.exports = FormEmbedDomain; diff --git a/app/src/forms/common/models/tables/formEmbedDomainHistory.js b/app/src/forms/common/models/tables/formEmbedDomainHistory.js new file mode 100644 index 000000000..3417cdd19 --- /dev/null +++ b/app/src/forms/common/models/tables/formEmbedDomainHistory.js @@ -0,0 +1,72 @@ +const { Model } = require('objection'); +const { Timestamps } = require('../mixins'); +const { Regex } = require('../../constants'); +const stamps = require('../jsonSchema').stamps; + +class FormEmbedDomainHistory extends Timestamps(Model) { + static get tableName() { + return 'form_embed_domain_history'; + } + + static get idColumn() { + return 'id'; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['id', 'formEmbedDomainId', 'newStatus', 'createdBy'], + properties: { + id: { type: 'string', pattern: Regex.UUID }, + formEmbedDomainId: { type: 'string', pattern: Regex.UUID }, + previousStatus: { type: ['string', 'null'], maxLength: 50 }, + newStatus: { type: 'string', minLength: 1, maxLength: 50 }, + reason: { type: ['string', 'null'] }, + ...stamps, + }, + }; + } + + static get relationMappings() { + const FormEmbedDomain = require('./formEmbedDomain'); + + return { + domain: { + relation: Model.BelongsToOneRelation, + modelClass: FormEmbedDomain, + join: { + from: 'form_embed_domain_history.formEmbedDomainId', + to: 'form_embed_domain.id', + }, + }, + }; + } + + static get modifiers() { + return { + filterDomainId(query, formEmbedDomainId) { + if (formEmbedDomainId) { + query.where('formEmbedDomainId', formEmbedDomainId); + } + }, + filterPreviousStatus(query, status) { + if (status) { + query.where('previousStatus', status); + } + }, + filterNewStatus(query, status) { + if (status) { + query.where('newStatus', status); + } + }, + orderByCreatedAt(query, direction = 'desc') { + query.orderBy('createdAt', direction); + }, + orderDefault(builder) { + builder.orderBy('createdAt', 'desc'); + }, + }; + } +} + +module.exports = FormEmbedDomainHistory; diff --git a/app/src/forms/common/models/tables/formEmbedDomainStatusCode.js b/app/src/forms/common/models/tables/formEmbedDomainStatusCode.js new file mode 100644 index 000000000..26ba01f5c --- /dev/null +++ b/app/src/forms/common/models/tables/formEmbedDomainStatusCode.js @@ -0,0 +1,40 @@ +const { Model } = require('objection'); + +class FormEmbedDomainStatusCode extends Model { + static get tableName() { + return 'form_embed_domain_status_code'; + } + + static get idColumn() { + return 'code'; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['code', 'display'], + properties: { + code: { type: 'string', minLength: 1, maxLength: 50 }, + display: { type: 'string', minLength: 1, maxLength: 255 }, + description: { type: ['string', 'null'], maxLength: 1000 }, + }, + }; + } + + static get relationMappings() { + const FormEmbedDomain = require('./formEmbedDomain'); + + return { + domains: { + relation: Model.HasManyRelation, + modelClass: FormEmbedDomain, + join: { + from: 'form_embed_domain_status_code.code', + to: 'form_embed_domain.status', + }, + }, + }; + } +} + +module.exports = FormEmbedDomainStatusCode; diff --git a/app/src/forms/common/models/views/formEmbedDomainHistoryVw.js b/app/src/forms/common/models/views/formEmbedDomainHistoryVw.js new file mode 100644 index 000000000..34a138402 --- /dev/null +++ b/app/src/forms/common/models/views/formEmbedDomainHistoryVw.js @@ -0,0 +1,73 @@ +const { Model } = require('objection'); +const { Regex } = require('../../constants'); + +class FormEmbedDomainHistoryVw extends Model { + static get tableName() { + return 'form_embed_domain_history_vw'; + } + + static get idColumn() { + return 'historyId'; + } + + static get jsonSchema() { + return { + type: 'object', + properties: { + formEmbedDomainId: { type: 'string', pattern: Regex.UUID }, + ministry: { type: 'string' }, + formName: { type: 'string' }, + domain: { type: 'string' }, + historyId: { type: 'string', pattern: Regex.UUID }, + previousStatus: { type: ['string', 'null'] }, + newStatus: { type: 'string' }, + reason: { type: ['string', 'null'] }, + statusChangedAt: { type: ['string', 'null'] }, + statusChangedBy: { type: ['string', 'null'] }, + }, + }; + } + + static get modifiers() { + return { + formEmbedDomainId(query, formEmbedDomainId) { + if (formEmbedDomainId) { + query.where('formEmbedDomainId', formEmbedDomainId); + } + }, + filterMinistry(query, ministry) { + if (ministry) { + query.where('ministry', ministry); + } + }, + filterFormName(query, formName) { + if (formName) { + query.where('formName', 'ilike', `%${formName}%`); + } + }, + filterDomain(query, domain) { + if (domain) { + query.where('domain', 'ilike', `%${domain}%`); + } + }, + filterPreviousStatus(query, status) { + if (status) { + query.where('previousStatus', status); + } + }, + filterNewStatus(query, status) { + if (status) { + query.where('newStatus', status); + } + }, + orderByStatusChangedAt(query, direction = 'desc') { + query.orderBy('statusChangedAt', direction); + }, + orderDefault(builder) { + builder.orderBy('statusChangedAt', 'desc'); + }, + }; + } +} + +module.exports = FormEmbedDomainHistoryVw; diff --git a/app/src/forms/common/models/views/formEmbedDomainVw.js b/app/src/forms/common/models/views/formEmbedDomainVw.js new file mode 100644 index 000000000..e50096355 --- /dev/null +++ b/app/src/forms/common/models/views/formEmbedDomainVw.js @@ -0,0 +1,69 @@ +const { Model } = require('objection'); +const { Regex } = require('../../constants'); + +class FormEmbedDomainVw extends Model { + static get tableName() { + return 'form_embed_domain_vw'; + } + + static get idColumn() { + return 'id'; + } + + static get jsonSchema() { + return { + type: 'object', + properties: { + id: { type: 'string', pattern: Regex.UUID }, + ministry: { type: 'string' }, + formName: { type: 'string' }, + domain: { type: 'string' }, + status: { type: 'string' }, + requestedAt: { type: ['string', 'null'] }, + requestedBy: { type: ['string', 'null'] }, + }, + }; + } + + static get modifiers() { + return { + filterMinistry(query, ministry) { + if (ministry) { + query.where('ministry', ministry); + } + }, + filterFormName(query, formName) { + if (formName) { + query.where('formName', 'ilike', `%${formName}%`); + } + }, + filterDomain(query, domain) { + if (domain) { + query.where('domain', 'ilike', `%${domain}%`); + } + }, + filterStatus(query, status) { + if (status) { + query.where('status', status); + } + }, + orderByMinistry(query, direction = 'asc') { + query.orderBy('ministry', direction); + }, + orderByFormName(query, direction = 'asc') { + query.orderBy('formName', direction); + }, + orderByDomain(query, direction = 'asc') { + query.orderBy('domain', direction); + }, + orderByStatus(query, direction = 'asc') { + query.orderBy('status', direction); + }, + orderByRequestedAt(query, direction = 'desc') { + query.orderBy('requestedAt', direction); + }, + }; + } +} + +module.exports = FormEmbedDomainVw; diff --git a/app/src/forms/embed/controller.js b/app/src/forms/embed/controller.js new file mode 100644 index 000000000..8facc7fa0 --- /dev/null +++ b/app/src/forms/embed/controller.js @@ -0,0 +1,48 @@ +const service = require('./service'); + +module.exports = { + getFormEmbedDomainStatusCodes: async (req, res, next) => { + try { + const response = await service.getFormEmbedDomainStatusCodes(); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + listDomains: async (req, res, next) => { + try { + const response = await service.listDomains(req.params.formId); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + getDomainHistory: async (req, res, next) => { + try { + const response = await service.getDomainHistory(req.params.formEmbedDomainId); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + + requestDomain: async (req, res, next) => { + try { + const response = await service.requestDomain(req.params.formId, req.body, req.currentUser); + res.status(201).json(response); + } catch (error) { + next(error); + } + }, + + removeDomain: async (req, res, next) => { + try { + await service.removeDomain(req.params.formEmbedDomainId); + res.status(204).send(); + } catch (error) { + next(error); + } + }, +}; diff --git a/app/src/forms/embed/routes.js b/app/src/forms/embed/routes.js new file mode 100644 index 000000000..8c6c99226 --- /dev/null +++ b/app/src/forms/embed/routes.js @@ -0,0 +1,37 @@ +const routes = require('express').Router(); +const controller = require('./controller'); +const { currentUser, hasFormPermissions } = require('../auth/middleware/userAccess'); +const P = require('../common/constants').Permissions; +const validateParameter = require('../common/middleware/validateParameter'); + +routes.use(currentUser); + +routes.param('formId', validateParameter.validateFormId); +routes.param('formEmbedDomainId', validateParameter.validateFormEmbedDomainId); + +// List domains for a form +routes.get('/:formId/embed/statusCodes', hasFormPermissions([P.FORM_READ]), async (req, res, next) => { + await controller.getFormEmbedDomainStatusCodes(req, res, next); +}); + +// List domains for a form +routes.get('/:formId/embed', hasFormPermissions([P.FORM_READ]), async (req, res, next) => { + await controller.listDomains(req, res, next); +}); + +// Request a domain to be added to allowed domains +routes.post('/:formId/embed', hasFormPermissions([P.FORM_UPDATE]), async (req, res, next) => { + await controller.requestDomain(req, res, next); +}); + +// Remove a domain from allowed list +routes.delete('/:formId/embed/:formEmbedDomainId', hasFormPermissions([P.FORM_UPDATE]), async (req, res, next) => { + await controller.removeDomain(req, res, next); +}); + +// List status history for a domain +routes.get('/:formId/embed/:formEmbedDomainId/history', hasFormPermissions([P.FORM_READ]), async (req, res, next) => { + await controller.getDomainHistory(req, res, next); +}); + +module.exports = routes; diff --git a/app/src/forms/embed/service.js b/app/src/forms/embed/service.js new file mode 100644 index 000000000..43178de18 --- /dev/null +++ b/app/src/forms/embed/service.js @@ -0,0 +1,157 @@ +const Problem = require('api-problem'); +const uuid = require('uuid'); +const { FormEmbedDomain, FormEmbedDomainStatusCode, FormEmbedDomainHistory, FormEmbedDomainHistoryVw } = require('../common/models'); +const { FormEmbedDomainStatuses } = require('../common/constants'); +const log = require('../../components/log'); + +const service = { + /** + * @function getFormEmbedDomainStatusCodes + * Gets all form embed domain status codes + * @returns {Promise} An array of requested domains + */ + getFormEmbedDomainStatusCodes: async () => { + return FormEmbedDomainStatusCode.query(); + }, + + /** + * @function listDomains + * Gets all domains for a form + * @param {string} formId The form uuid + * @param {Object} params The query params + * @returns {Promise} An array of requested domains + */ + listDomains: async (formId, params = {}) => { + return FormEmbedDomain.query().modify('filterFormId', formId).modify('filterStatus', params.status); + }, + + /** + * @function getDomainHistory + * Gets the history for a domain + * @param {string} formEmbedDomainId The form embed domain uuid + * @returns {Promise} The domain history + */ + getDomainHistory: async (formEmbedDomainId) => { + return FormEmbedDomainHistoryVw.query().modify('formEmbedDomainId', formEmbedDomainId).modify('orderDefault'); + }, + + /** + * @function requestDomain + * Requests a domain to be added to allowed domains + * @param {string} formId The form uuid + * @param {Object} data The request body + * @param {Object} currentUser The current user + * @returns {Promise} The created requested domain + */ + requestDomain: async (formId, data, currentUser) => { + // Check if domain already exists for this form + const existing = await FormEmbedDomain.query().modify('filterFormId', formId).where('domain', data.domain).first(); + + if (existing) { + // If it exists but was previously denied, we can resubmit + if (existing.status === FormEmbedDomainStatuses.DENIED) { + let trx; + try { + trx = await FormEmbedDomain.startTransaction(); + + // Add history record for the status change + await FormEmbedDomainHistory.query(trx).insert({ + id: uuid.v4(), + formEmbedDomainId: existing.id, + previousStatus: existing.status, + newStatus: FormEmbedDomainStatuses.PENDING, + createdBy: currentUser.usernameIdp, + createdAt: new Date().toISOString(), + }); + + // Update status to pending + const updated = await FormEmbedDomain.query(trx).patchAndFetchById(existing.id, { + status: FormEmbedDomainStatuses.PENDING, + }); + + await trx.commit(); + return updated; + } catch (err) { + if (trx) await trx.rollback(); + throw err; + } + } else { + // If it's pending or approved, don't allow a new request + throw new Problem(409, `Domain request already exists with status: ${existing.status}`); + } + } + + // Create a new domain request + return await FormEmbedDomain.query().insert({ + id: uuid.v4(), + formId: formId, + domain: data.domain, + status: FormEmbedDomainStatuses.SUBMITTED, + requestedAt: new Date().toISOString(), + requestedBy: currentUser.usernameIdp, + }); + }, + + /** + * @function removeDomain + * Permanently removes a domain + * @param {string} formEmbedDomainId The form embed domain uuid + * @returns {Promise} The number of deleted domains + */ + removeDomain: async (formEmbedDomainId) => { + // First delete history records + await FormEmbedDomainHistory.query().where('formEmbedDomainId', formEmbedDomainId).delete(); + + // Then delete the domain + return FormEmbedDomain.query().deleteById(formEmbedDomainId); + }, + + /** + * @function isEmbedAllowed + * Checks if embedding is allowed for a form from a specific origin + * @param {string} formId The form uuid + * @param {string} origin The origin to check + * @returns {Promise} True if embedding is allowed + * @throws {Error} If there's an error parsing the URL or querying the database + */ + isEmbedAllowed: async (formId, origin) => { + if (!origin) { + log.debug(`Embedding denied: No origin provided for form ${formId}`); + return false; + } + + let domain; + try { + // Parse the origin to extract domain + const url = new URL(origin); + domain = url.hostname; + } catch (error) { + log.error(`Invalid origin URL format for form ${formId}: ${origin}`, error); + const enhancedError = new Error(`Invalid origin URL format: ${origin}`); + enhancedError.originalError = error; + throw enhancedError; + } + + try { + // Check if the domain is in the approved list + const allowed = await FormEmbedDomain.query() + .modify('filterFormId', formId) + .modify('filterStatus', FormEmbedDomainStatuses.APPROVED) + .whereRaw("? LIKE '%' || domain || '%'", [domain]) + .first(); + + const isAllowed = !!allowed; + if (!isAllowed) { + log.debug(`Embedding denied: Domain ${domain} not approved for form ${formId}`); + } + return isAllowed; + } catch (error) { + log.error(`Database error checking allowed domains for form ${formId} and domain ${domain}`, error); + const enhancedError = new Error(`Database error checking allowed domains for form ${formId}`); + enhancedError.originalError = error; + throw enhancedError; + } + }, +}; + +module.exports = service; diff --git a/app/src/forms/form/index.js b/app/src/forms/form/index.js index e18f1c620..b4abf72cc 100644 --- a/app/src/forms/form/index.js +++ b/app/src/forms/form/index.js @@ -4,8 +4,8 @@ const setupMount = require('../common/utils').setupMount; const encryptionKeyRoutes = require('./encryptionKey/routes'); const eventStreamConfigRoutes = require('./eventStreamConfig/routes'); const externalApiRoutes = require('./externalApi/routes'); - +const embedRoutes = require('../embed/routes'); module.exports.mount = (app) => { - const p = setupMount('forms', app, [routes, encryptionKeyRoutes, eventStreamConfigRoutes, externalApiRoutes]); + const p = setupMount('forms', app, [routes, encryptionKeyRoutes, eventStreamConfigRoutes, externalApiRoutes, embedRoutes]); return p; }; diff --git a/app/src/forms/form/routes.js b/app/src/forms/form/routes.js index 10c3572bc..6fb2a39cc 100644 --- a/app/src/forms/form/routes.js +++ b/app/src/forms/form/routes.js @@ -5,6 +5,7 @@ const apiAccess = require('../auth/middleware/apiAccess'); const { currentUser, hasFormPermissions } = require('../auth/middleware/userAccess'); const P = require('../common/constants').Permissions; const validateParameter = require('../common/middleware/validateParameter'); +const embedSecurity = require('../common/middleware/embed'); const controller = require('./controller'); routes.use(currentUser); @@ -22,7 +23,7 @@ routes.post('/', async (req, res, next) => { await controller.createForm(req, res, next); }); -routes.get('/:formId', apiAccess, hasFormPermissions([P.FORM_READ]), async (req, res, next) => { +routes.get('/:formId', apiAccess, embedSecurity, hasFormPermissions([P.FORM_READ]), async (req, res, next) => { await controller.readForm(req, res, next); }); @@ -58,11 +59,11 @@ routes.put('/:formId/emailTemplate', hasFormPermissions([P.EMAIL_TEMPLATE_READ, await controller.createOrUpdateEmailTemplate(req, res, next); }); -routes.get('/:formId/options', async (req, res, next) => { +routes.get('/:formId/options', embedSecurity, async (req, res, next) => { await controller.readFormOptions(req, res, next); }); -routes.get('/:formId/version', apiAccess, hasFormPermissions([P.FORM_READ]), async (req, res, next) => { +routes.get('/:formId/version', apiAccess, embedSecurity, hasFormPermissions([P.FORM_READ]), async (req, res, next) => { await controller.readPublishedForm(req, res, next); });