Skip to content

Commit

Permalink
#434: Show specific error message in FE via Toast pop up.
Browse files Browse the repository at this point in the history
* If you want to start a study with e.g. an empty lime survey id, a Toast pop up will be shown, that the ID is missing.
* Add global Toast component to show erroneous API response.
* Add html required attribute if property is required (not only a * symbol next to the label).
* Add trim to v-model for String properties, to trim whitespaced on users input.
* Add error translations.
* Replace alter messages with Toast pop ups.
  • Loading branch information
benitsch committed Jul 22, 2024
1 parent f56bc4e commit a78c88d
Show file tree
Hide file tree
Showing 16 changed files with 192 additions and 43 deletions.
6 changes: 6 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ Prevention -- A research institute of the Ludwig Boltzmann Gesellschaft,
Oesterreichische Vereinigung zur Foerderung der wissenschaftlichen Forschung).
Licensed under the Elastic License 2.0. */
<script setup lang="ts">
import { ref, provide } from 'vue';
import Toast from 'primevue/toast';
import Footer from './components/shared/Footer.vue';
import Header from './components/shared/Header.vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const toast = ref<Toast | null>(null);
provide('toast', toast);
router.beforeEach((to) => {
document.title = to.meta?.title ? to.meta.title + ' (More)' : 'More';
Expand All @@ -18,6 +23,7 @@ Licensed under the Elastic License 2.0. */
<Header />
<main class="mb-20 pt-14">
<router-view />
<Toast ref="toast" />
</main>
<Footer />
</template>
Expand Down
1 change: 0 additions & 1 deletion src/components/TimelineList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,6 @@ Licensed under the Elastic License 2.0. */
setupObservationAndInterventionFilterOptions(response.data);
})
.catch((e: AxiosError) => {
console.log(e);
handleIndividualError(e, 'cannot list studyTimeline');
});
}
Expand Down
21 changes: 10 additions & 11 deletions src/components/dialog/ObservationDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ Licensed under the Elastic License 2.0. */
import AbsoluteScheduler from '../shared/Scheduler.vue';
import { isObjectEmpty } from '../../utils/commonUtils';
import { ScheduleType } from '../../models/Scheduler';
import { AxiosError } from 'axios';
import { useErrorHandling } from '../../composable/useErrorHandling';
import { useToastService } from '../../composable/toastService';
const { handleToastErrors, showErrorToast } = useToastService();
const dialog = useDialog();
const { componentsApi } = useComponentsApi();
const { handleIndividualError } = useErrorHandling();
const studyStore = useStudyStore();
const { t } = useI18n();
Expand Down Expand Up @@ -65,8 +70,6 @@ Licensed under the Elastic License 2.0. */
const studyGroupId = ref(observation.studyGroupId);
const jsonError = ref();
function getLabelForChoiceValue(
value: any,
values: MoreTableChoice[],
Expand Down Expand Up @@ -124,17 +127,14 @@ Licensed under the Elastic License 2.0. */
if (report.valid) {
save(parsedProps);
} else {
jsonError.value = (report.errors || [])
.concat(report.warnings || [])
.map((e) => e.message)
.join(', ');
handleToastErrors(report);
}
})
.catch((e: AxiosError) => {
handleIndividualError(e, 'cannot fetch study');
});
} catch (e: any) {
jsonError.value = t('observation.error.noValidField', {
key: e.key,
message: e.message,
});
showErrorToast(t('global.error.general'));
}
}
Expand Down Expand Up @@ -276,7 +276,6 @@ Licensed under the Elastic License 2.0. */
</div>
<div v-if="properties.length" class="col-start-0 col-span-8">
<h5 class="mb-2">{{ $t('global.labels.config') }}</h5>
<div v-if="jsonError" class="error mb-3">{{ jsonError }}</div>
<div class="col-start-0 col-span-8">
<div v-if="properties">
<PropertyInputs
Expand Down
7 changes: 4 additions & 3 deletions src/components/dialog/shared/BooleanPropertyInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ Licensed under the Elastic License 2.0. */
<template>
<div class="flex flex-col gap-1">
<h5 class="font-bold">
<label v-if="property.name" :for="property.id"
>{{ $t(property.name) }}<span v-if="property.required">*</span></label
>
<label v-if="property.name" :for="property.id">
{{ $t(property.name) }}<span v-if="property.required">*</span>
</label>
</h5>
<div v-if="props.property.description" :id="`${property.id}-help`">
{{ $t(props.property.description) }}
Expand All @@ -44,6 +44,7 @@ Licensed under the Elastic License 2.0. */
v-model="booleanChecked"
:label="property.name"
class="mr-2"
:required="property.required"
:binary="true"
@change="emit('onBooleanChange', booleanChecked)"
/>
Expand Down
7 changes: 4 additions & 3 deletions src/components/dialog/shared/IntegerPropertyInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ Licensed under the Elastic License 2.0. */
<template>
<div class="gap-1">
<h5 class="font-bold">
<label :for="property.id"
>{{ $t(property.name) }}<span v-if="property.required">*</span></label
>
<label :for="property.id">
{{ $t(property.name) }}<span v-if="property.required">*</span>
</label>
</h5>

