diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 14c26a8f89..c9105942e9 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -145,6 +145,9 @@ module.exports = { "@feature-board-collaborative-text-editor-element": getDir( "src/modules/feature/board-collaborative-text-editor-element" ), + "@feature-board-video-conference-element": getDir( + "src/modules/feature/board-video-conference-element" + ), "@feature-board-deleted-element": getDir( "src/modules/feature/board-deleted-element" ), @@ -166,6 +169,7 @@ module.exports = { "@ui-kebab-menu": getDir("src/modules/ui/kebab-menu"), "@ui-layout": getDir("src/modules/ui/layout"), "@ui-light-box": getDir("src/modules/ui/light-box"), + "@ui-line-clamp": getDir("src/modules/ui/line-clamp"), "@ui-preview-image": getDir("src/modules/ui/preview-image"), "@ui-room-details": getDir("src/modules/ui/room-details"), "@ui-skip-link": getDir("src/modules/ui/skip-link"), diff --git a/src/assets/img/index.d.ts b/src/assets/img/index.d.ts index fedf5cbbc2..041d291d64 100644 --- a/src/assets/img/index.d.ts +++ b/src/assets/img/index.d.ts @@ -11,3 +11,7 @@ declare module "@/assets/img/collaborativeEditor.svg" { const value: string; export default value; } +declare module "@/assets/img/videoConference.svg" { + const value: string; + export default value; +} diff --git a/src/assets/img/videoConference.svg b/src/assets/img/videoConference.svg new file mode 100644 index 0000000000..e1bbc5ccc7 --- /dev/null +++ b/src/assets/img/videoConference.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/icons/material/index.ts b/src/components/icons/material/index.ts index 5a3ce94511..2bb64c1a26 100644 --- a/src/components/icons/material/index.ts +++ b/src/components/icons/material/index.ts @@ -136,6 +136,7 @@ import { mdiTune, mdiUndo, mdiUndoVariant, + mdiVideo, mdiViewAgendaOutline, mdiViewDashboard, mdiViewDashboardOutline, @@ -282,6 +283,7 @@ export { mdiTune, mdiUndo, mdiUndoVariant, + mdiVideo, mdiViewAgendaOutline, mdiViewDashboard, mdiViewDashboardOutline, diff --git a/src/components/organisms/vCustomDialog.vue b/src/components/organisms/vCustomDialog.vue index 8e503755e0..706dedebaa 100644 --- a/src/components/organisms/vCustomDialog.vue +++ b/src/components/organisms/vCustomDialog.vue @@ -6,7 +6,7 @@ @click:outside="closeDialog" @keydown.esc="closeDialog" > - + diff --git a/src/locales/de.ts b/src/locales/de.ts index c6aae542b5..b4774c7bc6 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -167,6 +167,7 @@ export default { "common.words.color.purple": "Violett", "common.words.color.red": "Rot", "common.words.color.turquoise": "Türkis", + "common.words.color.yellow": "Gelb", "common.words.copiedToClipboard": "In die Zwischenablage kopiert", "common.words.courseGroups": "Kursgruppen", "common.words.courses": "Kurse", @@ -353,6 +354,8 @@ export default { "components.board.menu.fileElement": "Datei-Einstellungen", "components.board.menu.linkElement": "Link-Einstellungen", "components.board.menu.submissionElement": "Abgabe-Einstellungen", + "components.board.menu.videoConferenceElement": + "Videokonferenz-Einstellungen", "components.board.menu.deletedElement": "Gelöschter-Inhalt-Einstellungen", "components.board.notifications.errors.fileNameExists": "Es existiert bereits eine Datei mit diesem Namen.", @@ -448,6 +451,9 @@ export default { "components.cardElement.titleElement.validation.required": "Bitte Titel angeben.", "components.cardElement.titleElement": "Titelelement", + "components.cardElement.videoConferenceElement": "Videokonferenz", + "components.cardElement.videoConferenceElement.label": + "Videokonferenz-Titel einfügen", "components.cardElement.deletedElement": "Gelöschter Inhalt", "components.cardElement.deletedElement.warning.externalToolElement": "Tool {toolName} nicht verfügbar. Bitte an Schuladministrator:in wenden.", @@ -472,6 +478,8 @@ export default { "components.elementTypeSelection.elements.submissionElement.subtitle": "Abgabe", "components.elementTypeSelection.elements.textElement.subtitle": "Text", + "components.elementTypeSelection.elements.videoConferenceElement.subtitle": + "Videokonferenz", "components.externalTools.status.deactivated": "Deaktiviert", "components.externalTools.status.incomplete": "Unvollständig", "components.externalTools.status.latest": "Aktuell", @@ -1494,6 +1502,14 @@ export default { "Lehrkraft erfolgreich angelegt!", "pages.administration.teachers.new.title": "Lehrkraft hinzufügen", "pages.administration.teachers.table.edit.ariaLabel": "Lehrkraft bearbeiten", + "pages.common.tools.configureVideoconferenceDialog.title": + "Videokonferenz erstellen", + "pages.common.tools.configureVideoconferenceDialog.text.allModeratorPermission": + "Alle Nutzer:innen nehmen als Moderator:in teil", + "pages.common.tools.configureVideoconferenceDialog.text.mute": + "Teilnehmer:innen beim Betreten stummschalten", + "pages.common.tools.configureVideoconferenceDialog.text.waitingRoom": + "Freigabe durch Moderator:in, bevor der Raum betreten werden kann", "pages.content._id.addToTopic": "Hinzufügen zu", "pages.content._id.collection.selectElements": "Wählen Sie die Elemente, die Sie zum Thema hinzufügen möchten", @@ -1562,14 +1578,6 @@ export default { "pages.courseRooms.tabLabel.groups": "Gruppen", "pages.courseRooms.tabLabel.tools": "Tools", "pages.courseRooms.tabLabel.toolsOld": "Tools", - "pages.courseRooms.tools.configureVideoconferenceDialog.title": - "Videokonferenz {roomName} erstellen", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.allModeratorPermission": - "Alle Nutzer:innen nehmen als Moderator:in teil", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.mute": - "Teilnehmer:innen beim Betreten stummschalten", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.waitingRoom": - "Freigabe durch Moderator:in, bevor der Raum betreten werden kann", "pages.files.overview.courseFiles": "Kurs-Dateien", "pages.files.overview.favorites": "Favoriten", "pages.files.overview.personalFiles": "Meine persönlichen Dateien", @@ -1809,6 +1817,10 @@ export default { "Die Videokonferenz wurde noch nicht gestartet oder du bist nicht berechtigt, an ihr teilzunehmen.", "pages.videoConference.info.notStarted": "Die Videokonferenz wurde noch nicht gestartet.", + "pages.videoConference.info.notEnabledParticipants": + "Das Element ist nicht mehr verfügbar. Bitte an die Lehrkraft wenden.", + "pages.videoConference.info.notEnabledTeacher": + "Das Element ist nicht mehr verfügbar. Bitte an den Admin wenden.", "pages.videoConference.title": "Videokonferenz BigBlueButton", "error.500": "Es gibt Probleme im System. Wir arbeiten daran, das Problem zu beheben. Bitte später erneut versuchen.", diff --git a/src/locales/en.ts b/src/locales/en.ts index e6a8d7ee50..9432425ba4 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -168,6 +168,7 @@ export default { "common.words.color.purple": "Purple", "common.words.color.red": "Red", "common.words.color.turquoise": "Turquoise", + "common.words.color.yellow": "Yellow", "common.words.copiedToClipboard": "Copied to the clipboard", "common.words.courseGroups": "Course Groups", "common.words.courses": "Courses", @@ -350,6 +351,7 @@ export default { "components.board.menu.fileElement": "File settings", "components.board.menu.linkElement": "Link settings", "components.board.menu.submissionElement": "Submission settings", + "components.board.menu.videoConferenceElement": "Video conference settings", "components.board.menu.deletedElement": "Deleted content settings", "components.board.notifications.errors.fileNameExists": "A file with this name already exists.", @@ -445,6 +447,9 @@ export default { "components.cardElement.titleElement.validation.required": "Please enter a title.", "components.cardElement.titleElement": "Title element", + "components.cardElement.videoConferenceElement": "Video conference", + "components.cardElement.videoConferenceElement.label": + "Insert video conference title", "components.cardElement.deletedElement": "Deleted content", "components.cardElement.deletedElement.warning.externalToolElement": "Tool {toolName} not available. Please contact the school administrator.", @@ -468,6 +473,8 @@ export default { "components.elementTypeSelection.elements.submissionElement.subtitle": "Submission", "components.elementTypeSelection.elements.textElement.subtitle": "Text", + "components.elementTypeSelection.elements.videoConferenceElement.subtitle": + "Video conference", "components.externalTools.status.deactivated": "Deactivated", "components.externalTools.status.incomplete": "Configuration incomplete", "components.externalTools.status.latest": "Latest", @@ -1472,6 +1479,14 @@ export default { "pages.administration.teachers.new.success": "Teacher successfully created!", "pages.administration.teachers.new.title": "Add teacher", "pages.administration.teachers.table.edit.ariaLabel": "Edit teacher", + "pages.common.tools.configureVideoconferenceDialog.title": + "Create video conference", + "pages.common.tools.configureVideoconferenceDialog.text.allModeratorPermission": + "All users participate as moderators", + "pages.common.tools.configureVideoconferenceDialog.text.mute": + "Mute participants when entering", + "pages.common.tools.configureVideoconferenceDialog.text.waitingRoom": + "Approval by the moderator before the room can be entered", "pages.content._id.addToTopic": "To be added to", "pages.content._id.collection.selectElements": "Select the items you want to add to the topic", @@ -1538,14 +1553,6 @@ export default { "pages.courseRooms.tabLabel.groups": "Groups", "pages.courseRooms.tabLabel.tools": "Tools", "pages.courseRooms.tabLabel.toolsOld": "Tools", - "pages.courseRooms.tools.configureVideoconferenceDialog.title": - "Create video conference {roomName}", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.allModeratorPermission": - "All users participate as moderators", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.mute": - "Mute participants when entering", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.waitingRoom": - "Approval by the moderator before the room can be entered", "pages.files.overview.courseFiles": "Course files", "pages.files.overview.favorites": "Favourites", "pages.files.overview.personalFiles": "My personal files", @@ -1776,6 +1783,10 @@ export default { "The video conference hasn't started yet or you don't have permission to join it.", "pages.videoConference.info.notStarted": "The video conference hasn't started yet.", + "pages.videoConference.info.notEnabledParticipants": + "The element is no longer available. Please contact the teacher.", + "pages.videoConference.info.notEnabledTeacher": + "The element is no longer available. Please contact the admin.", "pages.videoConference.title": "Video conference BigBlueButton", "error.500": "There are problems in the system. We are working on fixing the issue. Please try again later.", diff --git a/src/locales/es.ts b/src/locales/es.ts index 53935427e8..de782f0aac 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -170,6 +170,7 @@ export default { "common.words.color.purple": "Violeta", "common.words.color.red": "Roja", "common.words.color.turquoise": "Turquesa", + "common.words.color.yellow": "Amarillo", "common.words.copiedToClipboard": "Copiado en el portapapeles", "common.words.courseGroups": "grupos de cursos", "common.words.courses": "Cursos", @@ -357,6 +358,8 @@ export default { "components.board.menu.fileElement": "Configuración del archivo", "components.board.menu.linkElement": "Configuración del enlace", "components.board.menu.submissionElement": "Configuración del envío", + "components.board.menu.videoConferenceElement": + "Configuración de la videoconferencia", "components.board.menu.deletedElement": "Configuración de contenido eliminado", "components.board.notifications.errors.fileNameExists": @@ -453,6 +456,9 @@ export default { "components.cardElement.titleElement.validation.required": "Por favor ingrese un título.", "components.cardElement.titleElement": "Elemento título", + "components.cardElement.videoConferenceElement": "Videoconferencia", + "components.cardElement.videoConferenceElement.label": + "Insertar título de la videoconferencia", "components.cardElement.deletedElement": "Contenido eliminado", "components.cardElement.deletedElement.warning.externalToolElement": "La herramienta {toolName} no está disponible. Por favor comuníquese con el administrador de la escuela.", @@ -477,6 +483,8 @@ export default { "components.elementTypeSelection.elements.submissionElement.subtitle": "Envíos", "components.elementTypeSelection.elements.textElement.subtitle": "Texto", + "components.elementTypeSelection.elements.videoConferenceElement.subtitle": + "Videoconferencia", "components.externalTools.status.deactivated": "Desactivado", "components.externalTools.status.incomplete": "Configuración incompleta", "components.externalTools.status.latest": "Actual", @@ -1517,6 +1525,14 @@ export default { "¡Profesor creado correctamente!", "pages.administration.teachers.new.title": "Añadir profesor", "pages.administration.teachers.table.edit.ariaLabel": "Editar profesor", + "pages.common.tools.configureVideoconferenceDialog.title": + "Crear videoconferencia", + "pages.common.tools.configureVideoconferenceDialog.text.allModeratorPermission": + "Todas las usuarias participan como moderadoras", + "pages.common.tools.configureVideoconferenceDialog.text.mute": + "Silenciar a las participantes al entrar", + "pages.common.tools.configureVideoconferenceDialog.text.waitingRoom": + "Aprobación del moderador antes de poder ingresar a la sala", "pages.content._id.addToTopic": "Para ser añadido a", "pages.content._id.collection.selectElements": "Selecciona los elementos que deses añadir al tema", @@ -1584,14 +1600,6 @@ export default { "pages.courseRooms.tabLabel.groups": "Grupos", "pages.courseRooms.tabLabel.tools": "Herramientas", "pages.courseRooms.tabLabel.toolsOld": "Herramientas", - "pages.courseRooms.tools.configureVideoconferenceDialog.title": - "Crear videoconferencia {roomName}", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.allModeratorPermission": - "Todas las usuarias participan como moderadoras", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.mute": - "Silenciar a las participantes al entrar", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.waitingRoom": - "Aprobación del moderador antes de poder ingresar a la sala", "pages.files.overview.courseFiles": "Archivos del curso", "pages.files.overview.favorites": "Favoritos", "pages.files.overview.personalFiles": "Archivos personales", @@ -1827,6 +1835,10 @@ export default { "La videoconferencia aún no ha comenzado o no tienes permiso para unirte.", "pages.videoConference.info.notStarted": "La videoconferencia aún no ha comenzado.", + "pages.videoConference.info.notEnabledParticipants": + "El elemento ya no está disponible. Póngase en contacto con el profesor.", + "pages.videoConference.info.notEnabledTeacher": + "El elemento ya no está disponible. Póngase en contacto con el administrador.", "pages.videoConference.title": "Videoconferencia BigBlueButton", "error.500": "Hay problemas en el sistema. Estamos trabajando para solucionar el problema. Vuelva a intentarlo más tarde.", diff --git a/src/locales/uk.ts b/src/locales/uk.ts index 1429cd4309..af0bfd3c40 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -170,6 +170,7 @@ export default { "common.words.color.purple": "Фіалка", "common.words.color.red": "Червоний", "common.words.color.turquoise": "Бірюза", + "common.words.color.yellow": "Жовтий", "common.words.copiedToClipboard": "Скопійовано в буфер обміну", "common.words.courseGroups": "курсові групи", "common.words.courses": "Мій курс", @@ -358,6 +359,8 @@ export default { "components.board.menu.fileElement": "Налаштування файлу", "components.board.menu.linkElement": "Налаштування посилання", "components.board.menu.submissionElement": "Налаштування Подання", + "components.board.menu.videoConferenceElement": + "Налаштування відеоконференція", "components.board.menu.deletedElement": "Видалені налаштування вмісту", "components.board.notifications.errors.fileNameExists": "Файл з такою назвою вже існує.", @@ -454,6 +457,9 @@ export default { "components.cardElement.titleElement.validation.required": "Будь ласка, введіть назву.", "components.cardElement.titleElement": "Елемент заголовка", + "components.cardElement.videoConferenceElement": "Відеоконференція", + "components.cardElement.videoConferenceElement.label": + "Вставити назву відеоконференції", "components.cardElement.deletedElement": "Видалений вміст", "components.cardElement.deletedElement.warning.externalToolElement": "Інструмент {toolName} недоступний. Будь ласка, зверніться до адміністратора школи.", @@ -476,6 +482,8 @@ export default { "components.elementTypeSelection.elements.submissionElement.subtitle": "Подання", "components.elementTypeSelection.elements.textElement.subtitle": "Текст", + "components.elementTypeSelection.elements.videoConferenceElement.subtitle": + "Відеоконференція", "components.externalTools.status.deactivated": "Деактивовано", "components.externalTools.status.incomplete": "Конфігурація не завершена", "components.externalTools.status.latest": "Останній", @@ -1500,6 +1508,14 @@ export default { "pages.administration.teachers.new.success": "Викладача успішно створено!", "pages.administration.teachers.new.title": "Додати викладача", "pages.administration.teachers.table.edit.ariaLabel": "Редагування вчителя", + "pages.common.tools.configureVideoconferenceDialog.title": + "Створити відеоконференцію", + "pages.common.tools.configureVideoconferenceDialog.text.allModeratorPermission": + "Усі користувачі беруть участь як модератори", + "pages.common.tools.configureVideoconferenceDialog.text.mute": + "Вимкнення звуку учасників при вході", + "pages.common.tools.configureVideoconferenceDialog.text.waitingRoom": + "Схвалення модератором перед входом до кімнати", "pages.content._id.addToTopic": "Для додавання в", "pages.content._id.collection.selectElements": "Виберіть елементи, які треба додати до теми", @@ -1566,14 +1582,6 @@ export default { "pages.courseRooms.tabLabel.groups": "Групи", "pages.courseRooms.tabLabel.tools": "Інструмент", "pages.courseRooms.tabLabel.toolsOld": "Інструмент", - "pages.courseRooms.tools.configureVideoconferenceDialog.title": - "Створити відеоконференцію {roomName}", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.allModeratorPermission": - "Усі користувачі беруть участь як модератори", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.mute": - "Вимкнення звуку учасників при вході", - "pages.courseRooms.tools.configureVideoconferenceDialog.text.waitingRoom": - "Схвалення модератором перед входом до кімнати", "pages.files.overview.courseFiles": "Файли курсу", "pages.files.overview.favorites": "Обрані", "pages.files.overview.personalFiles": "Мої особисті справи", @@ -1803,6 +1811,10 @@ export default { "pages.videoConference.info.noPermission": "Відеоконференція ще не почалася або у вас немає дозволу приєднатися до неї.", "pages.videoConference.info.notStarted": "Відеоконференція ще не почалася.", + "pages.videoConference.info.notEnabledParticipants": + "Елемент більше не доступний. Будь ласка, зверніться до викладача.", + "pages.videoConference.info.notEnabledTeacher": + "Елемент більше не доступний. Будь ласка, зверніться з адміністратором.", "pages.videoConference.title": "Відеоконференція BigBlueButton", "error.500": "Є проблеми в системі. Ми працюємо над усуненням проблеми. Будь ласка, спробуйте пізніше.", diff --git a/src/modules/data/board/Board.store.ts b/src/modules/data/board/Board.store.ts index e06a91adba..eeb3a9ed6f 100644 --- a/src/modules/data/board/Board.store.ts +++ b/src/modules/data/board/Board.store.ts @@ -324,7 +324,7 @@ export const useBoardStore = defineStore("boardStore", () => { const deleteBoardSuccess = (payload: DeleteBoardSuccessPayload) => { if (payload.isOwnAction === true) { router.replace({ - name: "rooms-id", + name: "room-details", params: { id: roomId.value }, }); return; diff --git a/src/modules/data/board/Board.store.unit.ts b/src/modules/data/board/Board.store.unit.ts index d5a62b912f..2bdc64d6e2 100644 --- a/src/modules/data/board/Board.store.unit.ts +++ b/src/modules/data/board/Board.store.unit.ts @@ -1196,7 +1196,7 @@ describe("BoardStore", () => { }); expect(router.replace).toHaveBeenCalledWith({ - name: "rooms-id", + name: "room-details", params: { id: "roomId" }, }); }); diff --git a/src/modules/data/board/BoardApi.composable.ts b/src/modules/data/board/BoardApi.composable.ts index f3e1d06ce2..033fdfaedc 100644 --- a/src/modules/data/board/BoardApi.composable.ts +++ b/src/modules/data/board/BoardApi.composable.ts @@ -17,6 +17,7 @@ import { RichTextElementContentBody, RoomApiFactory, SubmissionContainerElementContentBody, + VideoConferenceElementContentBody, } from "@/serverApi/v3"; import { BoardContextType } from "@/types/board/BoardContext"; import { AnyContentElement } from "@/types/board/ContentElement"; @@ -124,6 +125,13 @@ export const useBoardApi = () => { type: ContentElementType.Drawing, }; } + + if (element.type === ContentElementType.VideoConference) { + return { + content: element.content, + type: ContentElementType.VideoConference, + } as VideoConferenceElementContentBody; + } throw new Error("element.type mapping is undefined for updateElementCall"); }; diff --git a/src/modules/data/board/BoardApi.composable.unit.ts b/src/modules/data/board/BoardApi.composable.unit.ts index 2d6a275dd5..56a896b404 100644 --- a/src/modules/data/board/BoardApi.composable.unit.ts +++ b/src/modules/data/board/BoardApi.composable.unit.ts @@ -267,6 +267,29 @@ describe("BoardApi.composable", () => { ); }); + it("should call elementControllerUpdateElement api with VideoConferenceElement", async () => { + const { updateElementCall } = useBoardApi(); + const payload: serverApi.VideoConferenceElementResponse = { + id: "video-conference-element-id", + type: ContentElementType.VideoConference, + content: { + title: "Some title", + }, + timestamps: timestampsResponseFactory.build(), + }; + const data = { + content: payload.content, + type: ContentElementType.VideoConference, + }; + + await updateElementCall(payload); + + expect(elementApi.elementControllerUpdateElement).toHaveBeenCalledWith( + payload.id, + { data } + ); + }); + it("should throw error for unkown element type", async () => { const { updateElementCall } = useBoardApi(); const payload = { diff --git a/src/modules/data/room/RoomCreate.state.ts b/src/modules/data/room/RoomCreate.state.ts index 12d6e390e8..8a1c607a13 100644 --- a/src/modules/data/room/RoomCreate.state.ts +++ b/src/modules/data/room/RoomCreate.state.ts @@ -1,5 +1,5 @@ -import { RoomApiFactory, RoomColor } from "@/serverApi/v3"; -import { RoomCreateParams, RoomItem } from "@/types/room/Room"; +import { RoomApiFactory } from "@/serverApi/v3"; +import { RoomCreateParams, RoomItem, RoomColorEnum } from "@/types/room/Room"; import { $axios, mapAxiosErrorToResponseError } from "@/utils/api"; import { ref } from "vue"; @@ -9,7 +9,7 @@ export const useRoomCreateState = () => { const roomData = ref({ name: "", - color: RoomColor.BlueGrey, + color: RoomColorEnum.BlueGrey, startDate: undefined, endDate: undefined, }); diff --git a/src/modules/data/room/RoomCreate.state.unit.ts b/src/modules/data/room/RoomCreate.state.unit.ts index 8f17f278b3..f6613980d2 100644 --- a/src/modules/data/room/RoomCreate.state.unit.ts +++ b/src/modules/data/room/RoomCreate.state.unit.ts @@ -6,7 +6,7 @@ import { useApplicationError } from "@/composables/application-error.composable" import { initializeAxios, mapAxiosErrorToResponseError } from "@/utils/api"; import setupStores from "@@/tests/test-utils/setupStores"; import ApplicationErrorModule from "@/store/application-error"; -import { RoomCreateParams } from "@/types/room/Room"; +import { RoomCreateParams, RoomColorEnum } from "@/types/room/Room"; import { ref } from "vue"; import { apiResponseErrorFactory, @@ -73,7 +73,7 @@ describe("useRoomCreateState", () => { describe("createRoom", () => { const roomData = ref({ name: "Room 1", - color: serverApi.RoomColor.BlueGrey, + color: RoomColorEnum.BlueGrey, startDate: undefined, endDate: undefined, }); diff --git a/src/modules/data/room/RoomDetails.store.ts b/src/modules/data/room/RoomDetails.store.ts index 02a288c5a0..af82c1c601 100644 --- a/src/modules/data/room/RoomDetails.store.ts +++ b/src/modules/data/room/RoomDetails.store.ts @@ -1,7 +1,13 @@ import { RoomBoardItem, RoomDetails } from "@/types/room/Room"; import { ref } from "vue"; import { defineStore } from "pinia"; -import { RoomApiFactory } from "@/serverApi/v3"; +import { + BoardApiFactory, + BoardLayout, + BoardParentType, + CreateBoardBodyParams, + RoomApiFactory, +} from "@/serverApi/v3"; import { $axios, mapAxiosErrorToResponseError } from "@/utils/api"; import { createApplicationError } from "@/utils/create-application-error.factory"; @@ -17,6 +23,7 @@ export const useRoomDetailsStore = defineStore("roomDetailsStore", () => { const roomBoards = ref([]); const roomApi = RoomApiFactory(undefined, "/v3", $axios); + const boardApi = BoardApiFactory(undefined, "/v3", $axios); const fetchRoom = async (id: string) => { try { @@ -38,6 +45,22 @@ export const useRoomDetailsStore = defineStore("roomDetailsStore", () => { } }; + const createBoard = async ( + roomId: string, + layout: BoardLayout, + title: string + ) => { + const params: CreateBoardBodyParams = { + title: title, + parentId: roomId, + parentType: BoardParentType.Room, + layout, + }; + const boardId = (await boardApi.boardControllerCreateBoard(params)).data.id; + + return boardId; + }; + const resetState = () => { isLoading.value = true; room.value = undefined; @@ -51,6 +74,7 @@ export const useRoomDetailsStore = defineStore("roomDetailsStore", () => { return { deactivateRoom, fetchRoom, + createBoard, isLoading, resetState, room, diff --git a/src/modules/data/room/RoomDetails.store.unit.ts b/src/modules/data/room/RoomDetails.store.unit.ts index ae0bca7fa4..b14e8b6d23 100644 --- a/src/modules/data/room/RoomDetails.store.unit.ts +++ b/src/modules/data/room/RoomDetails.store.unit.ts @@ -8,6 +8,7 @@ import { initializeAxios, mapAxiosErrorToResponseError } from "@/utils/api"; import { apiResponseErrorFactory, axiosErrorFactory, + mockApiResponse, } from "@@/tests/test-utils"; jest.mock("@/utils/api"); @@ -35,12 +36,14 @@ const setupErrorResponse = (message = "NOT_FOUND", code = 404) => { describe("useRoomDetailsStore", () => { let roomApiMock: DeepMocked; + let boardApiMock: DeepMocked; let axiosMock: DeepMocked; let mockedCreateApplicationErrorCalls: ReturnType; beforeEach(() => { setActivePinia(createPinia()); roomApiMock = createMock(); + boardApiMock = createMock(); axiosMock = createMock(); mockedCreateApplicationErrorCalls = createMock>(); @@ -49,6 +52,7 @@ describe("useRoomDetailsStore", () => { ); jest.spyOn(serverApi, "RoomApiFactory").mockReturnValue(roomApiMock); + jest.spyOn(serverApi, "BoardApiFactory").mockReturnValue(boardApiMock); initializeAxios(axiosMock); }); @@ -135,4 +139,30 @@ describe("useRoomDetailsStore", () => { expect(store.room).toBeUndefined(); }); }); + + describe("createBoard", () => { + it("should call createBoard api", async () => { + const { store } = setup(); + const boardId = "board-id"; + const roomId = "room-id"; + const layout = serverApi.BoardLayout.Columns; + const title = "title"; + + boardApiMock.boardControllerCreateBoard.mockResolvedValue( + mockApiResponse({ + data: { id: boardId }, + }) + ); + + const result = await store.createBoard(roomId, layout, title); + + expect(result).toBe(boardId); + expect(boardApiMock.boardControllerCreateBoard).toHaveBeenCalledWith({ + title, + parentId: roomId, + parentType: serverApi.BoardParentType.Room, + layout, + }); + }); + }); }); diff --git a/src/modules/data/room/RoomEdit.state.ts b/src/modules/data/room/RoomEdit.state.ts index 6ba2853c07..5dcac87131 100644 --- a/src/modules/data/room/RoomEdit.state.ts +++ b/src/modules/data/room/RoomEdit.state.ts @@ -1,5 +1,9 @@ -import { RoomApiFactory, RoomColor } from "@/serverApi/v3"; -import { RoomDetails, RoomUpdateParams } from "@/types/room/Room"; +import { RoomApiFactory } from "@/serverApi/v3"; +import { + RoomDetails, + RoomUpdateParams, + RoomColorEnum, +} from "@/types/room/Room"; import { $axios, mapAxiosErrorToResponseError } from "@/utils/api"; import { createApplicationError } from "@/utils/create-application-error.factory"; import { ref } from "vue"; @@ -19,7 +23,7 @@ export const useRoomEditState = () => { const roomData = ref({ name: "", - color: RoomColor.BlueGrey, + color: RoomColorEnum.BlueGrey, startDate: undefined, endDate: undefined, }); diff --git a/src/modules/data/room/RoomEdit.state.unit.ts b/src/modules/data/room/RoomEdit.state.unit.ts index 52bf187f5b..fc1cc125b4 100644 --- a/src/modules/data/room/RoomEdit.state.unit.ts +++ b/src/modules/data/room/RoomEdit.state.unit.ts @@ -10,6 +10,7 @@ import { apiResponseErrorFactory, axiosErrorFactory, } from "@@/tests/test-utils"; +import { RoomColorEnum } from "@/types/room/Room"; jest.mock("@/utils/api"); const mockedMapAxiosErrorToResponseError = jest.mocked( @@ -105,7 +106,7 @@ describe("useRoomEditState", () => { expect(isLoading.value).toBe(true); const params = { name: "room-name", - color: serverApi.RoomColor.BlueGrey, + color: RoomColorEnum.BlueGrey, }; await updateRoom("room-id", params); @@ -123,7 +124,7 @@ describe("useRoomEditState", () => { const { updateRoom, isLoading } = setup(); const params = { name: "room-name", - color: serverApi.RoomColor.BlueGrey, + color: RoomColorEnum.BlueGrey, }; roomApiMock.roomControllerUpdateRoom.mockRejectedValue({ code: 404 }); diff --git a/src/modules/data/room/Rooms.state.unit.ts b/src/modules/data/room/Rooms.state.unit.ts index b5ad2d8f0f..09440ba79b 100644 --- a/src/modules/data/room/Rooms.state.unit.ts +++ b/src/modules/data/room/Rooms.state.unit.ts @@ -10,6 +10,7 @@ import { apiResponseErrorFactory, axiosErrorFactory, } from "@@/tests/test-utils"; +import { RoomColorEnum } from "@/types/room/Room"; jest.mock("@/utils/api"); const mockedMapAxiosErrorToResponseError = jest.mocked( @@ -125,7 +126,7 @@ describe("useRoomsState", () => { { id: "1", name: "Room 1", - color: serverApi.RoomColor.BlueGrey, + color: RoomColorEnum.BlueGrey, schoolId: "6749dd4e657d98af622e370c", createdAt: "2024.11.18", updatedAt: "2024.11.18", diff --git a/src/modules/data/room/roomMembers/roomMembers.composable.ts b/src/modules/data/room/roomMembers/roomMembers.composable.ts index f121d8bde3..d500b19ea3 100644 --- a/src/modules/data/room/roomMembers/roomMembers.composable.ts +++ b/src/modules/data/room/roomMembers/roomMembers.composable.ts @@ -6,8 +6,6 @@ import { SchoolApiFactory, RoomMemberResponse, SchoolForExternalInviteResponse, - UserIdAndRole, - UserIdAndRoleRoleNameEnum, } from "@/serverApi/v3"; import { $axios } from "@/utils/api"; import { useI18n } from "vue-i18n"; @@ -112,15 +110,8 @@ export const useRoomMembers = (roomId: string) => { userIds.includes(member.userId) ); - const userIdsAndRoles: UserIdAndRole[] = newMembers.map((member) => ({ - userId: member.userId, - roleName: UserIdAndRoleRoleNameEnum.Roomeditor, - })); - try { - await roomApi.roomControllerAddMembers(roomId, { - userIdsAndRoles, - }); + await roomApi.roomControllerAddMembers(roomId, { userIds }); roomMembers.value.push( ...newMembers.map((member) => ({ ...member, diff --git a/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts b/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts index fa066e906b..cfe59bb320 100644 --- a/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts +++ b/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts @@ -16,7 +16,6 @@ import { RoleName, RoomMemberResponse, SchoolUserListResponse, - UserIdAndRoleRoleNameEnum, } from "@/serverApi/v3/api"; import { useBoardNotifier } from "@util-board"; import { schoolsModule, authModule } from "@/store"; @@ -278,12 +277,7 @@ describe("useRoomMembers", () => { expect(roomApiMock.roomControllerAddMembers).toHaveBeenCalledWith( roomId, { - userIdsAndRoles: [ - { - userId: firstPotentialMember.userId, - roleName: UserIdAndRoleRoleNameEnum.Roomeditor, - }, - ], + userIds: [firstPotentialMember.userId], } ); expect(roomMembers.value).toEqual([ diff --git a/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElement.unit.ts b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElement.unit.ts new file mode 100644 index 0000000000..991d498b12 --- /dev/null +++ b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElement.unit.ts @@ -0,0 +1,609 @@ +import { + AUTH_MODULE_KEY, + ENV_CONFIG_MODULE_KEY, + NOTIFIER_MODULE_KEY, + VIDEO_CONFERENCE_MODULE_KEY, +} from "@/utils/inject"; + +import { videoConferenceElementResponseFactory } from "@@/tests/test-utils/factory/videoConferenceElementResponseFactory"; +import { + useBoardFocusHandler, + useBoardPermissions, + useContentElementState, +} from "@data-board"; +import { createMock, DeepMocked } from "@golevelup/ts-jest"; +import { shallowMount } from "@vue/test-utils"; +import { computed, ref } from "vue"; +import NotifierModule from "@/store/notifier"; +import { createModuleMocks } from "@@/tests/test-utils/mock-store-module"; +import { + ConfigResponse, + VideoConferenceElementContent, + VideoConferenceScope, +} from "@/serverApi/v3/api"; +import VideoConferenceContentElementDisplay from "./VideoConferenceContentElementDisplay.vue"; +import VideoConferenceContentElementCreate from "./VideoConferenceContentElementCreate.vue"; +import { videoConferenceElementContentFactory } from "@@/tests/test-utils/factory/videoConferenceElementContentFactory"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { + BoardMenu, + BoardMenuActionDelete, + BoardMenuActionMoveDown, + BoardMenuActionMoveUp, +} from "@ui-board"; +import { VideoConferenceContentElement } from "@feature-board-video-conference-element"; +import AuthModule from "@/store/auth"; +import VideoConferenceModule from "@/store/video-conference"; +import EnvConfigModule from "@/store/env-config"; +import { Router, useRoute, useRouter } from "vue-router"; +import { VideoConferenceState } from "@/store/types/video-conference"; +import { VDialog } from "vuetify/lib/components/index.mjs"; + +jest.mock("@data-board/ContentElementState.composable"); +jest.mock("@data-board/BoardFocusHandler.composable"); +jest.mock("@data-board/BoardPermissions.composable"); + +window.open = jest.fn(); + +jest.mock("vue-router"); +const useRouterMock = useRouter; +const useRouteMock = useRoute; +useRouteMock.mockReturnValue({ params: { id: "room-id" } }); + +const mockedUseContentElementState = jest.mocked(useContentElementState); + +let defaultElement = videoConferenceElementResponseFactory.build(); +const mockedEnvConfigModule = createModuleMocks(EnvConfigModule, { + getEnv: createMock({ + FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED: true, + }), +}); + +describe("VideoConferenceContentElement", () => { + let router: DeepMocked; + let route: DeepMocked>; + let useBoardFocusHandlerMock: DeepMocked< + ReturnType + >; + let useBoardPermissionsMock: DeepMocked< + ReturnType + >; + + beforeEach(() => { + route = createMock>(); + useRouteMock.mockReturnValue(route); + useRouteMock.mockReturnValue({ params: { id: "room-id" } }); + + router = createMock(); + useRouterMock.mockReturnValue(router); + + useBoardFocusHandlerMock = + createMock>(); + useBoardPermissionsMock = createMock< + ReturnType + >({ + isTeacher: true, + hasMovePermission: false, + hasCreateCardPermission: false, + hasCreateColumnPermission: false, + hasEditPermission: false, + hasDeletePermission: false, + isStudent: false, + }); + + jest.mocked(useBoardFocusHandler).mockReturnValue(useBoardFocusHandlerMock); + + jest.mocked(useBoardPermissions).mockReturnValue(useBoardPermissionsMock); + defaultElement = videoConferenceElementResponseFactory.build(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const setupWrapper = ( + options: { + content?: VideoConferenceElementContent; + isEditMode: boolean; + role?: "teacher" | "student"; + columnIndex?: number; + rowIndex?: number; + elementIndex?: number; + videoConferenceModuleGetter?: Partial; + } = { + content: undefined, + isEditMode: true, + role: "teacher", + columnIndex: 0, + rowIndex: 1, + elementIndex: 2, + } + ) => { + const { + content, + isEditMode, + role = "teacher", + columnIndex = 0, + rowIndex = 1, + elementIndex = 2, + videoConferenceModuleGetter, + } = options; + + const element = { + ...defaultElement, + content: videoConferenceElementContentFactory.build({ + title: undefined, + ...content, + }), + }; + + mockedUseContentElementState.mockReturnValue({ + modelValue: ref(element.content), + computedElement: computed(() => element), + isLoading: ref(false), + }); + + const notifierModule = createModuleMocks(NotifierModule); + const authModule = createModuleMocks(AuthModule, { + getUserRoles: [role], + }); + + const videoConferenceModule = createModuleMocks(VideoConferenceModule, { + getVideoConferenceInfo: { + state: VideoConferenceState.NOT_STARTED, + options: { + everyAttendeeJoinsMuted: false, + moderatorMustApproveJoinRequests: false, + everybodyJoinsAsModerator: false, + }, + }, + getLoading: false, + ...videoConferenceModuleGetter, + }); + + const joinVideoConferenceMock = jest + .fn() + .mockResolvedValueOnce({ url: "https://example.com" }); + videoConferenceModule.joinVideoConference = joinVideoConferenceMock; + + const wrapper = shallowMount(VideoConferenceContentElement, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + provide: { + [NOTIFIER_MODULE_KEY.valueOf()]: notifierModule, + [ENV_CONFIG_MODULE_KEY.valueOf()]: mockedEnvConfigModule, + [AUTH_MODULE_KEY.valueOf()]: authModule, + [VIDEO_CONFERENCE_MODULE_KEY.valueOf()]: videoConferenceModule, + }, + }, + props: { + element, + isEditMode, + columnIndex, + rowIndex, + elementIndex, + }, + }); + + return { + element, + videoConferenceModule, + wrapper, + }; + }; + + describe("when video conference element is displayed", () => { + describe("and content title is undefined", () => { + it("should not render display of video conference content", () => { + const { wrapper } = setupWrapper({ + isEditMode: false, + }); + + const videoConferenceElementDisplay = wrapper.findComponent( + VideoConferenceContentElementDisplay + ); + + expect(videoConferenceElementDisplay.exists()).toBe(false); + }); + + it("should not render video conference element menu", () => { + const { wrapper } = setupWrapper({ + isEditMode: false, + }); + + const videoConferenceElementMenu = wrapper.findComponent(BoardMenu); + + expect(videoConferenceElementMenu.exists()).toBe(false); + }); + }); + + describe("and content title is defined", () => { + it("should render display of video conference content with correct props", () => { + const videoConferenceElementContent = + videoConferenceElementContentFactory.build({ title: "test-title" }); + const { wrapper, element } = setupWrapper({ + content: videoConferenceElementContent, + isEditMode: false, + }); + + const videoConferenceElementDisplay = wrapper.findComponent( + VideoConferenceContentElementDisplay + ); + + expect(videoConferenceElementDisplay.props().title).toEqual( + element.content.title + ); + expect(videoConferenceElementDisplay.props().isEditMode).toEqual(false); + expect(videoConferenceElementDisplay.props().canStart).toEqual(true); + expect( + videoConferenceElementDisplay.props().hasParticipationPermission + ).toEqual(true); + expect(videoConferenceElementDisplay.props().isRunning).toEqual(false); + }); + + it("should have the correct aria-label", () => { + const videoConferenceElementContent = + videoConferenceElementContentFactory.build(); + const { wrapper } = setupWrapper({ + content: videoConferenceElementContent, + isEditMode: false, + }); + + const videoConferenceElement = wrapper.findComponent( + '[data-testid="video-conference-element"]' + ); + + expect(videoConferenceElement.attributes("aria-label")).toEqual( + "components.cardElement.videoConferenceElement, common.ariaLabel.newTab" + ); + }); + + describe("and element is in edit mode", () => { + it.each(["up", "down"])( + "should 'emit move-keyboard:edit' when arrow key %s is pressed", + async (key) => { + const videoConferenceElementContent = + videoConferenceElementContentFactory.build(); + const { wrapper } = setupWrapper({ + content: videoConferenceElementContent, + isEditMode: true, + }); + + const videoConferenceElement = wrapper.findComponent( + '[data-testid="video-conference-element"]' + ); + + await videoConferenceElement.trigger(`keydown.${key}`); + + expect(wrapper.emitted()).toHaveProperty("move-keyboard:edit"); + } + ); + }); + + describe("and element is in view mode", () => { + it.each(["up", "down"])( + "should not 'emit move-keyboard:edit' when arrow key %s is pressed", + async (key) => { + const videoConferenceElementContent = + videoConferenceElementContentFactory.build(); + const { wrapper } = setupWrapper({ + content: videoConferenceElementContent, + isEditMode: false, + }); + + const videoConferenceElement = wrapper.findComponent( + '[data-testid="video-conference-element"]' + ); + + await videoConferenceElement.trigger(`keydown.${key}`); + + expect(wrapper.emitted()).not.toHaveProperty("move-keyboard:edit"); + } + ); + }); + + describe("video conference element menu", () => { + it("should render video conference element menu", () => { + const videoConferenceElementContent = + videoConferenceElementContentFactory.build(); + const { wrapper } = setupWrapper({ + content: videoConferenceElementContent, + isEditMode: true, + }); + + const videoConferenceElementMenu = wrapper.findComponent(BoardMenu); + + expect(videoConferenceElementMenu.exists()).toBe(true); + }); + + it("should emit 'move-down:edit' event when move down menu item is clicked", async () => { + const videoConferenceElementContent = + videoConferenceElementContentFactory.build(); + const { wrapper } = setupWrapper({ + content: videoConferenceElementContent, + isEditMode: true, + }); + + const menuItem = wrapper.findComponent(BoardMenuActionMoveDown); + await menuItem.trigger("click"); + + expect(wrapper.emitted()).toHaveProperty("move-down:edit"); + }); + + it("should emit 'move-up:edit' event when move up menu item is clicked", async () => { + const videoConferenceElementContent = + videoConferenceElementContentFactory.build(); + const { wrapper } = setupWrapper({ + content: videoConferenceElementContent, + isEditMode: true, + }); + + const menuItem = wrapper.findComponent(BoardMenuActionMoveUp); + await menuItem.trigger("click"); + + expect(wrapper.emitted()).toHaveProperty("move-up:edit"); + }); + + it("should emit 'delete:element' event when delete menu item is clicked", async () => { + const videoConferenceElementContent = + videoConferenceElementContentFactory.build(); + const { wrapper } = setupWrapper({ + content: videoConferenceElementContent, + isEditMode: true, + }); + + const menuItem = wrapper.findComponent(BoardMenuActionDelete); + await menuItem.trigger("click"); + + expect(wrapper.emitted()).toHaveProperty("delete:element"); + }); + }); + + describe("onElementClick", () => { + describe("and video conference is not running", () => { + it("should open the configuration dialog", async () => { + const { wrapper } = setupWrapper({ + content: videoConferenceElementContentFactory.build(), + isEditMode: false, + }); + + const videoConferenceElementDisplay = wrapper.findComponent( + VideoConferenceContentElementDisplay + ); + await videoConferenceElementDisplay.vm.$emit("click"); + + const configurationDialog = wrapper.findComponent( + '[data-testid="videoconference-config-dialog"]' + ); + + expect(configurationDialog.props("modelValue")).toBe(true); + }); + }); + + describe("and video conference is running", () => { + it("should call joinVideoConference", async () => { + const { element, videoConferenceModule, wrapper } = setupWrapper({ + content: videoConferenceElementContentFactory.build(), + isEditMode: false, + videoConferenceModuleGetter: { + getVideoConferenceInfo: { + state: VideoConferenceState.RUNNING, + options: { + everyAttendeeJoinsMuted: false, + moderatorMustApproveJoinRequests: false, + everybodyJoinsAsModerator: false, + }, + }, + getLoading: true, + }, + }); + + const videoConferenceElementDisplay = wrapper.findComponent( + VideoConferenceContentElementDisplay + ); + await videoConferenceElementDisplay.vm.$emit("click"); + + expect( + videoConferenceModule.joinVideoConference + ).toHaveBeenCalledWith({ + scope: VideoConferenceScope.VideoConferenceElement, + scopeId: element.id, + }); + }); + }); + }); + + describe("onRefresh", () => { + it("should call fetchVideoConferenceInfo", async () => { + const { element, videoConferenceModule, wrapper } = setupWrapper({ + content: videoConferenceElementContentFactory.build(), + isEditMode: false, + }); + + const videoConferenceElementDisplay = wrapper.findComponent( + VideoConferenceContentElementDisplay + ); + await videoConferenceElementDisplay.vm.$emit("refresh"); + + expect( + videoConferenceModule.fetchVideoConferenceInfo + ).toHaveBeenCalledWith({ + scope: VideoConferenceScope.VideoConferenceElement, + scopeId: element.id, + }); + }); + }); + }); + }); + + describe("when video conference element is being created", () => { + describe("and no title was entered", () => { + it("should hide video conference element in view mode", () => { + const { wrapper } = setupWrapper({ + isEditMode: false, + }); + + const videoConferenceElement = wrapper.findComponent( + '[data-testid="video-conference-element"]' + ); + + expect(videoConferenceElement.attributes("class")).toContain("d-none"); + }); + + it("should not render video conference element menu in view mode", () => { + const { wrapper } = setupWrapper({ + isEditMode: false, + }); + + const videoConferenceElementMenu = wrapper.findComponent(BoardMenu); + + expect(videoConferenceElementMenu.exists()).toBe(false); + }); + }); + + describe("and element is in edit mode", () => { + it("should render VideoConferenceContentElementCreate component", () => { + const { wrapper } = setupWrapper({ isEditMode: true }); + + const videoConferenceCreateComponent = wrapper.findComponent( + VideoConferenceContentElementCreate + ); + + expect(videoConferenceCreateComponent.exists()).toBe(true); + }); + + it.each(["up", "down"])( + "should not 'emit move-keyboard:edit' when arrow key %s is pressed", + async (key) => { + const { wrapper } = setupWrapper({ + isEditMode: true, + }); + + const videoConferenceElement = wrapper.findComponent( + '[data-testid="video-conference-element"]' + ); + + await videoConferenceElement.trigger(`keydown.${key}`); + + expect(wrapper.emitted()).not.toHaveProperty("move-keyboard:edit"); + } + ); + + describe("video conference element menu", () => { + it("should render video conference element menu", () => { + const { wrapper } = setupWrapper({ + isEditMode: true, + }); + + const videoConferenceElementMenu = wrapper.findComponent(BoardMenu); + + expect(videoConferenceElementMenu.exists()).toBe(true); + }); + + it("should emit 'move-down:edit' event when move down menu item is clicked", async () => { + const { wrapper } = setupWrapper({ + isEditMode: true, + }); + + const menuItem = wrapper.findComponent(BoardMenuActionMoveDown); + await menuItem.trigger("click"); + + expect(wrapper.emitted()).toHaveProperty("move-down:edit"); + }); + + it("should emit 'move-up:edit' event when move up menu item is clicked", async () => { + const { wrapper } = setupWrapper({ + isEditMode: true, + }); + + const menuItem = wrapper.findComponent(BoardMenuActionMoveUp); + await menuItem.trigger("click"); + + expect(wrapper.emitted()).toHaveProperty("move-up:edit"); + }); + + it("should emit 'delete:element' event when delete menu item is clicked", async () => { + const { wrapper } = setupWrapper({ + isEditMode: true, + }); + + const menuItem = wrapper.findComponent(BoardMenuActionDelete); + await menuItem.trigger("click"); + + expect(wrapper.emitted()).toHaveProperty("delete:element"); + }); + }); + }); + + describe("onCreateTitle", () => { + describe("and title was provided", () => { + it("should display the title ", async () => { + const videoConferenceTitle = "Very specific vc title"; + const { wrapper } = setupWrapper({ + content: videoConferenceElementContentFactory.build({ + title: videoConferenceTitle, + }), + isEditMode: false, + }); + + expect(wrapper.html()).toEqual( + expect.stringContaining(videoConferenceTitle) + ); + }); + }); + }); + }); + describe("when a videoconference is started or joined", () => { + describe("and an error occurs", () => { + it("should display an error dialog", async () => { + const error = jest.fn(() => { + throw new Error(); + }); + const { wrapper } = setupWrapper({ + content: videoConferenceElementContentFactory.build(), + isEditMode: false, + videoConferenceModuleGetter: { + getError: error, + }, + }); + + const videoConferenceElement = wrapper.findComponent( + '[data-testid="video-conference-element"]' + ); + await videoConferenceElement.trigger("click"); + + const dialog = wrapper.findComponent({ + ref: "vDialog", + }); + + expect(dialog.props("modelValue")).toBe(true); + }); + }); + + describe("and no error occurs", () => { + it("should not display an error dialog", async () => { + const { wrapper } = setupWrapper({ + content: videoConferenceElementContentFactory.build(), + isEditMode: false, + videoConferenceModuleGetter: { + getError: null, + }, + }); + + const videoConferenceElement = wrapper.findComponent( + '[data-testid="video-conference-element"]' + ); + await videoConferenceElement.trigger("click"); + + const dialog = wrapper.findComponent({ + ref: "vDialog", + }); + + expect(dialog.props("modelValue")).toBe(false); + }); + }); + }); +}); diff --git a/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElement.vue b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElement.vue new file mode 100644 index 0000000000..dcb52f0873 --- /dev/null +++ b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElement.vue @@ -0,0 +1,377 @@ + + + diff --git a/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementCreate.unit.ts b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementCreate.unit.ts new file mode 100644 index 0000000000..2dab55985b --- /dev/null +++ b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementCreate.unit.ts @@ -0,0 +1,72 @@ +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { mount } from "@vue/test-utils"; +import VideoConferenceContentElementCreate from "./VideoConferenceContentElementCreate.vue"; + +const setupWrapper = () => { + const wrapper = mount(VideoConferenceContentElementCreate, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + }); + + return wrapper; +}; + +const title = "video conference title"; + +describe("VideoConferenceContentElementCreate", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("when a valid title is entered", () => { + describe("and enter is pressed", () => { + it("should not show error-message", async () => { + const wrapper = setupWrapper(); + + await wrapper.findComponent({ name: "VTextarea" }).setValue(title); + await wrapper.find("form").trigger("submit.prevent"); + + const alerts = wrapper.find('[role="alert"]'); + expect(alerts.text()).toBe(""); + }); + + it("should emit create:title event", async () => { + const wrapper = setupWrapper(); + + await wrapper.findComponent({ name: "VTextarea" }).setValue(title); + await wrapper + .findComponent({ name: "VTextarea" }) + .trigger("keydown.enter"); + + expect(wrapper.emitted("create:title")).toEqual([[title]]); + }); + }); + }); + + describe("when the title field is empty", () => { + describe("and the submit button is clicked", () => { + it("should show required-error-message", async () => { + const wrapper = setupWrapper(); + + await wrapper.findComponent({ name: "VTextarea" }).setValue(""); + await wrapper.find("form").trigger("submit.prevent"); + + const alerts = wrapper.find('[role="alert"]').text(); + expect(alerts).toEqual("common.validation.required2"); + }); + + it("should not emit create:title event", async () => { + const wrapper = setupWrapper(); + + await wrapper.findComponent({ name: "VTextarea" }).setValue(""); + await wrapper.find("form").trigger("submit.prevent"); + + expect(wrapper.emitted("create:title")).toBeUndefined(); + }); + }); + }); +}); diff --git a/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementCreate.vue b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementCreate.vue new file mode 100644 index 0000000000..21a0800882 --- /dev/null +++ b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementCreate.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementDisplay.unit.ts b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementDisplay.unit.ts new file mode 100644 index 0000000000..291efc4b1e --- /dev/null +++ b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementDisplay.unit.ts @@ -0,0 +1,248 @@ +import { mount, VueWrapper } from "@vue/test-utils"; +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import VideoConferenceContentElementDisplay from "./VideoConferenceContentElementDisplay.vue"; +import { BOARD_IS_LIST_LAYOUT } from "@util-board"; +import { ENV_CONFIG_MODULE_KEY } from "@/utils/inject"; +import { createModuleMocks } from "@@/tests/test-utils/mock-store-module"; +import { envConfigModule } from "@/store"; +import EnvConfigModule from "@/store/env-config"; +import { createMock } from "@golevelup/ts-jest"; +import { ConfigResponse } from "@/serverApi/v3"; +import setupStores from "@@/tests/test-utils/setupStores"; +import { envsFactory } from "@@/tests/test-utils"; + +const mockedEnvConfigModule = createModuleMocks(EnvConfigModule, { + getEnv: createMock({ + FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED: true, + }), +}); + +const setupWrapper = ({ + propsData = {}, + envOverrides = {}, +}: { + propsData?: object; + envOverrides?: Partial; +} = {}) => { + const envs = envsFactory.build({ + FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED: true, + ...envOverrides, + }); + envConfigModule.setEnvs(envs); + + const wrapper = mount(VideoConferenceContentElementDisplay, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + provide: { + [BOARD_IS_LIST_LAYOUT as symbol]: false, + [ENV_CONFIG_MODULE_KEY.valueOf()]: mockedEnvConfigModule, + }, + }, + props: { + isEditMode: false, + isRunning: false, + hasParticipationPermission: false, + canStart: false, + title: "", + ...propsData, + }, + }); + + return wrapper; +}; + +describe("VideoConferenceContentElementDisplay", () => { + beforeEach(() => { + setupStores({ envConfigModule: EnvConfigModule }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("Title", () => { + it("should display a title", () => { + const title = "video conference"; + const wrapper = setupWrapper({ + propsData: { + isEditMode: false, + isRunning: false, + hasParticipationPermission: true, + canStart: true, + title, + }, + }); + + const titleElement = wrapper.find( + '[data-testid="content-element-title-slot"]' + ); + expect(titleElement.text()).toEqual(title); + }); + }); + + describe("Alerts", () => { + describe("and the feature is disabled", () => { + it("should show 'not enabled for teacher' alert", () => { + const wrapper = setupWrapper({ + propsData: { + isRunning: false, + hasParticipationPermission: true, + canStart: true, + title: "video conference", + }, + envOverrides: { + FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED: false, + }, + }); + + const alert = wrapper.findComponent( + '[data-testid="vc-info-box-no-feature"]' + ); + const text = alert.find("span.my-auto"); + expect(text.text()).toEqual( + "pages.videoConference.info.notEnabledTeacher" + ); + }); + }); + + describe("and video conference is not running", () => { + it("should show 'not started' alert for a participant", () => { + const wrapper = setupWrapper({ + propsData: { + isRunning: false, + hasParticipationPermission: true, + canStart: false, + title: "video conference", + }, + }); + + const alert = wrapper.findComponent('[data-testid="vc-info-box"]'); + const text = alert.find("span.my-auto"); + expect(text.text()).toEqual("pages.videoConference.info.notStarted"); + }); + + it("should show 'no permission' alert for a user with no participation permission", () => { + const wrapper = setupWrapper({ + propsData: { + isRunning: false, + hasParticipationPermission: false, + canStart: false, + title: "video conference", + }, + }); + + const alert = wrapper.findComponent('[data-testid="vc-info-box"]'); + const text = alert.find("span.my-auto"); + expect(text.text()).toEqual("pages.videoConference.info.noPermission"); + }); + + it("should not display any alert when the user is a teacher", () => { + const wrapper = setupWrapper({ + propsData: { + isRunning: false, + hasParticipationPermission: true, + canStart: true, + title: "video conference", + }, + }); + + const alert = wrapper.find('[data-testid="vc-info-box-show"]'); + expect(alert.exists()).toEqual(false); + }); + }); + + describe("and video conference is running", () => { + it("should show 'no permission' alert when user has no participation permission", () => { + const wrapper = setupWrapper({ + propsData: { + isRunning: true, + hasParticipationPermission: false, + canStart: false, + title: "video conference", + }, + }); + + const alert = wrapper.findComponent( + '[data-testid="vc-info-box-no-permission"]' + ); + const text = alert.find("span.my-auto"); + expect(text.text()).toEqual("pages.videoConference.info.noPermission"); + }); + + it("should display a pulsating dot when the user has permission", () => { + const wrapper = setupWrapper({ + propsData: { + isRunning: true, + hasParticipationPermission: true, + canStart: true, + title: "video conference", + }, + }); + + const dot = wrapper.find('[data-testid="vc-pulsating-dot"]'); + expect(dot.exists()).toEqual(true); + }); + }); + }); + + describe("Events", () => { + const triggerClick = async (wrapper: VueWrapper) => { + const element = wrapper.find( + '[data-testid="board-video-conference-element"]' + ); + await element.trigger("click"); + }; + + describe("and video conference is running", () => { + it("should emit a click event when the user can join and the conference is running", async () => { + const wrapper = setupWrapper({ + propsData: { + isEditMode: false, + isRunning: true, + hasParticipationPermission: true, + canStart: false, + title: "video conference", + }, + }); + + await triggerClick(wrapper); + expect(wrapper.emitted("click")).toBeDefined(); + }); + }); + + describe("and video conference is not running", () => { + it("should emit a refresh event when the user can join", async () => { + const wrapper = setupWrapper({ + propsData: { + isEditMode: false, + isRunning: false, + hasParticipationPermission: true, + canStart: false, + title: "video conference", + }, + }); + + await triggerClick(wrapper); + expect(wrapper.emitted("refresh")).toBeDefined(); + }); + + it("should emit a click event when the user is a teacher", async () => { + const wrapper = setupWrapper({ + propsData: { + isEditMode: false, + isRunning: false, + hasParticipationPermission: true, + canStart: true, + title: "video conference", + }, + }); + + await triggerClick(wrapper); + expect(wrapper.emitted("click")).toBeDefined(); + }); + }); + }); +}); diff --git a/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementDisplay.vue b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementDisplay.vue new file mode 100644 index 0000000000..e863102603 --- /dev/null +++ b/src/modules/feature/board-video-conference-element/components/VideoConferenceContentElementDisplay.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/src/modules/feature/board-video-conference-element/index.ts b/src/modules/feature/board-video-conference-element/index.ts new file mode 100644 index 0000000000..678738239b --- /dev/null +++ b/src/modules/feature/board-video-conference-element/index.ts @@ -0,0 +1,3 @@ +import VideoConferenceContentElement from "./components/VideoConferenceContentElement.vue"; + +export { VideoConferenceContentElement }; diff --git a/src/modules/feature/board/board/Board.unit.ts b/src/modules/feature/board/board/Board.unit.ts index b7339b7fef..03d68e7045 100644 --- a/src/modules/feature/board/board/Board.unit.ts +++ b/src/modules/feature/board/board/Board.unit.ts @@ -882,7 +882,7 @@ describe("Board", () => { expect(wrapperVM.isBoardVisible).toBe(false); expect(router.replace).toHaveBeenCalledWith({ - name: "rooms-id", + name: "room-details", params: { id: mockRoomId }, }); expect( diff --git a/src/modules/feature/board/board/Board.vue b/src/modules/feature/board/board/Board.vue index 4b7bac62ad..05d59f0475 100644 --- a/src/modules/feature/board/board/Board.vue +++ b/src/modules/feature/board/board/Board.vue @@ -322,7 +322,7 @@ watch( setAlert(); if (!(isBoardVisible.value || isTeacher)) { - router.replace({ name: "rooms-id", params: { id: roomId.value } }); + router.replace({ name: "room-details", params: { id: roomId.value } }); applicationErrorModule.setError( createApplicationError( HttpStatusCode.Forbidden, diff --git a/src/modules/feature/board/card/ContentElementList.unit.ts b/src/modules/feature/board/card/ContentElementList.unit.ts index c616528806..ba50b62e47 100644 --- a/src/modules/feature/board/card/ContentElementList.unit.ts +++ b/src/modules/feature/board/card/ContentElementList.unit.ts @@ -19,6 +19,7 @@ import { RichTextContentElement } from "@feature-board-text-element"; import { createMock } from "@golevelup/ts-jest"; import { shallowMount } from "@vue/test-utils"; import ContentElementList from "./ContentElementList.vue"; +import { VideoConferenceContentElement } from "@feature-board-video-conference-element"; describe("ContentElementList", () => { const setup = (props: { @@ -44,7 +45,7 @@ describe("ContentElementList", () => { [ENV_CONFIG_MODULE_KEY.valueOf()]: mockedEnvConfigModule, }, }, - props: { ...props }, + props: { ...props, rowIndex: 0, columnIndex: 0 }, }); return { wrapper }; @@ -93,6 +94,10 @@ describe("ContentElementList", () => { elementType: ContentElementType.Deleted, component: DeletedElement, }, + { + elementType: ContentElementType.VideoConference, + component: VideoConferenceContentElement, + }, ]; it.each(elementComponents)( diff --git a/src/modules/feature/board/card/ContentElementList.vue b/src/modules/feature/board/card/ContentElementList.vue index 940a4dc4ff..abbb4ab1ad 100644 --- a/src/modules/feature/board/card/ContentElementList.vue +++ b/src/modules/feature/board/card/ContentElementList.vue @@ -89,6 +89,19 @@ @move-up:edit="onMoveElementUp(index, element)" @delete:element="onDeleteElement" /> + { + return element.type === ContentElementType.VideoConference; +}; + +const showVideoConferenceElement = ( + element: AnyContentElement +): element is VideoConferenceElementResponse => { + return ( + envConfigModule.getEnv.FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED && + isVideoConferenceElementResponse(element) + ); +}; + const onMoveElementDown = ( elementIndex: number, element: AnyContentElement diff --git a/src/modules/feature/board/shared/AddElementDialog.composable.ts b/src/modules/feature/board/shared/AddElementDialog.composable.ts index 7fc101c201..41b07a8a7d 100644 --- a/src/modules/feature/board/shared/AddElementDialog.composable.ts +++ b/src/modules/feature/board/shared/AddElementDialog.composable.ts @@ -10,6 +10,7 @@ import { mdiPuzzleOutline, mdiTextBoxEditOutline, mdiTrayArrowUp, + mdiVideo, } from "@icons/material"; import { useBoardNotifier } from "@util-board"; import { useI18n } from "vue-i18n"; @@ -158,6 +159,17 @@ export const useAddElementDialog = ( }); } + if (envConfigModule.getEnv.FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED) { + options.push({ + icon: mdiVideo, + label: t( + "components.elementTypeSelection.elements.videoConferenceElement.subtitle" + ), + action: () => onElementClick(ContentElementType.VideoConference), + testId: "create-element-video-conference", + }); + } + const askType = () => { elementTypeOptions.value = options; isDialogOpen.value = true; diff --git a/src/modules/feature/board/shared/AddElementDialog.composable.unit.ts b/src/modules/feature/board/shared/AddElementDialog.composable.unit.ts index c98f39be04..8531c6486f 100644 --- a/src/modules/feature/board/shared/AddElementDialog.composable.unit.ts +++ b/src/modules/feature/board/shared/AddElementDialog.composable.unit.ts @@ -103,7 +103,10 @@ describe("ElementTypeSelection Composable", () => { await onElementClick(elementType); expect(addElementMock).toHaveBeenCalledTimes(1); - expect(addElementMock).toBeCalledWith({ type: elementType, cardId }); + expect(addElementMock).toHaveBeenCalledWith({ + type: elementType, + cardId, + }); expect(isDialogOpen.value).toBe(false); }); @@ -210,7 +213,7 @@ describe("ElementTypeSelection Composable", () => { await onElementClick(elementType); - expect(showCustomNotifierMock).toBeCalledTimes(0); + expect(showCustomNotifierMock).toHaveBeenCalledTimes(0); }); }); @@ -288,6 +291,7 @@ describe("ElementTypeSelection Composable", () => { FEATURE_TLDRAW_ENABLED: true, FEATURE_COLUMN_BOARD_COLLABORATIVE_TEXT_EDITOR_ENABLED: true, FEATURE_PREFERRED_CTL_TOOLS_ENABLED: true, + FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED: true, } ) => { const cardId = "cardId"; @@ -335,8 +339,8 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[0].action; action(); - expect(addElementMock).toBeCalledTimes(1); - expect(addElementMock).toBeCalledWith({ + expect(addElementMock).toHaveBeenCalledTimes(1); + expect(addElementMock).toHaveBeenCalledWith({ type: ContentElementType.RichText, cardId, }); @@ -351,7 +355,7 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[0].action; action(); - expect(closeDialogMock).toBeCalledTimes(1); + expect(closeDialogMock).toHaveBeenCalledTimes(1); }); }); @@ -365,8 +369,8 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[1].action; action(); - expect(addElementMock).toBeCalledTimes(1); - expect(addElementMock).toBeCalledWith({ + expect(addElementMock).toHaveBeenCalledTimes(1); + expect(addElementMock).toHaveBeenCalledWith({ type: ContentElementType.File, cardId, }); @@ -382,7 +386,7 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[1].action; action(); - expect(closeDialogMock).toBeCalledTimes(1); + expect(closeDialogMock).toHaveBeenCalledTimes(1); }); }); @@ -395,8 +399,8 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[2].action; action(); - expect(addElementMock).toBeCalledTimes(1); - expect(addElementMock).toBeCalledWith({ + expect(addElementMock).toHaveBeenCalledTimes(1); + expect(addElementMock).toHaveBeenCalledWith({ type: ContentElementType.SubmissionContainer, cardId, }); @@ -411,7 +415,7 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[2].action; action(); - expect(closeDialogMock).toBeCalledTimes(1); + expect(closeDialogMock).toHaveBeenCalledTimes(1); }); }); @@ -425,8 +429,8 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[3].action; action(); - expect(addElementMock).toBeCalledTimes(1); - expect(addElementMock).toBeCalledWith({ + expect(addElementMock).toHaveBeenCalledTimes(1); + expect(addElementMock).toHaveBeenCalledWith({ type: ContentElementType.ExternalTool, cardId, }); @@ -442,7 +446,7 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[3].action; action(); - expect(closeDialogMock).toBeCalledTimes(1); + expect(closeDialogMock).toHaveBeenCalledTimes(1); }); }); describe("when the DrawingElement action is called", () => { @@ -455,8 +459,8 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[4].action; action(); - expect(addElementMock).toBeCalledTimes(1); - expect(addElementMock).toBeCalledWith({ + expect(addElementMock).toHaveBeenCalledTimes(1); + expect(addElementMock).toHaveBeenCalledWith({ type: ContentElementType.Drawing, cardId, }); @@ -472,7 +476,7 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[4].action; action(); - expect(closeDialogMock).toBeCalledTimes(1); + expect(closeDialogMock).toHaveBeenCalledTimes(1); }); }); describe("when the CollaborativeTextEditorElement action is called", () => { @@ -485,8 +489,8 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[5].action; action(); - expect(addElementMock).toBeCalledTimes(1); - expect(addElementMock).toBeCalledWith({ + expect(addElementMock).toHaveBeenCalledTimes(1); + expect(addElementMock).toHaveBeenCalledWith({ type: ContentElementType.CollaborativeTextEditor, cardId, }); @@ -502,7 +506,38 @@ describe("ElementTypeSelection Composable", () => { const action = elementTypeOptions.value[5].action; action(); - expect(closeDialogMock).toBeCalledTimes(1); + expect(closeDialogMock).toHaveBeenCalledTimes(1); + }); + }); + describe("when the VideoConference action is called", () => { + it("should call video conference element function with right argument", async () => { + const { elementTypeOptions, addElementMock, cardId } = setup(); + const { askType } = useAddElementDialog(addElementMock, cardId); + + askType(); + + console.log(elementTypeOptions.value); + const action = elementTypeOptions.value[7].action; + action(); + + expect(addElementMock).toHaveBeenCalledTimes(1); + expect(addElementMock).toHaveBeenCalledWith({ + type: ContentElementType.VideoConference, + cardId, + }); + }); + + it("should set isDialogOpen to false", async () => { + const { elementTypeOptions, addElementMock, closeDialogMock, cardId } = + setup(); + const { askType } = useAddElementDialog(addElementMock, cardId); + + askType(); + + const action = elementTypeOptions.value[7].action; + action(); + + expect(closeDialogMock).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/modules/feature/board/shared/AddElementDialog.vue b/src/modules/feature/board/shared/AddElementDialog.vue index 186d9eb050..c42dc76d72 100644 --- a/src/modules/feature/board/shared/AddElementDialog.vue +++ b/src/modules/feature/board/shared/AddElementDialog.vue @@ -6,11 +6,9 @@ > - {{ $t("components.elementTypeSelection.dialog.title") }} + {{ t("components.elementTypeSelection.dialog.title") }} - + - {{ $t("common.labels.close") }} + {{ t("common.labels.close") }} @@ -37,10 +35,13 @@ import { ExtendedIconBtn } from "@ui-extended-icon-btn"; import { computed, ComputedRef } from "vue"; import { useSharedElementTypeSelection } from "./SharedElementTypeSelection.composable"; +import { useI18n } from "vue-i18n"; const { isDialogOpen, closeDialog, elementTypeOptions } = useSharedElementTypeSelection(); +const { t } = useI18n(); + const dialogWidth: ComputedRef = computed(() => elementTypeOptions.value.length >= 3 ? 426 : 320 ); diff --git a/src/modules/feature/room/BoardGrid.vue b/src/modules/feature/room/BoardGrid.vue index 0439c5bdc0..25f86243e3 100644 --- a/src/modules/feature/room/BoardGrid.vue +++ b/src/modules/feature/room/BoardGrid.vue @@ -4,6 +4,7 @@ v-for="(board, index) in boards" :key="board.id" cols="12" + sm="6" md="4" xl="3" > diff --git a/src/modules/feature/room/BoardTile.unit.ts b/src/modules/feature/room/BoardTile.unit.ts index 6229d21a97..3ae2b7eaef 100644 --- a/src/modules/feature/room/BoardTile.unit.ts +++ b/src/modules/feature/room/BoardTile.unit.ts @@ -42,7 +42,7 @@ describe("@feature-room/BoardTile", () => { it("should display tile in draft style", () => { const { wrapper } = setup({ board: mockBoard, index: 0 }); - expect(wrapper.classes()).toContain("board-is-draft"); + expect(wrapper.classes()).toContain("opacity-70"); }); }); }); diff --git a/src/modules/feature/room/BoardTile.vue b/src/modules/feature/room/BoardTile.vue index 2f643bd0f4..7e8659437c 100644 --- a/src/modules/feature/room/BoardTile.vue +++ b/src/modules/feature/room/BoardTile.vue @@ -1,23 +1,26 @@ @@ -28,6 +31,7 @@ import { RoomBoardItem } from "@/types/room/Room"; import { mdiViewAgendaOutline, mdiViewDashboardOutline } from "@icons/material"; import { computed, PropType, toRef } from "vue"; import { useI18n } from "vue-i18n"; +import { LineClamp } from "@ui-line-clamp"; const props = defineProps({ board: { type: Object as PropType, required: true }, @@ -35,7 +39,6 @@ const props = defineProps({ }); const { t } = useI18n(); - const board = toRef(props, "board"); const isListBoard = computed(() => { @@ -50,6 +53,7 @@ const subtitleIcon = computed(() => { const icon = isListBoard.value ? mdiViewAgendaOutline : mdiViewDashboardOutline; + return icon; }); @@ -62,6 +66,7 @@ const subtitleText = computed(() => { const suffix = ` - ${t("common.words.draft")}`; return text + suffix; } + return text; }); @@ -69,17 +74,3 @@ const boardPath = computed(() => { return `/boards/${board.value.id}`; }); - - diff --git a/src/modules/feature/room/RoomColorPicker/RoomColorPicker.unit.ts b/src/modules/feature/room/RoomColorPicker/RoomColorPicker.unit.ts index 68459cdc87..9bc9964f9b 100644 --- a/src/modules/feature/room/RoomColorPicker/RoomColorPicker.unit.ts +++ b/src/modules/feature/room/RoomColorPicker/RoomColorPicker.unit.ts @@ -5,7 +5,7 @@ import { import { mount } from "@vue/test-utils"; import { ComponentProps } from "vue-component-type-helpers"; import RoomColorPicker from "./RoomColorPicker.vue"; -import { RoomColor } from "@/serverApi/v3"; +import { RoomColorEnum } from "@/types/room/Room"; describe("@feature-room/RoomColorPicker", () => { const setup = (props?: ComponentProps) => { @@ -42,7 +42,7 @@ describe("@feature-room/RoomColorPicker", () => { describe("when a color is given", () => { it("should render given color as selected", () => { - const { wrapper } = setup({ color: RoomColor.Red }); + const { wrapper } = setup({ color: RoomColorEnum.Red }); const selectedColor = wrapper.findComponent( "[data-testid=color-swatch-red]" diff --git a/src/modules/feature/room/RoomColorPicker/RoomColorPicker.vue b/src/modules/feature/room/RoomColorPicker/RoomColorPicker.vue index bd8f65954e..b338f94c02 100644 --- a/src/modules/feature/room/RoomColorPicker/RoomColorPicker.vue +++ b/src/modules/feature/room/RoomColorPicker/RoomColorPicker.vue @@ -5,7 +5,7 @@ role="radiogroup" aria-labelledby="room-color-label" > -