Skip to content

Commit 60bb5f2

Browse files
authored
BC-9142 change order of rooms (#3961)
- User can change order of rooms via drag and drop - User can change order of rooms via arrow keys - introduced lib "vue-router-mock" to simplify router test suite injections - introduced room.store.ts, putting together all composables and rest api endpoints in one single source of truth regarding room rest calls - unified error translation keys for entities and common actions like duplicate, copy, create ... - error notifications added to async-util functions, allowing to further reduce code written to do axios rest calls - introduced explicit axios async util, covering 4xx http codes to further enrich notification error messages - introduced pluralisation for the newly added error notifications of a given entity type - AlertContainer fix checking for a translation key before using removing a warning in console - Aligned layout of rooms with layout of boards in terms of appearance and usage - see screenshot
1 parent 78a5e3c commit 60bb5f2

40 files changed

+927
-828
lines changed

package-lock.json

Lines changed: 14 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"vite-plugin-vuetify": "^2.1.2",
9393
"vite-tsconfig-paths": "^5.1.4",
9494
"vitest": "^3.2.4",
95+
"vue-router-mock": "^2.0.0",
9596
"vue-tsc": "^3.1.2",
9697
"vuex-module-decorators": "^1.2.0"
9798
},

src/components/molecules/AlertContainer.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
@click:close="removeNotifier(notification.id)"
2020
>
2121
<div class="alert-text mr-2" data-testId="alert-text">
22-
{{ t(notification.text) ?? notification.text }}
22+
{{ getNotificationText(notification.text) }}
2323
</div>
2424
</v-alert>
2525
</transition-group>
2626
</div>
2727
</template>
2828

2929
<script setup lang="ts">
30+
import { i18nKeyExists } from "@/plugins/i18n";
3031
import { AlertStatus, useNotificationStore } from "@data-app";
3132
import { mdiAlert, mdiAlertCircle, mdiCheckCircle, mdiInformation } from "@icons/material";
3233
import { storeToRefs } from "pinia";
@@ -49,6 +50,8 @@ const statusIcons: { [status in AlertStatus]: string } = {
4950
error: mdiAlertCircle,
5051
info: mdiInformation,
5152
};
53+
54+
const getNotificationText = (text: string) => (i18nKeyExists(text) ? t(text) : text);
5255
</script>
5356

5457
<style lang="scss" scoped>