<div v-if="props.property.description" :id="`${property.id}-help`">
Expand All @@ -44,6 +44,7 @@ Licensed under the Elastic License 2.0. */
:id="property.id"
v-model="property.value"
type="number"
:required="property.required"
:max="property.max"
:min="property.min"
:disabled="!editable"
Expand Down
7 changes: 4 additions & 3 deletions src/components/dialog/shared/ObservationPropertyInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,17 @@ Licensed under the Elastic License 2.0. */
<template>
<div class="flex flex-col gap-1">
<h6 class="font-bold">
<label v-if="property.name" :for="property.id"
>{{ $t(property.name) }}<span v-if="property.required">*</span></label
>
<label v-if="property.name" :for="property.id">
{{ $t(property.name) }}<span v-if="property.required">*</span>
</label>
</h6>
<Dropdown
:id="property.id"
v-model="observation"
:options="observationList"
option-label="title"
class="w-full"
:required="property.required"
:aria-describedby="`${property.id}-help`"
:disabled="!editable"
:placeholder="$t(props.property.description)"
Expand Down
8 changes: 4 additions & 4 deletions src/components/dialog/shared/StringListPropertyInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ Licensed under the Elastic License 2.0. */
<template>
<div class="flex flex-col gap-1">
<h6 class="font-bold">
<label
>{{ $t(property.name) }}<span v-if="property.required">*</span
><span v-if="property.required">*</span></label
>
<label>
{{ $t(property.name) }}<span v-if="property.required">*</span>
</label>
</h6>
<div>{{ $t(props.property.description) }}</div>
<div v-if="editable" class="flex w-full flex-col gap-1">
Expand All @@ -54,6 +53,7 @@ Licensed under the Elastic License 2.0. */
:class="!editable && property.value?.[index - 1] ? 'w-fit' : 'hidden'"
:value="property.value?.[index - 1]"
type="text"
:required="property.required"
:disabled="!editable"
:placeholder="t('global.labels.option', { value: index })"
style="display: block"
Expand Down
9 changes: 5 additions & 4 deletions src/components/dialog/shared/StringPropertyInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,20 @@ Licensed under the Elastic License 2.0. */
<template>
<div class="flex flex-col gap-1">
<h6 class="font-bold">
<label v-if="property.name" :for="property.id"
>{{ $t(property.name) }}<span v-if="property.required">*</span></label
>
<label v-if="property.name" :for="property.id">
{{ $t(property.name) }}<span v-if="property.required">*</span>
</label>
</h6>
<div v-if="props.property.description" :id="`${property.id}-help`">
{{ $t(props.property.description) }}
</div>

<InputText
:id="property.id"
v-model="property.value"
v-model.trim="property.value"
type="text"
class="w-full"
:required="property.required"
:aria-describedby="`${property.id}-help`"
:disabled="!editable"
:placeholder="
Expand Down
7 changes: 4 additions & 3 deletions src/components/dialog/shared/StringTextPropertyInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ Licensed under the Elastic License 2.0. */
<template>
<div class="flex flex-col gap-1">
<h6 class="font-bold">
<label v-if="property.name" :for="property.id"
>{{ $t(property.name) }}<span v-if="property.required">*</span></label
>
<label v-if="property.name" :for="property.id">
{{ $t(property.name) }}<span v-if="property.required">*</span>
</label>
</h6>
<div v-if="props.property.description" :id="`${property.id}-help`">
{{ $t(props.property.description) }}
Expand All @@ -44,6 +44,7 @@ Licensed under the Elastic License 2.0. */
v-model="property.value"
type="text"
class="w-full"
:required="property.required"
:aria-describedby="`${property.id}-help`"
:disabled="!editable"
:placeholder="
Expand Down
114 changes: 114 additions & 0 deletions src/composable/toastService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
Copyright LBI-DHP and/or licensed to LBI-DHP under one or more
contributor license agreements (LBI-DHP: Ludwig Boltzmann Institute
for Digital Health and Prevention -- A research institute of the
Ludwig Boltzmann Gesellschaft, Oesterreichische Vereinigung zur
Foerderung der wissenschaftlichen Forschung).
Licensed under the Elastic License 2.0.
*/
import { inject, Ref } from 'vue';
import { ToastServiceMethods } from 'primevue/toastservice';
import { ToastMessageOptions } from 'primevue/toast';
import { useI18n } from 'vue-i18n';
import {
ValidationReport,
ValidationReportItem,
} from '../generated-sources/openapi';

