Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-8213 - validate room dates #3434

Merged
merged 38 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9f90a65
add max length validation for room title
odalys-dataport Oct 23, 2024
b6da67a
Merge branch 'main' of https://github.com/hpi-schul-cloud/nuxt-client…
odalys-dataport Oct 28, 2024
e40089f
add min date
odalys-dataport Oct 28, 2024
1770f85
Merge branch 'main' of https://github.com/hpi-schul-cloud/nuxt-client…
odalys-dataport Nov 1, 2024
98bbeb2
fix vuetify lang key missing warning
odalys-dataport Nov 1, 2024
27e3def
startBeforeEndDate validator
odalys-dataport Nov 3, 2024
ff8391a
adjust validator to work for both dates
odalys-dataport Nov 3, 2024
dab7e72
show prop error messages
odalys-dataport Nov 4, 2024
fa7b1a0
remove translations code
odalys-dataport Nov 7, 2024
ee8067c
fix es date format error message
odalys-dataport Nov 7, 2024
6113e73
combine error messages in date picker
odalys-dataport Nov 7, 2024
299f19f
german error message
odalys-dataport Nov 7, 2024
853c86b
translated error message
odalys-dataport Nov 7, 2024
103dd6a
solve merge conflicts in language files
odalys-dataport Nov 11, 2024
88dcc53
show notification on general error
odalys-dataport Nov 12, 2024
783fe38
only show startdate error on startdate field
odalys-dataport Nov 12, 2024
a0529e1
Merge branch 'main' of https://github.com/hpi-schul-cloud/nuxt-client…
odalys-dataport Nov 12, 2024
7b774d6
add notification on edit
odalys-dataport Nov 12, 2024
a27f8bb
Merge branch 'main' into BC-8213-validate-room-dates
odalys-dataport Nov 17, 2024
e7d1531
adjust roomform tests
odalys-dataport Nov 17, 2024
9cabbbc
add basic tests for small room components
odalys-dataport Nov 17, 2024
ccdd8c1
disable today's date for end date picker
odalys-dataport Nov 17, 2024
6d80bce
reduce validator complexity
odalys-dataport Nov 17, 2024
6d4434f
Merge branch 'main' into BC-8213-validate-room-dates
odalys-dataport Nov 18, 2024
372d29d
allow same day
odalys-dataport Nov 18, 2024
e9ed0b3
adjust BoardTile tests
odalys-dataport Nov 19, 2024
f7d27a3
BoardGrid Tests
odalys-dataport Nov 19, 2024
09c59e5
adjust RoomDetails tests
odalys-dataport Nov 19, 2024
e31eda8
handle error in component (create)
odalys-dataport Nov 19, 2024
c5f71ec
separate date compare function
odalys-dataport Nov 19, 2024
d7a5e38
test save event for correct values
odalys-dataport Nov 19, 2024
e43d8a8
handle bad requests in component
odalys-dataport Nov 22, 2024
2b99358
remove unnecessary error type
odalys-dataport Nov 24, 2024
86e730a
use flushPromises instead of nextTick
odalys-dataport Nov 24, 2024
937eaaa
add todos for complicated tests
odalys-dataport Nov 25, 2024
fabf5c5
translated error message for max length validation
odalys-dataport Nov 25, 2024
f8b521d
removed unused imports
odalys-dataport Nov 25, 2024
2cf9b0c
Merge branch 'main' into BC-8213-validate-room-dates
odalys-dataport Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,10 @@ export default {
"components.roomForm.labels.timePeriod": "Zeitraum",
"components.roomForm.labels.timePeriod.from": "Zeitraum von",
"components.roomForm.labels.timePeriod.to": "Zeitraum bis",
"components.roomForm.validation.generalSaveError":
"Beim Speichern ist ein Fehler aufgetreten. Bitte überprüfe deine Eingaben und versuche es erneut.",
"components.roomForm.validation.timePeriod.startBeforeEnd":
"Das Startdatum muss vor dem Enddatum liegen.",
"components.timePicker.validation.format": "Bitte Format HH:MM verwenden.",
"components.timePicker.validation.required": "Bitte Uhrzeit angeben.",
"error.400": "400 – Fehlerhafte Anfrage",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,10 @@ export default {
"components.roomForm.labels.timePeriod": "Time period",
"components.roomForm.labels.timePeriod.from": "Time period from",
"components.roomForm.labels.timePeriod.to": "Time period to",
"components.roomForm.validation.generalSaveError":
"An error occurred while saving. Please check your inputs and try again.",
"components.roomForm.validation.timePeriod.startBeforeEnd":
"The start date must be before the end date.",
"components.timePicker.validation.format": "Please use format HH:MM",
"components.timePicker.validation.required": "Please enter a time.",
"error.400": "401 – Bad Request",
Expand Down
6 changes: 5 additions & 1 deletion src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export default {
"components.cardElement.deletedElement.warning.externalToolElement":
"La herramienta {toolName} no está disponible. Por favor comuníquese con el administrador de la escuela.",
"components.datePicker.validation.format":
"Por favor utilice el formato DD.MM.YYYY",
"Por favor utilice el formato DD.MM.AAAA",
"components.datePicker.validation.required": "Por favor ingrese una fecha.",
"components.dateTimePicker.messages.dateInPast":
"La fecha y la hora están en el pasado.",
Expand Down Expand Up @@ -818,6 +818,10 @@ export default {
"components.roomForm.labels.timePeriod": "Periodo de tiempo",
"components.roomForm.labels.timePeriod.from": "Periodo de tiempo desde",
"components.roomForm.labels.timePeriod.to": "Periodo de tiempo hasta",
"components.roomForm.validation.generalSaveError":
"Se ha producido un error al guardar. Por favor, compruebe sus entradas e inténtelo de nuevo.",
"components.roomForm.validation.timePeriod.startBeforeEnd":
"La fecha de inicio debe ser anterior a la fecha de finalización.",
"components.timePicker.validation.format":
"Por favor utilice el formato HH:MM",
"components.timePicker.validation.required": "Por favor ingrese un tiempo.",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/uk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,10 @@ export default {
"components.roomForm.labels.timePeriod": "Період часу",
"components.roomForm.labels.timePeriod.from": "Період від",
"components.roomForm.labels.timePeriod.to": "Період до",
"components.roomForm.validation.generalSaveError":
"Виникла помилка при збереженні. Будь ласка, перевірте свої записи та спробуйте ще раз.",
"components.roomForm.validation.timePeriod.startBeforeEnd":
"Дата початку повинна передувати даті закінчення.",
"components.timePicker.validation.format": "Використовуйте формат ГГ:ХХ",
"components.timePicker.validation.required": "Будь ласка, введіть час.",
"error.400": "400 – Неприпустимий запит",
Expand Down
19 changes: 17 additions & 2 deletions src/modules/data/room/RoomCreate.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { RoomApiFactory, RoomColor } from "@/serverApi/v3";
import { RoomCreateParams, RoomItem } from "@/types/room/Room";
import { $axios, mapAxiosErrorToResponseError } from "@/utils/api";
import { createApplicationError } from "@/utils/create-application-error.factory";
import { injectStrict, NOTIFIER_MODULE_KEY } from "@/utils/inject";
import { ref } from "vue";
import { useI18n } from "vue-i18n";

export const useRoomCreateState = () => {
const notifierModule = injectStrict(NOTIFIER_MODULE_KEY);
const { t } = useI18n();

const roomApi = RoomApiFactory(undefined, "/v3", $axios);
const isLoading = ref(true);

Expand All @@ -15,15 +20,25 @@ export const useRoomCreateState = () => {
endDate: undefined,
});

const createRoom = async (params: RoomCreateParams): Promise<RoomItem> => {
const createRoom = async (
params: RoomCreateParams
): Promise<RoomItem | undefined> => {
isLoading.value = true;
try {
odalys-dataport marked this conversation as resolved.
Show resolved Hide resolved
const room = (await roomApi.roomControllerCreateRoom(params)).data;

return room;
} catch (error) {
const responseError = mapAxiosErrorToResponseError(error);

throw createApplicationError(responseError.code);
if (responseError.type === "API_VALIDATION_ERROR") {
notifierModule.show({
text: t("components.roomForm.validation.generalSaveError"),
status: "error",
});
} else {
throw createApplicationError(responseError.code);
}
odalys-dataport marked this conversation as resolved.
Show resolved Hide resolved
} finally {
isLoading.value = false;
}
Expand Down
20 changes: 17 additions & 3 deletions src/modules/data/room/RoomEdit.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { RoomApiFactory, RoomColor } from "@/serverApi/v3";
import { RoomDetails, RoomUpdateParams } from "@/types/room/Room";
import { $axios, mapAxiosErrorToResponseError } from "@/utils/api";
import { createApplicationError } from "@/utils/create-application-error.factory";
import { injectStrict, NOTIFIER_MODULE_KEY } from "@/utils/inject";
import { ref } from "vue";
import { useI18n } from "vue-i18n";

export const useRoomEditState = () => {
const notifierModule = injectStrict(NOTIFIER_MODULE_KEY);
const { t } = useI18n();

const roomApi = RoomApiFactory(undefined, "/v3", $axios);
const isLoading = ref(true);

Expand Down Expand Up @@ -40,14 +45,23 @@ export const useRoomEditState = () => {
const updateRoom = async (
id: string,
params: RoomUpdateParams
): Promise<void> => {
): Promise<RoomDetails | undefined> => {
isLoading.value = true;
try {
await roomApi.roomControllerUpdateRoom(id, params);
const room = (await roomApi.roomControllerUpdateRoom(id, params)).data;
odalys-dataport marked this conversation as resolved.
Show resolved Hide resolved

return room;
} catch (error) {
const responseError = mapAxiosErrorToResponseError(error);

throw createApplicationError(responseError.code);
if (responseError.type === "API_VALIDATION_ERROR") {
notifierModule.show({
text: t("components.roomForm.validation.generalSaveError"),
status: "error",
});
} else {
throw createApplicationError(responseError.code);
}
} finally {
isLoading.value = false;
}
Expand Down
63 changes: 56 additions & 7 deletions src/modules/feature/room/RoomForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
<div class="d-flex">
<DatePicker
:date="roomData.startDate"
:min-date="todayISO"
:errors="startDateErrors"
class="w-50 mr-4"
data-testid="room-start-date-input"
:aria-label="$t('components.roomForm.labels.timePeriod.from')"
@update:date="onUpdateStartDate"
/>
<DatePicker
:date="roomData.endDate"
:min-date="todayISO"
class="w-50 ml-4"
data-testid="room-end-date-input"
:aria-label="$t('components.roomForm.labels.timePeriod.to')"
Expand Down Expand Up @@ -66,13 +69,15 @@ import { computed, PropType, unref } from "vue";
import RoomColorPicker from "./RoomColorPicker/RoomColorPicker.vue";
import { DatePicker } from "@ui-date-time-picker";
import { ErrorObject, useVuelidate } from "@vuelidate/core";
import { helpers, required } from "@vuelidate/validators";
import { helpers, required, maxLength } from "@vuelidate/validators";
import { useI18n } from "vue-i18n";
import { RoomCreateParams, RoomUpdateParams } from "@/types/room/Room";
import {
ConfirmationDialog,
useConfirmationDialog,
} from "@ui-confirmation-dialog";
import { DATETIME_FORMAT } from "@/plugins/datetime";
import dayjs from "dayjs";

const props = defineProps({
room: {
Expand All @@ -86,13 +91,58 @@ const { t } = useI18n();
const { askConfirmation } = useConfirmationDialog();

const roomData = computed(() => props.room);
const todayISO = computed(() =>
dayjs.tz(new Date(), "DD.MM.YYYY", "UTC").format(DATETIME_FORMAT.inputDate)
MartinSchuhmacher marked this conversation as resolved.
Show resolved Hide resolved
);

const startBeforeEndDate = (compareDate: {
date: string | undefined;
type: "startDate" | "endDate";
}) => {
const givenDateIsStartDate = compareDate.type === "endDate";
odalys-dataport marked this conversation as resolved.
Show resolved Hide resolved

return helpers.withParams(
{ type: "startBeforeEndDate", value: compareDate },
helpers.withMessage(
t("components.roomForm.validation.timePeriod.startBeforeEnd"),
(givenDate: string) => {
odalys-dataport marked this conversation as resolved.
Show resolved Hide resolved
let startDate: string | undefined;
let endDate: string | undefined;

if (givenDateIsStartDate) {
startDate = givenDate;
endDate = compareDate.date;
} else {
startDate = compareDate.date;
endDate = givenDate;
}

if (!startDate || !endDate) return true;
return new Date(startDate) < new Date(endDate);
}
)
);
};

// Validation
const validationRules = computed(() => ({
roomData: {
name: {
maxLength: maxLength(100),
required: helpers.withMessage(t("common.validation.required2"), required),
},
startDate: {
startBeforeEndDate: startBeforeEndDate({
date: roomData.value.endDate,
type: "endDate",
}),
},
endDate: {
startBeforeEndDate: startBeforeEndDate({
date: roomData.value.startDate,
type: "startDate",
}),
},
},
}));

Expand All @@ -102,27 +152,26 @@ const v$ = useVuelidate(
{ $lazy: true, $autoDirty: true }
);

const startDateErrors = computed(() => v$.value.roomData.startDate.$errors);
const endDateErrors = computed(() => v$.value.roomData.endDate.$errors);

const onUpdateColor = () => {
v$.value.$touch();
};

const onUpdateStartDate = (newDate: string) => {
roomData.value.startDate = newDate;
v$.value.$touch();
};

const onUpdateEndDate = (newDate: string) => {
roomData.value.endDate = newDate;
v$.value.$touch();
};

const onSave = async () => {
const valid = await v$.value.$validate();
if (!valid) {
// TODO notify user form is invalid
return;
if (valid) {
emit("save", roomData.value);
}
emit("save", roomData.value);
};

const onCancel = async () => {
Expand Down
7 changes: 5 additions & 2 deletions src/modules/page/room/RoomCreate.page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ const breadcrumbs: Breadcrumb[] = [
];

const onSave = async (roomParams: RoomCreateParams) => {
const { id } = await createRoom(roomParams);
router.push({ name: "room-details", params: { id } });
const room = await createRoom(roomParams);

if (room) {
router.push({ name: "room-details", params: { id: room.id } });
}
odalys-dataport marked this conversation as resolved.
Show resolved Hide resolved
};

const onCancel = () => {
Expand Down
13 changes: 8 additions & 5 deletions src/modules/page/room/RoomEdit.page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,14 @@ watch(
);

const onSave = async (roomParams: RoomUpdateParams) => {
await updateRoom(route.params.id as string, roomParams);
router.push({
name: "room-details",
params: { id: route.params.id as string },
});
const room = await updateRoom(route.params.id as string, roomParams);

if (room) {
router.push({
name: "room-details",
params: { id: route.params.id as string },
});
}
};

const onCancel = () => {
Expand Down
47 changes: 31 additions & 16 deletions src/modules/ui/date-time-picker/DatePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</template>

<script setup lang="ts">
import { computed, ref, watchEffect, unref } from "vue";
import { computed, ref, watchEffect, unref, PropType, toRef } from "vue";
import { useDebounceFn, computedAsync } from "@vueuse/core";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
Expand All @@ -61,13 +61,15 @@ const props = defineProps({
required: { type: Boolean },
minDate: { type: String },
maxDate: { type: String },
errors: { type: Array as PropType<ErrorObject[]>, default: () => [] },
});
const emit = defineEmits(["update:date", "error"]);
const { t, locale } = useI18n();

const showDateDialog = ref(false);
const inputField = ref<HTMLInputElement | null>(null);
const dateString = ref<string>();
const externalErrors = toRef(props, "errors");

watchEffect(() => {
dateString.value = props.date
Expand All @@ -77,12 +79,13 @@ watchEffect(() => {

const dateObject = computed({
get() {
if (v$.value.dateString.$invalid)
return props.date ? new Date(props.date) : undefined;
if (isValid.value) {
return dateString.value
? dayjs(dateString.value, DATETIME_FORMAT.date).toDate()
: undefined;
MartinSchuhmacher marked this conversation as resolved.
Show resolved Hide resolved
}

return dateString.value
? dayjs(dateString.value, DATETIME_FORMAT.date).toDate()
: undefined;
return props.date ? new Date(props.date) : undefined;
},
set(newValue) {
dateString.value = dayjs(newValue).format(DATETIME_FORMAT.date);
Expand All @@ -104,24 +107,36 @@ const rules = computed(() => ({

const v$ = useVuelidate(rules, { dateString }, { $lazy: true });

const errorMessages = computedAsync(async () => {
return await getErrorMessages(v$.value.dateString);
const isValid = computed(() => {
return !v$.value.dateString.$invalid;
});

const combinedErrors = computed(() => {
return v$.value.dateString.$errors.concat(externalErrors.value);
});

const errorMessages = computedAsync<string[] | null>(async () => {
const messages = await getErrorMessages(combinedErrors.value);

return messages ?? [];
}, null);

// TODO - figure out type, ExtractRulesResults is not exported
const getErrorMessages = useDebounceFn((validationModel: any) => {
const messages = validationModel.$errors.map((e: ErrorObject) => {
return e.$message;
});
const getErrorMessages = useDebounceFn(
async (errors: ErrorObject[] | undefined) => {
const messages = errors?.map((e: ErrorObject) => {
return unref(e.$message);
});

return unref(messages);
}, 700);
return messages;
},
700
);

const validate = () => {
v$.value.dateString.$touch();
v$.value.$validate();

if (!v$.value.dateString.$invalid) {
if (isValid.value) {
emitDate();
} else {
emit("error");
Expand Down
Loading