Repository: MORE-Platform/more-studymanager-frontend
Pull request #583: #529 + 267: Show error when study endpoint fails + use locale to external "ABOUT MORE" link
Author: login: ManuelTS
Created: 2025-04-10T09:55:59Z | Merged: 2025-04-14T07:45:41Z

Fixes #267 + #529 

Linked issue #267: create study - input validation
no validation on email and phone number fields (Jan)
Linked issue comments: user: login: pavinod
created_at: 2023-08-28T09:10:55Z
body: Email validation done. Invalid phone number can still be entered. 
author_association: MEMBER
user: login: ManuelTS
created_at: 2025-04-10T06:20:45Z
body: Added native error messages for e-mail and phone nr. validation failures
author_association: CONTRIBUTOR

Diff:
diff --git a/src/components/StudyList.vue b/src/components/StudyList.vue
index e51dd93a..02425757 100644
--- a/src/components/StudyList.vue
+++ b/src/components/StudyList.vue
@@ -28,12 +28,14 @@ Licensed under the Elastic License 2.0. */
   import FileUpload, { FileUploadUploaderEvent } from 'primevue/fileupload';
   import Button from 'primevue/button';
   import { DownloadData } from '../models/DataDownloadModel';
+  import { useToastService } from '../composable/toastService';
 
   const studyStore = useStudyStore();
   const router = useRouter();
   const dialog = useDialog();
   const loader = useLoader();
   const { t } = useI18n();
+  const { showErrorToast } = useToastService();
 
   const sortOptions: MoreTableSortOptions = {
     sortField: 'studyId',
@@ -264,12 +266,19 @@ Licensed under the Elastic License 2.0. */
     studyStore.exportStudyData({ studyId } as DownloadData);
   }
 
-  function onImportStudy(event: FileUploadUploaderEvent): void {
+  const onImportStudy = (event: FileUploadUploaderEvent): void => {
     const file: File = Array.isArray(event.files)
       ? event.files[0]
       : event.files;
-    studyStore.importStudy(file);
-  }
+
+    studyStore.importStudy(file).catch((): void => {
+      showErrorToast(
+        t('study.studyList.error.studyImportFailed', {
+          filename: file?.name ? `"${file.name}"` : '',
+        }),
+      );
+    });
+  };
 
   studyStore.listStudies();
 </script>
diff --git a/src/components/shared/Footer.vue b/src/components/shared/Footer.vue
index a2ad1c90..efb4bb04 100644
--- a/src/components/shared/Footer.vue
+++ b/src/components/shared/Footer.vue
@@ -10,6 +10,7 @@ Licensed under the Elastic License 2.0. */
     FrontendConfiguration,
   } from '../../generated-sources/openapi';
   import OverlayPanel from 'primevue/overlaypanel';
+  import { useI18n } from 'vue-i18n';
 
   const uiConfig = inject('uiConfig') as FrontendConfiguration;
   const buildInfo = inject('buildInfo') as {
@@ -17,6 +18,7 @@ Licensed under the Elastic License 2.0. */
     backend: BuildInfo;
   };
   const buildInfoPanel = ref();
+  const { locale } = useI18n();
 </script>
 
 <template>
@@ -46,7 +48,7 @@ Licensed under the Elastic License 2.0. */
         {{ uiConfig.title }}
       </div>
       <a
-        href="https://dhp.lbg.ac.at/more/"
+        :href="`https://dhp.lbg.ac.at/more/${locale === 'en' ? '?lang=en' : ''}`"
         target="_blank"
         class="link text-base uppercase"
         >{{ $t('global.footer.aboutMore') }}</a
diff --git a/src/i18n/de.json b/src/i18n/de.json
index 47ada02e..0ccff91e 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -262,6 +262,9 @@
         "addStudy": "Neue Studie hinzufügen",
         "importStudy": "Studie importieren",
         "copyUrl": "Studie teilen"
+      },
+      "error": {
+        "studyImportFailed": "Die Studie {filename} konnte nicht importiert werden"
       }
     }
   },
diff --git a/src/i18n/en.json b/src/i18n/en.json
index e6978d67..75af7f39 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -262,6 +262,9 @@
         "addStudy": "Add new study",
         "copyUrl": "Copy URL",
         "importStudy": "Import study"
+      },
+      "error": {
+        "studyImportFailed": "The study {filename} could not be imported"
       }
     }
   },
diff --git a/src/stores/studyStore.ts b/src/stores/studyStore.ts
index 7900f657..b47b885e 100644
--- a/src/stores/studyStore.ts
+++ b/src/stores/studyStore.ts
@@ -121,22 +121,14 @@ export const useStudyStore = defineStore('study', () => {
     }
   }
 