/**
* This composable is responsible for showing errors from Backend to the Client with a Toast popup.
*/
export function useToastService(): any {
const toast: Ref<ToastServiceMethods> | undefined =
inject<Ref<ToastServiceMethods>>('toast');
const { t } = useI18n();

if (!toast) {
throw new Error('ToastService is not provided');
}

const handleToastErrors = (err: any): void => {
processConfigurationException(err);
processOtherExceptions(err);
};

function processConfigurationException(report: ValidationReport): void {
const errors: string[] = [];
const warnings: string[] = [];

report.errors?.forEach((error: ValidationReportItem) => {
const errorMessage = error.message
? t(error.message, { value: error.propertyId })
: null;
if (errorMessage) {
errors.push(errorMessage);
}
});

report.warnings?.forEach((warning: ValidationReportItem) => {
const warningMessage = warning.message
? t(warning.message, { value: warning.propertyId })
: null;
if (warningMessage) {
warnings.push(warningMessage);
}
});

if (errors.length) {
showErrorToast(errors.join('\n'));
}

if (warnings.length) {
showWarningToast(warnings.join('\n'));
}
}

function processOtherExceptions(error: any): void {
if (error.message) {
showErrorToast(error.message);
}
}

const showToast = (
severity: ToastMessageOptions['severity'],
summary: string,
detail: string,
): void => {
if (toast.value) {
toast.value.add({ severity, summary, detail });
} else {
console.error('Toast reference is not set');
}
};

const showErrorToast = (detail: string): void => {
if (toast.value) {
const severity: ToastMessageOptions['severity'] = 'error';
const summary = t('global.error.type.error');
const life: ToastMessageOptions['life'] = 5000;

toast.value.add({ severity, summary, detail, life });
} else {
console.error('Toast reference is not set');
}
};

const showWarningToast = (detail: string): void => {
if (toast.value) {
const severity: ToastMessageOptions['severity'] = 'warn';
const summary = t('global.error.type.warning');
const life: ToastMessageOptions['life'] = 5000;

toast.value.add({ severity, summary, detail, life });
} else {
console.error('Toast reference is not set');
}
};

return {
handleToastErrors,
showToast,
showErrorToast,
showWarningToast,
};
}
10 changes: 10 additions & 0 deletions src/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@
},
"footer": {
"aboutMore": "Über More"
},
"error": {
"type": {
"error": "Fehler",
"warning": "Warnung",
"info": "Info"
},
"general": "Ein allgemeiner Fehler ist aufgetreten.",
"required": "Erforderlicher Wert '{value}' fehlt.",
"immutable": "Der Wert von '{value}' darf nicht geändert werden."
}
},

Expand Down
10 changes: 10 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@
},
"footer": {
"aboutMore": "About More"
},
"error": {
"type": {
"error": "Error",
"warning": "Warning",
"info": "Info"
},
"general": "A General error occurred.",
"required": "Required value '{value}' missing.",
"immutable": "The value '{value}' is immutable and cannot be changed."
}
},

Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import PrimeVue from 'primevue/config';
import Tooltip from 'primevue/tooltip';
import ConfirmationService from 'primevue/confirmationservice';
import DialogService from 'primevue/dialogservice';
import ToastService from 'primevue/toastservice';
// Router
import { Router } from './router';
import AuthService from './service/AuthService';
Expand Down Expand Up @@ -106,6 +107,7 @@ app.use(Router);
app.use(PrimeVue);
app.use(ConfirmationService);
app.use(DialogService);
app.use(ToastService);
app.use(pinia);

app.mount('#app');
Expand Down
Loading

0 comments on commit a78c88d

Please sign in to comment.