Repository: hpi-schul-cloud/nuxt-client
Pull request #3683: BC-9628 - improve invitation dialog
Author: login: hoeppner-dataport
Created: 2025-05-12T11:01:34Z | Merged: 2025-05-16T11:27:40Z

# Short Description
Several improvements for the createInvitationLink-dialog:
- entering a date doesn't lead to "jumping" description (due to validation error message)
- if "link expires" is checked, date should be required, and adding disabled
- change text of info alert from "supervising learning guide" to "supervising teacher" (also for Spanish, Ukrainian)

## Links to Ticket and related Pull-Requests
BC-9628

## Changes

<!--
- What will the PR change?
- Why are the changes required?
- Links to documentation / tickets if exists, or provide more details here.
-->

## Data-security

<!--
Please note here about:
- any data model changes
- any changes about logging of user data
- any changes about permissions
- user input, authentication and other user data related things
If you are not sure if it is relevant, take a look at confluence or ask the data-security team.
-->

## Screenshots of UI changes

<!--
- For UI changes, insert screenshots here.
- Has it been reviewed by a UX colleague?
-->

## Checklist before merging

- [x] QA: In addition to review, the code has been manually tested (if manual testing is possible)
- [x] PO: Any deviation from requirements was agreed with Product-Owner / ticket author / support-team
- [x] DEV: Every new component is implemented having accessibility in mind (e.g. aria-label, role property)

> Notice: Please keep this Pull-Request as a Draft (or add WIP label), until it is ready to be reviewed