-  async function importStudy(importedStudy: File): Promise<void> {
-    await importExportApi
+  const importStudy = (importedStudy: File): Promise<void> =>
+    importExportApi
       .importStudy(importedStudy, {
         headers: {
           'Content-Type': 'multipart/form-data',
         },
       })
-      .then(() => {
-        setTimeout(function () {
-          listStudies();
-        }, 100);
-      })
-      .catch((e: AxiosError) =>
-        handleIndividualError(e, 'cannot import study'),
-      );
-  }
+      .then(listStudies);
 
   async function exportStudyConfig(studyId: number): Promise<void> {
     await importExportApi
diff --git a/src/utils/stringUtils.ts b/src/utils/stringUtils.ts
index 6da6dfd2..64a55c63 100644
--- a/src/utils/stringUtils.ts
+++ b/src/utils/stringUtils.ts
@@ -1,7 +1,9 @@
 // Source https://emailregex.com/
 export const validateEmail = (email: string | undefined | null): boolean =>
   !!email &&
-  /^(([^<>()[]\\.,;:\s@"]+(\.[^<>()[]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
+  // pipeline says there are unnecessary escapes, which is wrong, valid test e-mails would fail
+  // eslint-disable-next-line no-useless-escape
+  /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
     email,
   );
 
diff --git a/src/vue-i18n.d.ts b/src/vue-i18n.d.ts
new file mode 100644
index 00000000..daad822d
--- /dev/null
+++ b/src/vue-i18n.d.ts
@@ -0,0 +1,11 @@
+import { I18n } from 'vue-i18n';
+
+/** Tell TS + Vue that $t within components translates strings. */
+declare module '@vue/runtime-core' {
+  interface ComponentCustomProperties {
+    $t: typeof I18n.prototype.t; // Add $t to Vue's component properties
+    $d: (value: Date | number, key?: string, locale?: string) => string; // For date formatting
+  }
+}
+
+export {};


Reviews:
user: login: iaigner
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:21:41Z
user: login: ManuelTS
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T12:14:42Z
user: login: iaigner
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T07:44:42Z

Review comments:
diff_hunk: @@ -264,12 +266,19 @@ Licensed under the Elastic License 2.0. */
     studyStore.exportStudyData({ studyId } as DownloadData);
   }
 
-  function onImportStudy(event: FileUploadUploaderEvent): void {
+  const onImportStudy = (event: FileUploadUploaderEvent): void => {
     const file: File = Array.isArray(event.files)
       ? event.files[0]
       : event.files;
-    studyStore.importStudy(file);
-  }
+
+    studyStore.importStudy(file).catch((): void => {
+      showErrorToast(
+        t('study.studyList.error.studyImportFailed', {
+          filename: file?.name ? `"${file.name}"` : '',
path: src/components/StudyList.vue
user: login: iaigner
body: weils mir aufgefallen ist beim Testen - man kann die importierte Studie nur über die ID unterscheiden. Evtl. könnetn wir wenn du das schon angreifst hier auch noch beim file.name eine COPY hinten dranhängen, dass man es auch am Namen gleich sieht.
created_at: 2025-04-11T09:21:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -264,12 +266,19 @@ Licensed under the Elastic License 2.0. */
     studyStore.exportStudyData({ studyId } as DownloadData);
   }
 
-  function onImportStudy(event: FileUploadUploaderEvent): void {
+  const onImportStudy = (event: FileUploadUploaderEvent): void => {
     const file: File = Array.isArray(event.files)
       ? event.files[0]
       : event.files;
-    studyStore.importStudy(file);
-  }
+
+    studyStore.importStudy(file).catch((): void => {
+      showErrorToast(
+        t('study.studyList.error.studyImportFailed', {
+          filename: file?.name ? `"${file.name}"` : '',
path: src/components/StudyList.vue
user: login: ManuelTS
body: Done in https://github.com/MORE-Platform/more-studymanager-frontend/pull/583/commits/e823fdc56119c915a89450a5bd27865272ba0621, if ok please resolve this conversation + PR @iaigner 
created_at: 2025-04-11T12:14:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-10T10:39:48Z
body: ## Test Results
0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit e823fdc5. ± Comparison against base commit 5c1aa68a.

[test-results]:data:application/gzip;base64,H4sIAAkI+WcC/1WMyw7CIBBFf6WZtQsYhYI/YyiPZGJbDI9V47/b1EZhd8+5ydkg0Owz3Ad2GSBXKj9wNZlCcT1xP0pu9yNXa3vxpFcngqG5Ez6lmE6T6pqb2ea+/K8d3MQObls2LguVHcArvAZnheRcW82FUfommBGTw1FJgSNOhknk8P4Aemz0SPsAAAA=

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: markus-ebner
created_at: 2025-04-11T07:09:40Z
body: code lgtm :) functionality should be tested by @iaigner. Can you do that? Thanks :) 
author_association: CONTRIBUTOR