src/components/templates/DefaultWireframe.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
:fluid="maxWidth !== 'nativ'"
5151
class="main-content"
5252
:class="{
53+
'main-pb-96': mainWithBottomPadding,
5354
'pa-0': mainWithoutPadding,
5455
'container-short-width': maxWidth === 'short',
5556
'container-full-width': maxWidth === 'full',
@@ -98,6 +99,10 @@ const props = defineProps({
9899
mainWithoutPadding: {
99100
type: Boolean,
100101
},
102+
mainWithBottomPadding: {
103+
type: Boolean,
104+
default: false,
105+
},
101106
// neded if we don't want to have full page scrolling, so it's restricted to browsers viewport height
102107
isFlexContainer: {
103108
type: Boolean,
@@ -137,6 +142,10 @@ const showDivider = computed(() => !props.hideBorder && !!(props.headline || slo
137142
overflow-y: auto;
138143
}
139144
145+
.main-pb-96 {
146+
padding-bottom: 96px !important;
147+
}
148+
140149
.wireframe-container h1:first-of-type {
141150
margin-bottom: 16px;
142151
}

src/composables/async-tasks.composable.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1+
import { i18nKeyExists, useI18nGlobal } from "@/plugins/i18n";
12
import { Status } from "@/store/types/commons";
23
import { AsyncFunction } from "@/types/async.types";
4+
import { mapAxiosErrorToResponseError } from "@/utils/api";
35
import { useTryCatch } from "@/utils/try-catch.utils";
6+
import { notifyError } from "@data-app";
7+
import { logger } from "@util-logger";
48
import { computed, readonly, ref } from "vue";
59

6-
type TaskResult<T> = { success: true; result: T } | { success: false; result: undefined };
10+
type TaskResult<T> =
11+
| { success: true; result: T; error?: undefined }
12+
| { success: false; result?: undefined; error: Error };
713

814
export const useSafeTask = () => {
915
const error = ref<Error>();
1016
const status = ref<Status>("");
1117
const isRunning = computed(() => status.value === "pending");
1218

13-
const execute = async <T>(fn: AsyncFunction<T>): Promise<TaskResult<T>> => {
19+
const execute = async <T>(fn: AsyncFunction<T>, onErrorNotifyMessage?: string): Promise<TaskResult<T>> => {
1420
error.value = undefined;
1521
status.value = "pending";
1622

@@ -19,12 +25,17 @@ export const useSafeTask = () => {
1925
if (err) {
2026
error.value = err;
2127
status.value = "error";
22-
return { success: false, result: undefined };
28+
logger.error(err);
29+
30+
if (onErrorNotifyMessage) {
31+
notifyError(onErrorNotifyMessage);
32+
}
33+
return { success: false, result: undefined, error: error.value };
2334
}
2435

2536
status.value = "completed";
2637

27-
return { success: true, result };
38+
return { success: true, result, error: undefined };
2839
};
2940

3041
const reset = () => {
@@ -41,13 +52,45 @@ export const useSafeTask = () => {
4152
};
4253
};
4354

44-
export const useSafeTaskRunner = <T>(fn: AsyncFunction<T>) => {
55+
export const useSafeAxiosTask = () => {
56+
const { execute: safeExec, isRunning, reset, status, error } = useSafeTask();
57+
const { t } = useI18nGlobal();
58+
59+
const execute = async <T>(fn: AsyncFunction<T>, onErrorNotifyMessage?: string): Promise<TaskResult<T>> => {
60+
const { result, success, error } = await safeExec<T>(fn);
61+
62+
if (error && onErrorNotifyMessage) {
63+
const apiError = mapAxiosErrorToResponseError(error);
64+
65+
if (apiError.code) {
66+
const statusKey = `error.${apiError.code}`;
67+
const errorKeyExists = i18nKeyExists(statusKey);
68+
69+
if (errorKeyExists) {
70+
notifyError(`${onErrorNotifyMessage} ${t(statusKey)}`);
71+
} else {
72+
notifyError(onErrorNotifyMessage);
73+
}
74+
}
75+
}
76+
77+
if (success) {
78+
return { result, error, success: true };
79+
} else {
80+
return { result: undefined, error, success: false };
81+
}
82+
};
83+
84+
return { execute, isRunning, reset, status, error };
85+
};
86+
87+
export const useSafeTaskRunner = <T>(fn: AsyncFunction<T>, onErrorNotifyMessage?: string) => {
4588
const { error, status, isRunning, execute, reset } = useSafeTask();
4689

4790
const data = ref<T>();
4891

4992
const run = async () => {
50-
const { result, success } = await execute(fn);
93+
const { result, success } = await execute(fn, onErrorNotifyMessage);
5194
data.value = result;
5295
return { result, success };
5396
};

src/composables/async-tasks.composable.unit.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { useSafeTask, useSafeTaskRunner } from "./async-tasks.composable";
1+
import { useSafeAxiosTask, useSafeTask, useSafeTaskRunner } from "./async-tasks.composable";
2+
import { useNotificationStore } from "@data-app";
3+
import { createTestingPinia } from "@pinia/testing";
4+
import { createAxiosError } from "@util-axios-error";
5+
import { setActivePinia } from "pinia";
26
import { beforeEach, describe, expect, it } from "vitest";
37

48
describe("useSafeTask", () => {
@@ -123,3 +127,53 @@ describe("useSafeSingleTask", () => {
123127
expect(task.status.value).toBe("completed");
124128
});
125129
});
130+
131+
describe("useSafeAxiosTask", () => {
132+
beforeEach(() => {
133+
setActivePinia(createTestingPinia());
134+
});
135+
136+
it("should not notify when task succeeds", async () => {
137+
const mockFn = vi.fn().mockResolvedValue("success");
138+
139+
const { execute } = useSafeAxiosTask();
140+
await execute(mockFn, "Error message");
141+
142+
expect(useNotificationStore().notify).not.toHaveBeenCalled();
143+
});
144+
145+
it("should not notify when task fails but no error message provided", async () => {
146+
const mockFn = vi.fn().mockRejectedValue(new Error("Test error"));
147+
148+
const { execute } = useSafeAxiosTask();
149+
await execute(mockFn);
150+
151+
expect(useNotificationStore().notify).not.toHaveBeenCalled();
152+
});
153+
154+
it("should notify with custom message when error occurs and message provided", async () => {
155+
const mockFn = vi.fn().mockRejectedValue(new Error("Test error"));
156+
157+
const { execute } = useSafeAxiosTask();
158+
await execute(mockFn, "Something went wrong");
159+
160+
expect(useNotificationStore().notify).toHaveBeenCalledWith(
161+
expect.objectContaining({ status: "error", text: "Something went wrong" })
162+
);
163+
});
164+
165+
it("should notify with translated error code when axios error has matching translation", async () => {
166+
const axiosError = createAxiosError({
167+
data: { message: "Validation failed", code: 400, title: "", type: "" },
168+
});
169+
170+
const mockFn = vi.fn().mockRejectedValue(axiosError);
171+
172+
const { execute } = useSafeAxiosTask();
173+
await execute(mockFn, "Request failed.");
174+
175+
expect(useNotificationStore().notify).toHaveBeenCalledWith(
176+
expect.objectContaining({ status: "error", text: "Request failed. Fehlerhafte Anfrage" })
177+
);
178+
});
179+
});

src/locales/de.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export default {
9696
"common.labels.repeat": "Wiederholung",
9797
"common.labels.restore": "Wiederherstellen",
9898
"common.labels.role": "Rolle",
99-
"common.labels.room": "Raum",
99+
"common.labels.room": "Raum | Räume",
100100
"common.labels.search": "Suche",
101101
"common.labels.settings": "Einstellungen",
102102
"common.labels.size": "Größe",
@@ -130,6 +130,17 @@ export default {
130130
"common.nodata": "Keine Daten vorhanden",
131131
"common.notification.error": "Es ist ein Fehler aufgetreten.",
132132
"common.notification.connection.restored": "Die Verbindung wurde wieder hergestellt.",
133+
"common.notifications.errors.notCreated":
134+
"{type} konnte nicht erstellt werden. | {type} konnten nicht erstellt werden.",
135+
"common.notifications.errors.notDeleted":
136+
"{type} konnte nicht gelöscht werden. | {type} konnten nicht gelöscht werden.",
137+
"common.notifications.errors.notDuplicated":
138+
"{type} konnte nicht dupliziert werden. | {type} konnten nicht dupliziert werden.",
139+
"common.notifications.errors.notLoaded": "{type} konnte nicht geladen werden. | {type} konnten nicht geladen werden.",
140+
"common.notifications.errors.notMoved":
141+
"{type} konnte nicht verschoben werden. | {type} konnten nicht verschoben werden.",
142+
"common.notifications.errors.notExited":
143+
"{type} konnte nicht verlassen werden. | {type} konnten nicht verlassen werden.",
133144
"common.placeholder.birthdate": "20.2.2002",
134145
"common.placeholder.dateformat": "TT.MM.JJJJ",
135146
"common.placeholder.email.confirmation": "E-Mail-Adresse wiederholen",
@@ -845,11 +856,11 @@ export default {
845856
"data-room.copy.alert.success": "Raum erfolgreich dupliziert.",
846857
"data-room.copy.alert.error": "Der Dupliziervorgang konnte nicht abgeschlossen werden.",
847858
"data-room.copy.loading": "Raum wird dupliziert...",
848-
"error.400": "400 – Fehlerhafte Anfrage",
849-
"error.401": "401 – Leider fehlt die Berechtigung, diesen Inhalt zu sehen.",
850-
"error.403": "403 – Leider fehlt die Berechtigung, diesen Inhalt zu sehen.",
851-
"error.404": "404 – Seite nicht gefunden",
852-
"error.408": "408 – Zeitüberschreitung bei der Serververbindung",
859+
"error.400": "Fehlerhafte Anfrage",
860+
"error.401": "Leider fehlt die Berechtigung, diesen Inhalt zu sehen.",
861+
"error.403": "Leider fehlt die Berechtigung, diesen Inhalt zu sehen.",
862+
"error.404": "Seite nicht gefunden",
863+
"error.408": "Zeitüberschreitung bei der Serververbindung",
853864
"error.action.back": "Zur Startseite",
854865
"error.generic": "Ein Fehler ist aufgetreten",
855866
"error.load": "Fehler beim Laden der Daten.",

src/locales/en.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export default {
9494
"common.labels.repeat": "Repetition",
9595
"common.labels.restore": "Restore",
9696
"common.labels.role": "Role",
97-
"common.labels.room": "Room",
97+
"common.labels.room": "Room | Rooms",
9898
"common.labels.search": "Search",
9999
"common.labels.settings": "Setting",
100100
"common.labels.size": "Size",
@@ -127,6 +127,12 @@ export default {
127127
"common.medium.information.teacher": "Please contact the school administrator.",
128128
"common.nodata": "No data available",
129129
"common.notification.error": "An error has occurred.",
130+
"common.notifications.errors.notCreated": "{type} could not be created.",
131+
"common.notifications.errors.notDeleted": "{type} could not be deleted.",
132+
"common.notifications.errors.notDuplicated": "{type} could not be duplicated.",
133+
"common.notifications.errors.notLoaded": "{type} could not be loaded.",
134+
"common.notifications.errors.notMoved": "{type} could not be moved.",
135+
"common.notifications.errors.notExited": "{type} could not be exited.",
130136
"common.notification.connection.restored": "The connection has been restored.",
131137
"common.placeholder.birthdate": "20.2.2002",
132138
"common.placeholder.dateformat": "DD.MM.YYYY",
@@ -835,11 +841,11 @@ export default {
835841
"data-room.copy.alert.success": "Room successfully duplicated.",
836842
"data-room.copy.alert.error": "The duplication process could not be completed.",
837843
"data-room.copy.loading": "Room is being duplicated...",
838-
"error.400": "401 – Bad Request",
839-
"error.401": "401 – Unfortunately, you do not have permission to view this content.",
840-
"error.403": "403 – Unfortunately, you do not have permission to view this content.",
841-
"error.404": "404 – Not Found",
842-
"error.408": "408 – Timeout during server connection",
844+
"error.400": "Bad Request",
845+
"error.401": "Unfortunately, you do not have permission to view this content.",
846+
"error.403": "Unfortunately, you do not have permission to view this content.",
847+
"error.404": "Not Found",
848+
"error.408": "Timeout during server connection",
843849
"error.action.back": "Go to Dashboard",
844850
"error.generic": "An error has occurred",
845851
"error.load": "Error while loading the data.",

src/locales/es.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export default {
9696
"common.labels.repeat": "Repetición",
9797
"common.labels.restore": "Restaurar",
9898
"common.labels.role": "Papel",
99-
"common.labels.room": "Sala",
99+
"common.labels.room": "Sala | Salas",
100100
"common.labels.search": "Buscar",
101101
"common.labels.settings": "Ajustes",
102102
"common.labels.size": "Tamaño",
@@ -129,6 +129,12 @@ export default {
129129
"common.medium.information.teacher": "Por favor comuníquese con el administrador de la escuela.",
130130
"common.nodata": "Datos no disponibles",
131131
"common.notification.error": "Se ha producido un error.",
132+
"common.notifications.errors.notCreated": "{type} no se ha podido crear. | {type} no se han podido crear.",
133+
"common.notifications.errors.notDeleted": "{type} no se ha podido eliminar. | {type} no se han podido eliminar.",
134+
"common.notifications.errors.notDuplicated": "{type} no se ha podido duplicar. | {type} no se han podido duplicar.",
135+
"common.notifications.errors.notLoaded": "{type} no se ha podido cargar. | {type} no se han podido cargar.",
136+
"common.notifications.errors.notMoved": "{type} no se ha podido mover. | {type} no se han podido mover.",
137+
"common.notifications.errors.notExited": "{type} no se ha podido salir. | {type} no se han podido salir.",
132138
"common.notification.connection.restored": "La conexión se ha reestablecido.",
133139
"common.placeholder.birthdate": "20.2.2002",
134140
"common.placeholder.dateformat": "DD.MM.AAAA",
@@ -853,11 +859,11 @@ export default {
853859
"data-room.copy.alert.success": "Sala duplicada con éxito.",
854860
"data-room.copy.alert.error": "No se pudo completar el proceso de duplicación.",
855861
"data-room.copy.loading": "La sala se está duplicando...",
856-
"error.400": "401 – Solicitud incorrecta",
857-
"error.401": "401 – Lamentablemente, falta la autorización para ver este contenido.",
858-
"error.403": "403 – Lamentablemente, falta la autorización para ver este contenido.",
859-
"error.404": "404 – No encontrado",
860-
"error.408": "408 – Tiempo de espera de la conexión al servidor",
862+
"error.400": "Solicitud incorrecta",
863+
"error.401": "Lamentablemente, falta la autorización para ver este contenido.",
864+
"error.403": "Lamentablemente, falta la autorización para ver este contenido.",
865+
"error.404": "No encontrado",
866+
"error.408": "Tiempo de espera de la conexión al servidor",
861867
"error.action.back": "Al panel",
862868
"error.generic": "Se ha producido un error",
863869
"error.load": "Error al cargar los datos.",

0 commit comments

Comments
 (0)