Diff:
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 772375490dd..4e9b1a41ee5 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -1914,7 +1914,7 @@ export default {
 		"The next step is to create a link, which can be used to enter the room.",
 	"pages.rooms.members.inviteMember.secondStep.title": "Share invitation link",
 	"pages.rooms.members.inviteMember.infoAlert.text":
-		"Students from other schools must always be added to the room by a supervising learning guide from their school.",
+		"Students from other schools must always be added to the room by a supervising teacher from their school.",
 	"pages.rooms.members.inviteMember.form.description.label":
 		"Description (optional)",
 	"pages.rooms.members.inviteMember.form.description.hint":
diff --git a/src/locales/es.ts b/src/locales/es.ts
index 56110a9c07e..3f17e5e6326 100644
--- a/src/locales/es.ts
+++ b/src/locales/es.ts
@@ -1975,7 +1975,7 @@ export default {
 	"pages.rooms.members.inviteMember.secondStep.title":
 		"Compartir enlace de invitación",
 	"pages.rooms.members.inviteMember.infoAlert.text":
-		"Los alumnos de otras escuelas siempre deben ser agregados a la sala por un guía de aprendizaje supervisor de su escuela a la sala.",
+		"Los alumnos de otras escuelas siempre deben ser agregados a la sala por un profesor supervisor de su escuela.",
 	"pages.rooms.members.inviteMember.form.description.label":
 		"Descripción (opcional)",
 	"pages.rooms.members.inviteMember.form.description.hint":
diff --git a/src/locales/uk.ts b/src/locales/uk.ts
index 1b43dc17fa9..99febd6e933 100644
--- a/src/locales/uk.ts
+++ b/src/locales/uk.ts
@@ -1948,7 +1948,7 @@ export default {
 	"pages.rooms.members.inviteMember.secondStep.title":
 		"Поділіться посиланням на запрошення",
 	"pages.rooms.members.inviteMember.infoAlert.text":
-		"Учні з інших шкіл повинні завжди супроводжуватися в кімнату супроводжуючим з їхньої школи до кімнати.",
+		"Учні з інших шкіл завжди повинні бути присутніми в кімнаті під наглядом вчителя з їхньої школи.",
 	"pages.rooms.members.inviteMember.form.description.label":
 		"Опис (необов'язково)",
 	"pages.rooms.members.inviteMember.form.description.hint":
diff --git a/src/modules/data/room/roomMembers/RoomMembers.store.ts b/src/modules/data/room/roomMembers/RoomMembers.store.ts
index 18cf3497da5..5d496fb0114 100644
--- a/src/modules/data/room/roomMembers/RoomMembers.store.ts
+++ b/src/modules/data/room/roomMembers/RoomMembers.store.ts
@@ -75,6 +75,7 @@ export const useRoomMembersStore = defineStore("roomMembersStore", () => {
 			roomMembers.value = data.map((member: RoomMemberResponse) => {
 				return {
 					...member,
+					fullName: `${member.lastName}, ${member.firstName}`,
 					isSelectable: !(
 						member.userId === currentUserId ||
 						member.roomRoleName === RoleName.Roomowner
@@ -135,6 +136,14 @@ export const useRoomMembersStore = defineStore("roomMembersStore", () => {
 		return roomMembers.value.find((member) => member.userId === userId);
 	};
 
+	const getMemberFullName = (userId = "") => {
+		const member = getMemberById(userId);
+		if (!member) {
+			return "";
+		}
+		return member.fullName;
+	};
+
 	const getSchools = async () => {
 		try {
 			const response =
@@ -279,6 +288,7 @@ export const useRoomMembersStore = defineStore("roomMembersStore", () => {
 		getPotentialMembers,
 		getSchools,
 		getMemberById,
+		getMemberFullName,
 		leaveRoom,
 		removeMembers,
 		updateMembersRole,
diff --git a/src/modules/data/room/roomMembers/RoomMembers.store.unit.ts b/src/modules/data/room/roomMembers/RoomMembers.store.unit.ts
index 68e77fcf517..6b0d8a29204 100644
--- a/src/modules/data/room/roomMembers/RoomMembers.store.unit.ts
+++ b/src/modules/data/room/roomMembers/RoomMembers.store.unit.ts
@@ -646,4 +646,17 @@ describe("useRoomMembers", () => {
 			});
 		});
 	});
+
+	describe("getMemberFullName", () => {
+		it("should return the full name of the member", async () => {
+			const member = roomMemberFactory.build({
+				roomRoleName: RoleName.Roomadmin,
+			});
+			const { roomMembersStore } = setup([member]);
+
+			const result = roomMembersStore.getMemberFullName(member.userId);
+
+			expect(result).toBe(`${member.lastName}, ${member.firstName}`);
+		});
+	});
 });
diff --git a/src/modules/feature/room/RoomMembers/ChangeRole.vue b/src/modules/feature/room/RoomMembers/ChangeRole.vue
index 41d2d29a074..56cc1255eaa 100644
--- a/src/modules/feature/room/RoomMembers/ChangeRole.vue
+++ b/src/modules/feature/room/RoomMembers/ChangeRole.vue
@@ -184,11 +184,7 @@ if (memberToChangeRole.length > 1) {
 }
 
 const currentUserFullName = computed(() => {
-	const currentUserMember = roomMembersStore.roomMembers.find(
-		(member) => member.userId === authModule.getUser?.id
-	);
-	if (!currentUserMember) return "";
-	return `${currentUserMember.firstName} ${currentUserMember.lastName}`;
+	return roomMembersStore.getMemberFullName(authModule.getUser?.id);
 });
 
 const memberFullName = computed(() => {
diff --git a/src/modules/feature/room/RoomMembers/InviteMembersDialog.vue b/src/modules/feature/room/RoomMembers/InviteMembersDialog.vue
index 411f2eb7359..77c271241a0 100644
--- a/src/modules/feature/room/RoomMembers/InviteMembersDialog.vue
+++ b/src/modules/feature/room/RoomMembers/InviteMembersDialog.vue
@@ -12,7 +12,6 @@
 					{{ modalTitle }}
 				</h2>
 			</template>
-
 			<template #text>
 				<template v-if="step === InvitationStep.PREPARE">
 					<p>
@@ -65,27 +64,26 @@
 							hide-details
 						/>
 
-						<div class="d-flex align-center justify-start my-n4">
+						<div class="d-flex align-center justify-start my-n4 pr-0">
 							<v-checkbox
-								v-model="formData.activeUntilCheck"
+								v-model="formData.activeUntilChecked"
 								:label="
 									t('pages.rooms.members.inviteMember.form.linkExpires.label')
 								"
 								hide-details
 								class="mr-2"
 							/>
-
-							<date-picker
+							<DatePicker
 								ref="datePicker"
 								v-model="formData.activeUntil"
 								:disabled="isDatePickerDisabled"
+								:required="!isDatePickerDisabled"
 								:min-date="new Date().toString()"
-								class="mr-2 mt-2"
+								class="mt-1"
 								data-testid="date-picker-until"
-								max-width="110px"
 								@click.prevent="pause"
 								@keydown.space.enter.prevent="pause"
-								@update:date="onUpdateValidDate"
+								@update:date="onUpdateDate"
 							/>
 						</div>
 
@@ -135,6 +133,7 @@
 						class="ms-auto"
 						color="primary"
 						variant="flat"
+						:disabled="isSubmitDisabled"
 						:text="t('common.actions.continue')"
 						data-testid="invite-participant-save-btn"
 						@click="onInviteMembers"
@@ -211,15 +210,21 @@ const defaultFormData = {
 	title: "",
 	restrictedToCreatorSchool: true,
 	isAlsoForStudents: false,
-	activeUntilCheck: false,
-	activeUntil: new Date(),
+	activeUntilChecked: false,
+	activeUntil: null as Date | null,
 	requiresConfirmation: true,
 };
 
 const formData = ref({ ...defaultFormData });
 
 const isDatePickerDisabled = computed(() => {
-	return !formData.value.activeUntilCheck;
+	return !formData.value.activeUntilChecked;
+});
+
+const isSubmitDisabled = computed(() => {
+	return (
+		formData.value.activeUntilChecked && formData.value.activeUntil === null
+	);
 });
 
 const modalTitle = computed(() => {
@@ -228,7 +233,7 @@ const modalTitle = computed(() => {
 		: t("pages.rooms.members.inviteMember.secondStep.title");
 });
 
-const onUpdateValidDate = (date: Date) => {
+const onUpdateDate = (date: Date | null) => {
 	formData.value.activeUntil = date;
 	unpause();
 };
@@ -245,9 +250,10 @@ const onClose = () => {
 const onInviteMembers = async () => {
 	const createLinkBodyParams = {
 		title: formData.value.title || "invitation link",
-		activeUntil: formData.value.activeUntilCheck
-			? formData.value.activeUntil.toString()
-			: "2900-01-01T00:00:00.000Z",
+		activeUntil:
+			formData.value.activeUntilChecked && formData.value.activeUntil
+				? formData.value.activeUntil.toString()
+				: "2900-01-01T00:00:00.000Z",
 		isOnlyForTeachers: !formData.value.isAlsoForStudents,
 		restrictedToCreatorSchool: formData.value.restrictedToCreatorSchool,
 		requiresConfirmation: formData.value.requiresConfirmation,
@@ -273,8 +279,8 @@ const { pause, unpause, deactivate } = useFocusTrap(inviteMembersContent, {
 
 watch(
 	() => formData.value.restrictedToCreatorSchool,
-	(newValue: boolean) => {
-		if (!newValue) {
+	(isRestrictedToCreatorSchool: boolean) => {
+		if (isRestrictedToCreatorSchool === false) {
 			formData.value.isAlsoForStudents = false;
 		}
 	}
@@ -282,8 +288,8 @@ watch(
 
 watch(
 	() => isOpen.value,
-	(newValue: boolean) => {
-		if (!newValue) {
+	(isOpen: boolean) => {
+		if (isOpen === false) {
 			deactivate();
 		}
 	}
diff --git a/src/modules/feature/room/RoomMembers/MembersTable.vue b/src/modules/feature/room/RoomMembers/MembersTable.vue
index cc69fddf2cf..f5c7fb06f18 100644
--- a/src/modules/feature/room/RoomMembers/MembersTable.vue
+++ b/src/modules/feature/room/RoomMembers/MembersTable.vue
@@ -73,7 +73,7 @@
 					})
 				"
 				:disabled="item.isSelectable === false"
-				:aria-label="`${item.firstName}  ${item.lastName}`"
+				:aria-label="`${item.fullName}`"
 				@click="
 					toggleSelect({
 						value: item.userId,
@@ -189,12 +189,8 @@ const onRemoveMembers = async (userIds: string[]) => {
 const confirmRemoval = async (userIds: string[]) => {
 	let message = t("pages.rooms.members.multipleRemove.confirmation");
 	if (userIds.length === 1) {
-		const member = roomMembers.value.find(
-			(member) => member.userId === userIds[0]
-		);
-		message = t("pages.rooms.members.remove.confirmation", {
-			memberFullName: `${member?.firstName} ${member?.lastName}`,
-		});
+		const memberFullName = roomMembersStore.getMemberFullName(userIds[0]);
+		message = t("pages.rooms.members.remove.confirmation", { memberFullName });
 	}
 	const shouldRemove = await askConfirmation({
 		message,
@@ -213,22 +209,16 @@ const onChangePermission = (userIds: string[]) => {
 
 const getAriaLabel = (
 	member: RoomMember,
-	actionFor?: "remove" | "changeRole"
+	actionFor: "remove" | "changeRole" | "" = ""
 ) => {
-	const memberFullName = `${member.firstName} ${member.lastName}`;
-	if (actionFor === "changeRole") {
-		return t("pages.rooms.members.changePermission.ariaLabel", {
-			memberFullName,
-		});
-	}
-	if (actionFor === "remove") {
-		return t("pages.rooms.members.remove.ariaLabel", {
-			memberFullName,
-		});
-	}
-	return t("pages.rooms.members.actionMenu.ariaLabel", {
-		memberFullName,
-	});
+	const memberFullName = member.fullName;
+	const mapActionToConst = {
+		remove: "pages.rooms.members.remove.ariaLabel",
+		changeRole: "pages.rooms.members.changePermission.ariaLabel",
+		"": "pages.rooms.members.actionMenu.ariaLabel",
+	};
+	const languageKey = mapActionToConst[actionFor];
+	return t(languageKey, { memberFullName });
 };
 
 const tableHeader = computed(() => {
diff --git a/src/modules/ui/date-time-picker/DatePicker.unit.ts b/src/modules/ui/date-time-picker/DatePicker.unit.ts
index 588f424924d..9a2b5275932 100644
--- a/src/modules/ui/date-time-picker/DatePicker.unit.ts
+++ b/src/modules/ui/date-time-picker/DatePicker.unit.ts
@@ -1,4 +1,8 @@
-import { ComponentMountingOptions, mount } from "@vue/test-utils";
+import {
+	ComponentMountingOptions,
+	flushPromises,
+	mount,
+} from "@vue/test-utils";
 import {
 	createTestingI18n,
 	createTestingVuetify,
@@ -21,6 +25,20 @@ describe("DatePicker", () => {
 		});
 	};
 
+	const setup = () => {
+		const wrapper = mountComponent({
+			props: {
+				date: new Date().toISOString(),
+				required: true,
+			},
+		});
+
+		const textField = wrapper.findComponent({ name: "v-text-field" });
+		const input = textField.find("input");
+
+		return { wrapper, textField, input };
+	};
+
 	it("should render component", () => {
 		const wrapper = mountComponent({
 			props: { date: new Date().toISOString() },
@@ -28,41 +46,61 @@ describe("DatePicker", () => {
 		expect(wrapper.findComponent(DatePicker).exists()).toBe(true);
 	});
 
-	it("should emit update:date event on input", async () => {
-		const wrapper = mountComponent({
-			props: { date: new Date().toISOString() },
-		});
+	it("should render text field", () => {
+		const { textField } = setup();
 
-		const textField = wrapper
-			.findComponent({ name: "v-text-field" })
-			.find("input");
 		expect(textField.exists()).toBe(true);
+	});
+
+	it("should emit update:date event on input", async () => {
+		const { wrapper, textField } = setup();
+
 		await textField.trigger("click");
 
-		const dateSelector = wrapper.findComponent({ name: "v-date-picker" });
-		expect(dateSelector.exists()).toBe(true);
-		dateSelector.vm.$emit("update:modelValue", new Date().toISOString());
+		const datePicker = wrapper.findComponent({ name: "v-date-picker" });
+		expect(datePicker.exists()).toBe(true);
 
+		await datePicker.vm.$emit("update:modelValue", new Date().toISOString());
+		await flushPromises();
 		expect(wrapper.emitted("update:date")).toHaveLength(1);
 	});
 
 	describe("when required prop is set & value is left empty", () => {
 		it("should emit error event", async () => {
-			const wrapper = mountComponent({
-				props: {
-					date: new Date().toISOString(),
-					required: true,
-				},
-			});
-
-			const textField = wrapper.findComponent({ name: "v-text-field" });
-			const input = textField.find("input");
+			const { wrapper, input } = setup();
 
 			await input.setValue("");
+			await flushPromises();
 
-			expect(wrapper.emitted("update:date")).toBeUndefined();
 			expect(wrapper.emitted("error")).toHaveLength(1);
 		});
+
+		it("should emit update:date event with null value", async () => {
+			const { wrapper, input } = setup();
+
+			await input.setValue("");
+			await flushPromises();
+
+			const updateDateEvent = wrapper.emitted("update:date");
+			const updateDateEventValue = updateDateEvent?.[0][0];
+
+			expect(updateDateEvent).toHaveLength(1);
+			expect(updateDateEventValue).toBe(null);
+		});
+
+		it("should display error message for invalid date format", async () => {
+			jest.useFakeTimers();
+			const { textField, input } = setup();
+
+			await input.setValue("");
+			jest.advanceTimersByTime(1000);
+			await flushPromises();
+
+			const errorElement = textField.find(".v-messages");
+			expect(errorElement.text()).toEqual(
+				"components.datePicker.validation.required"
+			);
+		});
 	});
 
 	describe("when date is invalid", () => {
@@ -75,13 +113,38 @@ describe("DatePicker", () => {
 			const input = textField.find("input");
 
 			await input.setValue("55.55.5555");
+			await flushPromises();
 
-			expect(wrapper.emitted("update:date")).toBeUndefined();
 			expect(wrapper.emitted("error")).toHaveLength(1);
 		});
 
-		// TODO: Can't properly be tested with debounced validation, needs a new approach
-		it.todo("should display error message");
+		it("should emit update:date event with null value", async () => {
+			const { wrapper, input } = setup();
+
+			await input.setValue("");
+			await flushPromises();
+
+			const updateDateEvent = wrapper.emitted("update:date");
+			const updateDateEventValue = updateDateEvent?.[0][0];
+
+			expect(updateDateEvent).toHaveLength(1);
+			expect(updateDateEventValue).toBe(null);
+		});
+
+		it("should display error message for invalid date format", async () => {
+			jest.useFakeTimers();
+			const { textField, input } = setup();
+
+			await input.setValue("22");
+			jest.advanceTimersByTime(1000);
+			await flushPromises();
+
+			const errorElement = textField.find(".v-messages");
+			expect(errorElement.text()).toEqual(
+				"components.datePicker.validation.format"
+			);
+		});
+
 		it.todo("should display external error message");
 	});
 });
diff --git a/src/modules/ui/date-time-picker/DatePicker.vue b/src/modules/ui/date-time-picker/DatePicker.vue
index 0692c7aff26..42ddbb3cf20 100644
--- a/src/modules/ui/date-time-picker/DatePicker.vue
+++ b/src/modules/ui/date-time-picker/DatePicker.vue
@@ -6,17 +6,17 @@
 	>
 		<template #activator="{ props: menuProps }">
 			<v-text-field
-				ref="inputField"
+				ref="date-text-field"
 				v-bind="menuProps"
 				v-bind.attr="$attrs"
 				v-model="dateString"
 				v-date-input-mask
-				:append-inner-icon="mdiCalendar"
+				:prepend-inner-icon="mdiCalendar"
 				:label="label"
 				:aria-label="ariaLabel"
 				:placeholder="t('common.placeholder.dateformat')"
 				:error-messages="errorMessages"
-				@update:model-value="validate"
+				@update:model-value="onUpdateTextfield"
 				@keydown.space="showDateDialog = true"
 				@keydown.prevent.enter="showDateDialog = true"
 				@keydown.up.down.stop
@@ -33,14 +33,22 @@
 				hide-header
 				show-adjacent-months
 				elevation="6"
-				@update:model-value="closeAndEmit"
+				@update:model-value="onPickDate"
 			/>
 		</UseFocusTrap>
 	</v-menu>
 </template>
 
 <script setup lang="ts">
-import { computed, ref, watchEffect, unref, PropType, toRef } from "vue";
+import {
+	computed,
+	ref,
+	watchEffect,
+	unref,
+	PropType,
+	toRef,
+	useTemplateRef,
+} from "vue";
 import { useDebounceFn, computedAsync } from "@vueuse/core";
 import dayjs from "dayjs";
 import { useI18n } from "vue-i18n";
@@ -69,10 +77,9 @@ const emit = defineEmits(["update:date", "error"]);
 const { t } = useI18n();
 
 const showDateDialog = ref(false);
-const inputField = ref<HTMLInputElement | null>(null);
+const dateTextField = useTemplateRef("date-text-field");
 const dateString = ref<string>();
 const externalErrors = toRef(props, "errors");
-
 watchEffect(() => {
 	dateString.value = props.date
 		? dayjs(props.date).format(DATETIME_FORMAT.date)
@@ -119,7 +126,6 @@ const combinedErrors = computed(() => {
 
 const errorMessages = computedAsync<string[] | null>(async () => {
 	const messages = await getErrorMessages(combinedErrors.value);
-
 	return messages ?? [];
 }, null);
 
@@ -134,30 +140,39 @@ const getErrorMessages = useDebounceFn(
 	700
 );
 
-const validate = () => {
-	v$.value.dateString.$touch();
-	v$.value.$validate();
-
-	if (isValid.value) {
-		emitDate();
-	} else {
-		emit("error");
+watchEffect(async () => {
+	if (props.required === true) {
+		v$.value.dateString.$touch();
+		await v$.value.$validate();
 	}
+});
+
+const onUpdateTextfield = async () => {
+	await emitDate();
 };
 
-const emitDate = () => {
-	const date = dateString.value
-		? dayjs(dateString.value, DATETIME_FORMAT.date).toISOString()
+const onPickDate = async () => {
+	dateString.value = dateObject.value
+		? dayjs(dateObject.value).format(DATETIME_FORMAT.date)
 		: undefined;
-
-	emit("update:date", date);
+	await emitDate();
+	showDateDialog.value = false;
+	dateTextField.value?.focus();
 };
 
-const closeAndEmit = () => {
-	showDateDialog.value = false;
-	inputField.value?.focus();
+const emitDate = async () => {
+	v$.value.dateString.$touch();
+	const isValid = await v$.value.$validate();
+	if (isValid) {
+		const formattedDate = dateString.value
+			? dayjs(dateString.value, DATETIME_FORMAT.date).toISOString()
+			: null;
 
-	emitDate();
+		emit("update:date", formattedDate);
+	} else {
+		emit("update:date", null);
+		emit("error");
+	}
 };
 </script>
 


Reviews:
user: login: muratmerdoglu-dp
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T09:59:15Z
user: login: hoeppner-dataport
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:26:42Z
user: login: bischofmax
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T12:32:26Z
user: login: hoeppner-dataport
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T13:55:05Z

Review comments:
diff_hunk: @@ -273,22 +275,27 @@ const { pause, unpause, deactivate } = useFocusTrap(inviteMembersContent, {
 
 watch(
 	() => formData.value.restrictedToCreatorSchool,
-	(newValue: boolean) => {
-		if (!newValue) {
+	(isRestrictedToCreatorSchool: boolean) => {
+		if (isRestrictedToCreatorSchool === false) {
 			formData.value.isAlsoForStudents = false;
 		}
 	}
 );
 
 watch(
 	() => isOpen.value,
-	(newValue: boolean) => {
-		if (!newValue) {
+	(isOpen: boolean) => {
+		if (isOpen === false) {
 			deactivate();
 		}
 	}
 );
 
+watchEffect(() => {
+	isSubmitDisabled.value =
+		formData.value.activeUntilChecked && formData.value.activeUntil === null;
+});
+
path: src/modules/feature/room/RoomMembers/InviteMembersDialog.vue
user: login: muratmerdoglu-dp
body: Instead of using `watchEffect` to update `isSubmitDisabled`, we can consider using a `computed` property. 
created_at: 2025-05-14T09:40:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -273,22 +275,27 @@ const { pause, unpause, deactivate } = useFocusTrap(inviteMembersContent, {
 
 watch(
 	() => formData.value.restrictedToCreatorSchool,
-	(newValue: boolean) => {
-		if (!newValue) {
+	(isRestrictedToCreatorSchool: boolean) => {
+		if (isRestrictedToCreatorSchool === false) {
 			formData.value.isAlsoForStudents = false;
 		}
 	}
 );
 
 watch(
 	() => isOpen.value,
-	(newValue: boolean) => {
-		if (!newValue) {
+	(isOpen: boolean) => {
+		if (isOpen === false) {
 			deactivate();
 		}
 	}
 );
 
+watchEffect(() => {
+	isSubmitDisabled.value =
+		formData.value.activeUntilChecked && formData.value.activeUntil === null;
+});
+
path: src/modules/feature/room/RoomMembers/InviteMembersDialog.vue
user: login: hoeppner-dataport
body: you are right: computed is better suited here. fixed.
created_at: 2025-05-14T10:26:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,7 +12,6 @@
 					{{ modalTitle }}
path: src/modules/feature/room/RoomMembers/InviteMembersDialog.vue
user: login: bischofmax
body: I am currently working on a generic dialog. This question came up while getting the font right. Why is everybody using text-h4 class instead of h4 Tag? Often h2 tag combined with text-h4 class? 
created_at: 2025-05-14T12:32:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,7 +12,6 @@
 					{{ modalTitle }}
path: src/modules/feature/room/RoomMembers/InviteMembersDialog.vue
user: login: hoeppner-dataport
body: This topic was discussed today in frontendArc. Outcome: there will be a ticket addressing cleaning up default styling of vuetify headlines and texts.
btw: This line was not touched in this PR.

created_at: 2025-05-15T13:55:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-15T14:01:12Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=hpi-schul-cloud_nuxt-client&pullRequest=3683) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=hpi-schul-cloud_nuxt-client&pullRequest=3683&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=hpi-schul-cloud_nuxt-client&pullRequest=3683&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hpi-schul-cloud_nuxt-client&pullRequest=3683&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [92.3% Coverage on New Code](https://sonarcloud.io/component_measures?id=hpi-schul-cloud_nuxt-client&pullRequest=3683&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=hpi-schul-cloud_nuxt-client&pullRequest=3683&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=hpi-schul-cloud_nuxt-client&pullRequest=3683)


author_association: NONE