From fc7dcc6b6fbe71efa553c159eac5b9a62ecc01e6 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Mon, 18 Sep 2023 08:37:31 +0200 Subject: [PATCH 01/46] Fix mediafile directory create cancel error (#2803) --- .../pages/mediafiles/services/mediafile-common.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/site/pages/meetings/pages/mediafiles/services/mediafile-common.service.ts b/client/src/app/site/pages/meetings/pages/mediafiles/services/mediafile-common.service.ts index eed06d2797..66189ce901 100644 --- a/client/src/app/site/pages/meetings/pages/mediafiles/services/mediafile-common.service.ts +++ b/client/src/app/site/pages/meetings/pages/mediafiles/services/mediafile-common.service.ts @@ -3,7 +3,7 @@ import { UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { filter, firstValueFrom } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { infoDialogSettings } from 'src/app/infrastructure/utils/dialog-settings'; import { PromptService } from 'src/app/ui/modules/prompt-dialog'; @@ -93,7 +93,7 @@ export class MediafileCommonService { newDirectoryForm.reset(); const dialogRef = this.dialog.open(templateRef, infoDialogSettings); - const result = await firstValueFrom(dialogRef.afterClosed().pipe(filter(result => result))); + const result = await firstValueFrom(dialogRef.afterClosed()); if (result) { const mediafile = { ...newDirectoryForm.value, From 74bc83fb479e6a2756526b4cd1205bf2c084fc13 Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:40:58 +0200 Subject: [PATCH 02/46] Fix user count on orga-level (#2811) --- client/src/app/domain/models/meetings/meeting.ts | 2 ++ .../app/gateways/repositories/meeting-repository.service.ts | 1 + .../pages/meetings/base/base-has-meeting-user-view-model.ts | 4 ++-- .../src/app/site/pages/meetings/view-models/view-meeting.ts | 2 +- .../pages/committees/modules/services/meeting.service.ts | 2 +- .../components/meeting-edit/meeting-edit.component.ts | 6 +++--- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/client/src/app/domain/models/meetings/meeting.ts b/client/src/app/domain/models/meetings/meeting.ts index 171d3485dc..a843a0c242 100644 --- a/client/src/app/domain/models/meetings/meeting.ts +++ b/client/src/app/domain/models/meetings/meeting.ts @@ -225,6 +225,7 @@ export class Meeting extends BaseModel { public chat_message_ids!: Id[]; // (chat_message/meeting_id)[]; public poll_candidate_list_ids!: Id[]; // (poll_candidate_list/meeting_id)[]; public poll_candidate_ids!: Id[]; // (poll_candidate/meeting_id)[]; + public user_ids!: Id[]; // Other relations public present_user_ids!: Id[]; // (user/is_present_in_meeting_ids)[]; @@ -371,6 +372,7 @@ export class Meeting extends BaseModel { `poll_candidate_list_ids`, `poll_candidate_ids`, `meeting_user_ids`, + `user_ids`, `users_enable_presence_view`, `users_enable_vote_weight`, `users_allow_self_set_present`, diff --git a/client/src/app/gateways/repositories/meeting-repository.service.ts b/client/src/app/gateways/repositories/meeting-repository.service.ts index 69ae2deb97..38d009bf52 100644 --- a/client/src/app/gateways/repositories/meeting-repository.service.ts +++ b/client/src/app/gateways/repositories/meeting-repository.service.ts @@ -59,6 +59,7 @@ export class MeetingRepositoryService extends BaseRepository = any */ export abstract class BaseHasMeetingUsersViewModel = any> extends BaseViewModel { public meeting_users: ViewMeetingUser[]; - public get users(): ViewUser[] { + public get calculated_users(): ViewUser[] { return this.meeting_users?.flatMap(user => user.user ?? []); } - public get user_ids(): number[] { + public get calculated_user_ids(): number[] { return this.meeting_users?.flatMap(user => user.user_id ?? []); } } diff --git a/client/src/app/site/pages/meetings/view-models/view-meeting.ts b/client/src/app/site/pages/meetings/view-models/view-meeting.ts index cedc70c5b8..e41336ec34 100644 --- a/client/src/app/site/pages/meetings/view-models/view-meeting.ts +++ b/client/src/app/site/pages/meetings/view-models/view-meeting.ts @@ -63,7 +63,7 @@ export class ViewMeeting extends BaseHasMeetingUsersViewModel { } public get userAmount(): number { - return this.meeting_user_ids?.length || 0; + return this.user_ids?.length || 0; } public get motionsAmount(): number { diff --git a/client/src/app/site/pages/organization/pages/committees/modules/services/meeting.service.ts b/client/src/app/site/pages/organization/pages/committees/modules/services/meeting.service.ts index 1235393212..805791c8c0 100644 --- a/client/src/app/site/pages/organization/pages/committees/modules/services/meeting.service.ts +++ b/client/src/app/site/pages/organization/pages/committees/modules/services/meeting.service.ts @@ -22,7 +22,7 @@ export class MeetingService { await this.router.navigate([`accounts`]); this.accountFilterService.clearAllFilters(); this.accountFilterService.toggleFilterOption(`id`, { - condition: meeting.user_ids, + condition: meeting.calculated_user_ids, label: meeting.name }); } diff --git a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/modules/committee-detail-meeting/components/meeting-edit/meeting-edit.component.ts b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/modules/committee-detail-meeting/components/meeting-edit/meeting-edit.component.ts index bc09436351..3eba0d317a 100644 --- a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/modules/committee-detail-meeting/components/meeting-edit/meeting-edit.component.ts +++ b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/modules/committee-detail-meeting/components/meeting-edit/meeting-edit.component.ts @@ -107,7 +107,7 @@ export class MeetingEditComponent extends BaseComponent implements OnInit { public committee!: ViewCommittee; public get meetingUsers(): ViewUser[] { - return this.editMeeting?.users || []; + return this.editMeeting?.calculated_users || []; } private meetingId: Id | null = null; @@ -309,7 +309,7 @@ export class MeetingEditComponent extends BaseComponent implements OnInit { }: any = meeting.getUpdatedModelData({ start_time: start_time, end_time: end_time, - admin_ids: [...(meeting.admin_group?.user_ids || [])] + admin_ids: [...(meeting.admin_group?.calculated_user_ids || [])] } as any); const patchDaterange = { start, @@ -373,7 +373,7 @@ export class MeetingEditComponent extends BaseComponent implements OnInit { */ private getUsersToUpdateForMeetingObject(): MeetingUserModifiedFields { const nextAdminIds = this.meetingForm.value.admin_ids as Id[]; - const previousAdminIds = this.editMeeting!.admin_group.user_ids || []; + const previousAdminIds = this.editMeeting!.admin_group.calculated_user_ids || []; const addedAdminIds = (nextAdminIds || []).difference(previousAdminIds); const removedAdminIds = previousAdminIds.difference(nextAdminIds); From 2a258a3a005520471869362dfcf882041108316f Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:55:34 +0200 Subject: [PATCH 03/46] Fix amendment display (#2819) --- .../base/base-motion-detail-child.component.ts | 7 +++++++ .../motion-highlight-form.component.ts | 13 +++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/base/base-motion-detail-child.component.ts b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/base/base-motion-detail-child.component.ts index 9f54e34133..e0a963c3bb 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/base/base-motion-detail-child.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/base/base-motion-detail-child.component.ts @@ -34,6 +34,8 @@ export abstract class BaseMotionDetailChildComponent extends BaseMeetingComponen if (!Object.keys(previousMotion || {}).length && Object.keys(motion).length) { this.onInitTextBasedAmendment(); // Assuming that it's an amendment } + + this.onAfterSetMotion(previousMotion, motion); } public get motion(): ViewMotion { @@ -206,6 +208,11 @@ export abstract class BaseMotionDetailChildComponent extends BaseMeetingComponen */ protected onInitTextBasedAmendment(): void {} + /** + * Function called after all eventual updates whenever the motion setter is called + */ + protected onAfterSetMotion(_previous: ViewMotion, _current: ViewMotion): void {} + /** * Function called when a new motion is passed and right after the internal `init`-function was called */ diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-highlight-form/motion-highlight-form.component.ts b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-highlight-form/motion-highlight-form.component.ts index b4b56ee0bb..eb13f56e6b 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-highlight-form/motion-highlight-form.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-highlight-form/motion-highlight-form.component.ts @@ -5,7 +5,7 @@ import { MatMenuTrigger } from '@angular/material/menu'; import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; import { ChangeRecoMode, LineNumberingMode } from 'src/app/domain/models/motions/motions.constants'; -import { ViewMotionChangeRecommendation } from 'src/app/site/pages/meetings/pages/motions'; +import { ViewMotion, ViewMotionChangeRecommendation } from 'src/app/site/pages/meetings/pages/motions'; import { MeetingComponentServiceCollectorService } from 'src/app/site/pages/meetings/services/meeting-component-service-collector.service'; import { ViewPortService } from 'src/app/site/services/view-port.service'; import { PromptService } from 'src/app/ui/modules/prompt-dialog'; @@ -154,7 +154,7 @@ export class MotionHighlightFormComponent extends BaseMotionDetailChildComponent let target: Element | null; // to make the selected line not stick at the very top of the screen, and to prevent it from being // conceiled from the header, we actually scroll to a element a little bit above. - if (line > 4) { + if ((line as number) > 4) { target = element.querySelector(`.os-line-number.line-number-` + ((line as number) - 4).toString(10)); } else { target = element.querySelector(`.title-line`); @@ -266,6 +266,15 @@ export class MotionHighlightFormComponent extends BaseMotionDetailChildComponent this.startLineNumber = this.motion?.start_line_number || 1; } + protected override onAfterSetMotion(previous: ViewMotion, current: ViewMotion): void { + if (!previous?.amendment_paragraphs && !!current?.amendment_paragraphs) { + const recoMode = this.meetingSettingsService.instant(`motions_recommendation_text_mode`); + if (recoMode) { + this.setChangeRecoMode(this.determineCrMode(recoMode as ChangeRecoMode)); + } + } + } + /** * Tries to determine the realistic CR-Mode from a given CR mode */ From 13c406e94447197de70247d6094d1bcb15373aee Mon Sep 17 00:00:00 2001 From: Joshua Sangmeister <33004050+jsangmeister@users.noreply.github.com> Date: Mon, 18 Sep 2023 23:38:38 +0200 Subject: [PATCH 04/46] Replace notify to_all with to_meeting (#2822) --- client/src/app/gateways/notify.service.ts | 22 +++++++++++-------- .../listen-editing.directive.ts | 2 +- .../c4-dialog/c4-dialog.component.ts | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/client/src/app/gateways/notify.service.ts b/client/src/app/gateways/notify.service.ts index 3313464af5..b5a236c9cd 100644 --- a/client/src/app/gateways/notify.service.ts +++ b/client/src/app/gateways/notify.service.ts @@ -30,7 +30,6 @@ interface NotifyBase { */ export interface NotifyRequest extends NotifyBase { channel_id: string; - to_all?: boolean; /** * Targeted Meeting as MeetingID @@ -83,7 +82,7 @@ interface ChannelIdResponse { interface NotifySendOptions { name: string; message: T; - toAll?: boolean; + meeting?: number; users?: number[]; channels?: string[]; } @@ -145,12 +144,17 @@ export class NotifyService extends BaseICCGatewayService(name: string, content: T): Promise { - await this.send(this.buildRequest({ name, message: content, toAll: true })); + * @param meetingId The meeting id to send this message to + */ + public async sendToMeeting( + name: string, + content: T, + meetingId: number = this.activeMeetingIdService.meetingId + ): Promise { + await this.send(this.buildRequest({ name, message: content, meeting: meetingId })); } /** @@ -197,8 +201,8 @@ export class NotifyService extends BaseICCGatewayService(this.EDIT_NOTIFICATION_NAME, content); + this.notifyService.sendToMeeting(this.EDIT_NOTIFICATION_NAME, content); } } diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.ts b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.ts index 25b96037dd..92bdebf8e5 100644 --- a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.ts +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.ts @@ -363,7 +363,7 @@ export class C4DialogComponent implements OnInit, OnDestroy { */ public enter_search(): void { this.caption = this.translate.instant(`Searching for players ...`); - this.notifyService.sendToAllUsers(`c4_search_request`, { name: this.getPlayerName() }); + this.notifyService.sendToMeeting(`c4_search_request`, { name: this.getPlayerName() }); } /** From 64c0ce7ac31783e74a4c73957b1cac9d8c79cab1 Mon Sep 17 00:00:00 2001 From: Joshua Sangmeister <33004050+jsangmeister@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:10:10 +0200 Subject: [PATCH 05/46] Minor cleanup (#2824) also fixed the missing chess functionality --- client/.eslintrc.js | 2 +- client/angular.json | 5 + client/package-lock.json | 56 +++ client/package.json | 2 + .../account-button.component.ts | 18 + .../assignment-detail.module.ts | 4 +- .../base-game-dialog/base-game-dialog.ts | 300 +++++++++++++++ ...egg-content-platform-dialog.component.scss | 4 + ...r-egg-content-platform-dialog.component.ts | 6 +- .../c4-dialog/c4-dialog.component.html | 8 +- .../c4-dialog/c4-dialog.component.scss | 4 +- .../c4-dialog/c4-dialog.component.ts | 352 +++--------------- .../chess-dialog/chess-dialog.module.ts | 19 + .../chess-dialog/chess-dialog.component.html | 21 ++ .../chess-dialog/chess-dialog.component.scss | 19 + .../chess-dialog.component.spec.ts | 24 ++ .../chess-dialog/chess-dialog.component.ts | 159 ++++++++ .../easter-egg/modules/chess-dialog/index.ts | 1 + 18 files changed, 693 insertions(+), 311 deletions(-) create mode 100644 client/src/app/ui/modules/sidenav/modules/easter-egg/components/base-game-dialog/base-game-dialog.ts create mode 100644 client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/chess-dialog.module.ts create mode 100644 client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.html create mode 100644 client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.scss create mode 100644 client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.spec.ts create mode 100644 client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.ts create mode 100644 client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/index.ts diff --git a/client/.eslintrc.js b/client/.eslintrc.js index da9a1ef869..d5f36f1ae5 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -53,7 +53,7 @@ module.exports = { } ], '@typescript-eslint/quotes': ['error', 'backtick', { 'avoidEscape': false }], - '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], + '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_' }], 'simple-import-sort/imports': 'error', 'simple-import-sort/exports': 'error', 'unused-imports/no-unused-imports': 'error', diff --git a/client/angular.json b/client/angular.json index eb7ff41ed8..028543dc0b 100644 --- a/client/angular.json +++ b/client/angular.json @@ -34,6 +34,11 @@ "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" + }, + { + "glob": "**/*", + "input": "node_modules/cm-chessboard/assets/", + "output": "/chess/" } ], "styles": ["src/styles.scss"], diff --git a/client/package-lock.json b/client/package-lock.json index 0b72867bff..f8bc6988d1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -32,6 +32,8 @@ "ascii-art-api": "^1.0.0", "broadcast-channel": "^4.18.0", "chart.js": "^3.9.1", + "cm-chess": "^3.3.3", + "cm-chessboard": "^8.1.5", "core-js": "^3.25.5", "date-fns": "^2.30.0", "exceljs": "^4.3.0", @@ -6011,6 +6013,11 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" }, + "node_modules/chess.mjs": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/chess.mjs/-/chess.mjs-1.4.0.tgz", + "integrity": "sha512-TxbpfE4BvRMteIWEy9j1wsR/UwYqGekmZFDcPrKjaqHbmeRwQOv5sribu6PE6nO/69uTxld0nfX7Ra0XA03j4Q==" + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -6213,6 +6220,28 @@ "node": ">=6" } }, + "node_modules/cm-chess": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/cm-chess/-/cm-chess-3.3.3.tgz", + "integrity": "sha512-Zp8nw0nPanaKMdF8qPrkHLtj/mrIjI/mTkYdJORKLAm12UVMpI0KpXu5zyV5Z7pLQP8KYRn2WXzuisKdpNONLw==", + "dependencies": { + "chess.mjs": "^1.4.0", + "cm-pgn": "3.2.1" + } + }, + "node_modules/cm-chessboard": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/cm-chessboard/-/cm-chessboard-8.1.5.tgz", + "integrity": "sha512-lEB/Dcajc6Noey7lF/L1abKn2o7tSxP7i+JSyIlBSIDNiZ3kv6bMeHdC4m9QVs3w/RyeCNJWGGMRd2sLOBO0Mw==" + }, + "node_modules/cm-pgn": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cm-pgn/-/cm-pgn-3.2.1.tgz", + "integrity": "sha512-XKSqQlDFgtenQN0k3cyQEGYjD8SCwoqcHABRJBNrgdnRlcoyvXHXPR0eLWitX8EjEYp+l3Gy2I4OSg62/Eep3w==", + "dependencies": { + "chess.mjs": "^1.4.0" + } + }, "node_modules/code-block-writer": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", @@ -22578,6 +22607,11 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" }, + "chess.mjs": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/chess.mjs/-/chess.mjs-1.4.0.tgz", + "integrity": "sha512-TxbpfE4BvRMteIWEy9j1wsR/UwYqGekmZFDcPrKjaqHbmeRwQOv5sribu6PE6nO/69uTxld0nfX7Ra0XA03j4Q==" + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -22720,6 +22754,28 @@ "shallow-clone": "^3.0.0" } }, + "cm-chess": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/cm-chess/-/cm-chess-3.3.3.tgz", + "integrity": "sha512-Zp8nw0nPanaKMdF8qPrkHLtj/mrIjI/mTkYdJORKLAm12UVMpI0KpXu5zyV5Z7pLQP8KYRn2WXzuisKdpNONLw==", + "requires": { + "chess.mjs": "^1.4.0", + "cm-pgn": "3.2.1" + } + }, + "cm-chessboard": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/cm-chessboard/-/cm-chessboard-8.1.5.tgz", + "integrity": "sha512-lEB/Dcajc6Noey7lF/L1abKn2o7tSxP7i+JSyIlBSIDNiZ3kv6bMeHdC4m9QVs3w/RyeCNJWGGMRd2sLOBO0Mw==" + }, + "cm-pgn": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cm-pgn/-/cm-pgn-3.2.1.tgz", + "integrity": "sha512-XKSqQlDFgtenQN0k3cyQEGYjD8SCwoqcHABRJBNrgdnRlcoyvXHXPR0eLWitX8EjEYp+l3Gy2I4OSg62/Eep3w==", + "requires": { + "chess.mjs": "^1.4.0" + } + }, "code-block-writer": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", diff --git a/client/package.json b/client/package.json index 7c4261031f..2beda633f1 100644 --- a/client/package.json +++ b/client/package.json @@ -60,6 +60,8 @@ "ascii-art-api": "^1.0.0", "broadcast-channel": "^4.18.0", "chart.js": "^3.9.1", + "cm-chess": "^3.3.3", + "cm-chessboard": "^8.1.5", "core-js": "^3.25.5", "date-fns": "^2.30.0", "exceljs": "^4.3.0", diff --git a/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.ts b/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.ts index f171946a01..2e8e7fe6df 100644 --- a/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.ts +++ b/client/src/app/site/modules/global-headbar/components/account-button/account-button.component.ts @@ -8,6 +8,7 @@ import { Id } from 'src/app/domain/definitions/key-types'; import { availableTranslations } from 'src/app/domain/definitions/languages'; import { getOmlVerboseName } from 'src/app/domain/definitions/organization-permission'; import { largeDialogSettings } from 'src/app/infrastructure/utils/dialog-settings'; +import { mediumDialogSettings } from 'src/app/infrastructure/utils/dialog-settings'; import { ActiveMeetingIdService } from 'src/app/site/pages/meetings/services/active-meeting-id.service'; import { MeetingSettingsService } from 'src/app/site/pages/meetings/services/meeting-settings.service'; import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user'; @@ -16,6 +17,7 @@ import { OperatorService } from 'src/app/site/services/operator.service'; import { ThemeService } from 'src/app/site/services/theme.service'; import { UserControllerService } from 'src/app/site/services/user-controller.service'; import { BaseUiComponent } from 'src/app/ui/base/base-ui-component'; +import { ChessDialogComponent } from 'src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component'; import { AccountDialogComponent } from '../account-dialog/account-dialog.component'; @@ -66,6 +68,8 @@ export class AccountButtonComponent extends BaseUiComponent implements OnInit { private _userSubscription: Subscription | null = null; private _isAllowedSelfSetPresent = false; private _languageTrigger: MatMenuTrigger | undefined = undefined; + private clickCounter = 0; + private clickTimeout: number | null = null; public constructor( private translate: TranslateService, @@ -147,6 +151,20 @@ export class AccountButtonComponent extends BaseUiComponent implements OnInit { buttonEvent.preventDefault(); buttonEvent.stopPropagation(); this.theme.toggleDarkMode(); + + this.clickCounter++; + if (this.clickTimeout) { + clearTimeout(this.clickTimeout); + } + + if (this.clickCounter === 4) { + this.clickCounter = 0; + this.dialog.open(ChessDialogComponent, { ...mediumDialogSettings }); + } else { + this.clickTimeout = setTimeout(() => { + this.clickCounter = 0; + }, 200); + } } public getStructureLevel(): string { diff --git a/client/src/app/site/pages/meetings/pages/assignments/pages/assignment-detail/assignment-detail.module.ts b/client/src/app/site/pages/meetings/pages/assignments/pages/assignment-detail/assignment-detail.module.ts index f2d8763b3e..dcbb785d4a 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/pages/assignment-detail/assignment-detail.module.ts +++ b/client/src/app/site/pages/meetings/pages/assignments/pages/assignment-detail/assignment-detail.module.ts @@ -16,6 +16,7 @@ import { DirectivesModule } from 'src/app/ui/directives'; import { EditorModule } from 'src/app/ui/modules/editor'; import { HeadBarModule } from 'src/app/ui/modules/head-bar'; import { SearchSelectorModule } from 'src/app/ui/modules/search-selector'; +import { ChessDialogModule } from 'src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog'; import { SortingListModule } from 'src/app/ui/modules/sorting/modules/sorting-list/sorting-list.module'; import { PipesModule } from 'src/app/ui/pipes/pipes.module'; @@ -60,7 +61,8 @@ import { AssignmentDetailServiceModule } from './services/assignment-detail-serv OpenSlidesTranslationModule.forChild(), ParticipantSearchSelectorModule, AgendaItemCommonServiceModule, - ChipSelectModule + ChipSelectModule, + ChessDialogModule ] }) export class AssignmentDetailModule {} diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/components/base-game-dialog/base-game-dialog.ts b/client/src/app/ui/modules/sidenav/modules/easter-egg/components/base-game-dialog/base-game-dialog.ts new file mode 100644 index 0000000000..932401dbc6 --- /dev/null +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/components/base-game-dialog/base-game-dialog.ts @@ -0,0 +1,300 @@ +import { Directive, OnDestroy, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; +import { NotifyResponse, NotifyService } from 'src/app/gateways/notify.service'; +import { ActiveMeetingService } from 'src/app/site/pages/meetings/services/active-meeting.service'; +import { OperatorService } from 'src/app/site/services/operator.service'; + +/** + * All states for the statemachine + */ +export type State = 'start' | 'search' | 'waitForResponse' | 'ownMove' | 'opponentMove'; + +/** + * All events that can be handled by the statemachine. + */ +export type StateEvent = + | 'searchClicked' + | 'receivedSearchRequest' + | 'receivedSearchResponse' + | 'receivedACK' + | 'waitTimeout' + | 'executedMove' + | 'receivedMove' + | 'receivedRagequit'; + +/** + * An action in one state. + */ +export interface SMAction { + handle: (data?: any) => State | null; +} + +/** + * The statemachine. Mapps events in states to actions. + */ +export type StateMachine = { [state in State]?: { [event in StateEvent]?: SMAction } }; + +/** + * A base class for two-player game dialogs, implementing all relevant functionality to synchronize + * the start and progress of the game via ICC. + */ +@Directive() +export abstract class BaseGameDialogComponent implements OnInit, OnDestroy { + /** + * Prefix to use for all ICC messages. Must be set by the subclass. + */ + protected abstract prefix: string; + + /** + * Contains if the user is currently within a meeting + */ + public inMeeting = false; + + public caption: string; + + /** + * The channel of the opponent. + */ + private replyChannel: string | null = null; + + /** + * The opponents name. + */ + public opponentName: string | null = null; + + /** + * A timeout to go from waiting to search state. + */ + private waitTimout: number | null = null; + + /** + * A list of all subscriptions, so they can b unsubscribed on desroy. + */ + private subscriptions: Subscription[] = []; + + /** + * The current state of the state machine. + */ + public state: State = `search`; + + /** + * This is the state machine for this game :) + */ + public SM: StateMachine = { + start: { + searchClicked: { + handle: () => { + this.reset(); + return `search`; + } + } + }, + search: { + receivedSearchRequest: { + handle: (notify: NotifyResponse<{ name: string }>) => { + this.replyChannel = notify.sender_channel_id; + this.opponentName = notify.message.name; + return `waitForResponse`; + } + }, + receivedSearchResponse: { + handle: (notify: NotifyResponse<{ name: string }>) => { + this.replyChannel = notify.sender_channel_id; + this.opponentName = notify.message.name; + const [message, nextState] = this.startGame(); + // send ACK + this.notifyService.sendToChannels(`${this.prefix}_ACK`, message, this.replyChannel); + return nextState; + } + } + }, + waitForResponse: { + receivedACK: { + handle: (notify: NotifyResponse<{}>) => { + if (notify.sender_channel_id !== this.replyChannel) { + return null; + } + const [_, nextState] = this.startGame(notify.message); + return nextState; + } + }, + waitTimeout: { + handle: () => `search` + }, + receivedRagequit: { + handle: (notify: NotifyResponse<{}>) => + notify.sender_channel_id === this.replyChannel ? `search` : null + } + }, + ownMove: { + executedMove: { + handle: (move: any) => { + const nextState = this.executeMove(move, true); + this.notifyService.sendToChannels(`${this.prefix}_move`, move, this.replyChannel!); + return nextState; + } + }, + receivedRagequit: { + handle: () => { + this.caption = this.translate.instant( + `Your opponent couldn't stand it anymore... You are the winner!` + ); + return `start`; + } + } + }, + opponentMove: { + receivedMove: { + handle: (notify: NotifyResponse) => { + if (notify.sender_channel_id !== this.replyChannel) { + return null; + } + return this.executeMove(notify.message); + } + }, + receivedRagequit: { + handle: () => { + this.caption = this.translate.instant( + `Your opponent couldn't stand it anymore... You are the winner!` + ); + return `start`; + } + } + } + }; + + public constructor( + private activeMeetingService: ActiveMeetingService, + protected notifyService: NotifyService, + private op: OperatorService, + protected translate: TranslateService + ) { + this.inMeeting = !!this.activeMeetingService.meetingId; + } + + public ngOnInit(): void { + this.state = `start`; + + // Setup all subscription for needed notify messages + this.subscriptions = [ + this.notifyService.getMessageObservable(`${this.prefix}_ACK`).subscribe(notify => { + if (!notify.sendByThisUser) { + this.handleEvent(`receivedACK`, notify); + } + }), + this.notifyService.getMessageObservable(`${this.prefix}_ragequit`).subscribe(notify => { + if (!notify.sendByThisUser) { + this.handleEvent(`receivedRagequit`, notify); + } + }), + this.notifyService.getMessageObservable(`${this.prefix}_search_request`).subscribe(notify => { + if (!notify.sendByThisUser) { + this.handleEvent(`receivedSearchRequest`, notify); + } + }), + this.notifyService.getMessageObservable(`${this.prefix}_search_response`).subscribe(notify => { + if (!notify.sendByThisUser) { + this.handleEvent(`receivedSearchResponse`, notify); + } + }), + this.notifyService.getMessageObservable(`${this.prefix}_move`).subscribe(notify => { + if (!notify.sendByThisUser) { + this.handleEvent(`receivedMove`, notify); + } + }) + ]; + } + + public ngOnDestroy(): void { + // send ragequit and unsubscribe all subscriptions. + if (this.replyChannel) { + this.notifyService.sendToChannels(`${this.prefix}_ragequit`, null, this.replyChannel); + } + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + } + + protected abstract reset(): void; + + protected abstract startGame(message?: any): [any, State]; + + protected abstract executeMove(move: any, ownMove?: boolean): State | null; + + /** + * Returns the operators name. + */ + public getPlayerName(): string { + return this.op.shortName; + } + + /** + * Main state machine handler. The current state handler will be called with + * the given event. If the handler returns a state (and not null), this will be + * the next state. The state enter method will be called. + * @param e The event for the statemachine. + * @param data Additional data for the handler. + */ + public handleEvent(e: StateEvent, data?: any): void { + let action: SMAction | null = null; + if (this.SM[this.state] && this.SM[this.state]![e]) { + action = this.SM[this.state]![e] as SMAction; + const nextState = action.handle(data); + if (nextState !== null) { + this.state = nextState; + if (this[`enter_${nextState}`]) { + this[`enter_${nextState}`](); + } + } + } + } + + // Enter state methods + /** + * Resets all attributes of the state machine. + */ + public enter_start(): void { + this.replyChannel = null; + this.opponentName = null; + } + + /** + * Sends a search request for other players. + */ + public enter_search(): void { + this.caption = this.translate.instant(`Searching for players ...`); + this.notifyService.sendToMeeting(`${this.prefix}_search_request`, { name: this.getPlayerName() }); + } + + /** + * Sends a search response for a previous request. + * Also sets up a timeout to go back into the search state. + */ + public enter_waitForResponse(): void { + this.caption = this.translate.instant(`Wait for response ...`); + this.notifyService.sendToChannels( + `${this.prefix}_search_response`, + { name: this.getPlayerName() }, + this.replyChannel! + ); + if (this.waitTimout) { + clearTimeout(this.waitTimout); + } + this.waitTimout = setTimeout(() => { + this.handleEvent(`waitTimeout`); + }, 5000); + } + + /** + * Sets the caption. + */ + public enter_ownMove(): void { + this.caption = this.translate.instant(`It's your turn!`); + } + + /** + * Sets the caption. + */ + public enter_opponentMove(): void { + this.caption = this.translate.instant(`It's your opponent's turn`); + } +} diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/components/easter-egg-content-platform-dialog/easter-egg-content-platform-dialog.component.scss b/client/src/app/ui/modules/sidenav/modules/easter-egg/components/easter-egg-content-platform-dialog/easter-egg-content-platform-dialog.component.scss index 184c5bb4c3..ccc21fe045 100644 --- a/client/src/app/ui/modules/sidenav/modules/easter-egg/components/easter-egg-content-platform-dialog/easter-egg-content-platform-dialog.component.scss +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/components/easter-egg-content-platform-dialog/easter-egg-content-platform-dialog.component.scss @@ -2,3 +2,7 @@ cursor: pointer; border-radius: 4px; } + +.mat-dialog-content { + max-height: 95vh; +} diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/components/easter-egg-content-platform-dialog/easter-egg-content-platform-dialog.component.ts b/client/src/app/ui/modules/sidenav/modules/easter-egg/components/easter-egg-content-platform-dialog/easter-egg-content-platform-dialog.component.ts index 17f162aedf..47a704c513 100644 --- a/client/src/app/ui/modules/sidenav/modules/easter-egg/components/easter-egg-content-platform-dialog/easter-egg-content-platform-dialog.component.ts +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/components/easter-egg-content-platform-dialog/easter-egg-content-platform-dialog.component.ts @@ -1,7 +1,8 @@ import { ComponentPortal, ComponentType } from '@angular/cdk/portal'; import { Component } from '@angular/core'; -import { C4DialogModule } from '../../modules/c4-dialog/c4-dialog.module'; +import { C4DialogModule } from '../../modules/c4-dialog'; +import { ChessDialogModule } from '../../modules/chess-dialog'; interface EasterEggModuleDescription { label: string; @@ -14,12 +15,11 @@ interface EasterEggModuleDescription { styleUrls: [`./easter-egg-content-platform-dialog.component.scss`] }) export class EasterEggContentPlatformDialogComponent { - public readonly choosableModules: EasterEggModuleDescription[] = [C4DialogModule]; + public readonly choosableModules: EasterEggModuleDescription[] = [C4DialogModule, ChessDialogModule]; public selectedModule: ComponentPortal | null = new ComponentPortal(C4DialogModule.getComponent()); public selectModule(component: ComponentType): void { - console.log(`selectedModule`, component); this.selectedModule = new ComponentPortal(component); } } diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.html b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.html index 3fe3575ce5..ff332736f1 100644 --- a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.html +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.html @@ -6,20 +6,20 @@

{{ caption | translate }}

- +
{{ getPlayerName() }}
-
- {{ partnerName }} +
+ {{ opponentName }}
-
+
diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.scss b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.scss index 5c14f0d070..d163529bd1 100644 --- a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.scss +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.scss @@ -43,10 +43,10 @@ span { #c4.disabled .thisPlayer { background-color: #8888ff; } -.partner { +.opponent { background-color: red; } -#c4.disabled .partner { +#c4.disabled .opponent { background-color: #ff8888; } .coin { diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.ts b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.ts index 92bdebf8e5..c543d391b2 100644 --- a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.ts +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/c4-dialog/components/c4-dialog/c4-dialog.component.ts @@ -1,18 +1,18 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { MatDialogRef } from '@angular/material/dialog'; +import { Component, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs'; -import { NotifyResponse, NotifyService } from 'src/app/gateways/notify.service'; +import { NotifyService } from 'src/app/gateways/notify.service'; import { ActiveMeetingService } from 'src/app/site/pages/meetings/services/active-meeting.service'; import { OperatorService } from 'src/app/site/services/operator.service'; +import { BaseGameDialogComponent, State } from '../../../../components/base-game-dialog/base-game-dialog'; + /** * All player types. */ enum Player { noPlayer, thisPlayer, - partner + opponent } /** @@ -21,58 +21,20 @@ enum Player { enum BoardStatus { Draw = `draw`, thisPlayer = `thisPlayer`, - partner = `partner`, + opponent = `opponent`, NotDecided = `not decided` } -/** - * All states for the statemachine - */ -type State = 'start' | 'search' | 'waitForResponse' | 'myTurn' | 'foreignTurn'; - -/** - * All events that can be handled by the statemachine. - */ -type StateEvent = - | 'searchClicked' - | 'recievedSearchRequest' - | 'recievedSearchResponse' - | 'recievedACK' - | 'waitTimeout' - | 'fieldClicked' - | 'recievedTurn' - | 'recievedRagequit'; - -/** - * An action in one state. - */ -interface SMAction { - handle: (data?: any) => State | null; -} - -/** - * The statemachine. Mapps events in states to actions. - */ -type StateMachine = { [state in State]?: { [event in StateEvent]?: SMAction } }; - @Component({ selector: `os-c4-dialog`, templateUrl: `./c4-dialog.component.html`, styleUrls: [`./c4-dialog.component.scss`] }) -export class C4DialogComponent implements OnInit, OnDestroy { - /** - * Contains if the user is currently within a meeting - */ - public inMeeting = false; - - /** - * The dialogs caption - */ - public caption = ``; +export class C4DialogComponent extends BaseGameDialogComponent implements OnInit { + protected prefix = `c4`; /** - * Saves, if the board is disabled. + * Saves if the board is disabled. */ public disableBoard = false; @@ -81,189 +43,27 @@ export class C4DialogComponent implements OnInit, OnDestroy { */ public board: Player[][] = []; - /** - * The channel of the partner. - */ - private replyChannel: string | null = null; - - /** - * The partners name. - */ - public partnerName: string | null = null; - - /** - * A timeout to go from waiting to search state. - */ - private waitTimout: number | null = null; - - /** - * A list of all subscriptions, so they can b unsubscribed on desroy. - */ - private subscriptions: Subscription[] = []; - - /** - * The current state of the state machine. - */ - public state: State = `search`; - - /** - * This is the state machine for this game :) - */ - public SM: StateMachine = { - start: { - searchClicked: { - handle: () => { - this.disableBoard = false; - this.resetBoard(); - return `search`; - } - } - }, - search: { - recievedSearchRequest: { - handle: (notify: NotifyResponse<{ name: string }>) => { - this.replyChannel = notify.sender_channel_id; - this.partnerName = notify.message.name; - return `waitForResponse`; - } - }, - recievedSearchResponse: { - handle: (notify: NotifyResponse<{ name: string }>) => { - this.replyChannel = notify.sender_channel_id; - this.partnerName = notify.message.name; - // who starts? - const startPlayer = Math.random() < 0.5 ? Player.thisPlayer : Player.partner; - const startPartner: boolean = startPlayer === Player.partner; - // send ACK - this.notifyService.sendToChannels(`c4_ACK`, startPartner, this.replyChannel); - return startPlayer === Player.thisPlayer ? `myTurn` : `foreignTurn`; - } - } - }, - waitForResponse: { - recievedACK: { - handle: (notify: NotifyResponse<{}>) => { - if (notify.sender_channel_id !== this.replyChannel) { - return null; - } - return notify.message ? `myTurn` : `foreignTurn`; - } - }, - waitTimeout: { - handle: () => `search` - }, - recievedRagequit: { - handle: (notify: NotifyResponse<{}>) => - notify.sender_channel_id === this.replyChannel ? `search` : null - } - }, - myTurn: { - fieldClicked: { - handle: (data: { col: number; row: number }) => { - if (this.colFree(data.col)) { - this.setCoin(data.col, Player.thisPlayer); - this.notifyService.sendToChannels(`c4_turn`, { col: data.col }, this.replyChannel!); - const nextState = this.getStateFromBoardStatus(); - return nextState === null ? `foreignTurn` : nextState; - } else { - return null; - } - } - }, - recievedRagequit: { - handle: () => { - this.caption = this.translate.instant( - `Your partner couldn't stand it anymore... You are the winner!` - ); - return `start`; - } - } - }, - foreignTurn: { - recievedTurn: { - handle: (notify: NotifyResponse<{ col: number }>) => { - if (notify.sender_channel_id !== this.replyChannel) { - return null; - } - const col: number = notify.message.col; - if (!this.colFree(col)) { - return null; - } - this.setCoin(col, Player.partner); - const nextState = this.getStateFromBoardStatus(); - return nextState === null ? `myTurn` : nextState; - } - }, - recievedRagequit: { - handle: () => { - this.caption = this.translate.instant( - `Your partner couldn't stand it anymore... You are the winner!` - ); - return `start`; - } - } - } - }; - public constructor( - private activeMeetingService: ActiveMeetingService, - public dialogRef: MatDialogRef, - private notifyService: NotifyService, - private op: OperatorService, - private translate: TranslateService + activeMeetingService: ActiveMeetingService, + notifyService: NotifyService, + op: OperatorService, + translate: TranslateService ) { - this.resetBoard(); - this.inMeeting = !!this.activeMeetingService.meetingId; + super(activeMeetingService, notifyService, op, translate); + this.reset(); } - public ngOnInit(): void { + public override ngOnInit(): void { // Setup initial values. - this.state = `start`; + super.ngOnInit(); this.caption = this.translate.instant(`Connect 4`); this.disableBoard = true; - - // Setup all subscription for needed notify messages - this.subscriptions = [ - this.notifyService.getMessageObservable(`c4_ACK`).subscribe(notify => { - if (!notify.sendByThisUser) { - this.handleEvent(`recievedACK`, notify); - } - }), - this.notifyService.getMessageObservable(`c4_ragequit`).subscribe(notify => { - if (!notify.sendByThisUser) { - this.handleEvent(`recievedRagequit`, notify); - } - }), - this.notifyService.getMessageObservable(`c4_search_request`).subscribe(notify => { - if (!notify.sendByThisUser) { - this.handleEvent(`recievedSearchRequest`, notify); - } - }), - this.notifyService.getMessageObservable(`c4_search_response`).subscribe(notify => { - if (!notify.sendByThisUser) { - this.handleEvent(`recievedSearchResponse`, notify); - } - }), - this.notifyService.getMessageObservable(`c4_turn`).subscribe(notify => { - if (!notify.sendByThisUser) { - this.handleEvent(`recievedTurn`, notify); - } - }) - ]; - } - - public ngOnDestroy(): void { - // send ragequit and unsubscribe all subscriptions. - if (this.replyChannel) { - this.notifyService.sendToChannels(`c4_ragequit`, null, this.replyChannel); - } - this.subscriptions.forEach(subscription => subscription.unsubscribe()); } /** * Resets the board. */ - private resetBoard(): void { + protected reset(): void { this.board = []; for (let i = 0; i < 7; i++) { const row = []; @@ -274,6 +74,24 @@ export class C4DialogComponent implements OnInit, OnDestroy { } } + protected startGame(message?: any): [any, State] { + if (message !== undefined) { + const state = message ? `ownMove` : `opponentMove`; + return [null, state]; + } else { + const startPlayer = Math.random() < 0.5 ? Player.thisPlayer : Player.opponent; + const startOpponent: boolean = startPlayer === Player.opponent; + const state = startPlayer === Player.thisPlayer ? `ownMove` : `opponentMove`; + return [startOpponent, state]; + } + } + + protected executeMove(move: any, ownMove?: boolean): State | null { + this.setCoin(move.col, ownMove ? Player.thisPlayer : Player.opponent); + const nextState = this.getStateFromBoardStatus(); + return nextState === null ? (ownMove ? `opponentMove` : `ownMove`) : nextState; + } + /** * Returns the class needed in the board. * @param row The row @@ -285,18 +103,11 @@ export class C4DialogComponent implements OnInit, OnDestroy { return `coin notSelected`; case Player.thisPlayer: return `coin thisPlayer`; - case Player.partner: - return `coin partner`; + case Player.opponent: + return `coin opponent`; } } - /** - * Returns the operators name. - */ - public getPlayerName(): string { - return this.op.shortName; - } - /** * Returns null, if the game is not finished. */ @@ -308,91 +119,32 @@ export class C4DialogComponent implements OnInit, OnDestroy { case BoardStatus.thisPlayer: this.caption = this.translate.instant(`You won!`); return `start`; - case BoardStatus.partner: - this.caption = this.translate.instant(`Your partner has won!`); + case BoardStatus.opponent: + this.caption = this.translate.instant(`Your opponent has won!`); return `start`; case BoardStatus.NotDecided: return null; } } - /** - * Main state machine handler. The current state handler will be called with - * the given event. If the handler returns a state (and not null), this will be - * the next state. The state enter method will be called. - * @param e The event for the statemachine. - * @param data Additional data for the handler. - */ - public handleEvent(e: StateEvent, data?: any): void { - let action: SMAction | null = null; - if (this.SM[this.state] && this.SM[this.state]![e]) { - action = this.SM[this.state]![e] as SMAction; - const nextState = action.handle(data); - if (nextState !== null) { - this.state = nextState; - if (this[`enter_${nextState}`]) { - this[`enter_${nextState}`](); - } - } - } - } - /** * Handler for clicks on the field. - * @param row the row clicked * @param col the col clicked */ - public clickField(row: number, col: number): void { - if (!this.disableBoard) { - this.handleEvent(`fieldClicked`, { row, col }); + public clickField(col: number): void { + if (!this.disableBoard && this.colFree(col)) { + this.handleEvent(`executedMove`, { col }); } } - // Enter state methods - /** - * Resets all attributes of the state machine. - */ - public enter_start(): void { + public override enter_start(): void { + super.enter_start(); this.disableBoard = true; - this.replyChannel = null; - this.partnerName = null; - } - - /** - * Sends a search request for other players. - */ - public enter_search(): void { - this.caption = this.translate.instant(`Searching for players ...`); - this.notifyService.sendToMeeting(`c4_search_request`, { name: this.getPlayerName() }); - } - - /** - * Sends a search response for a previous request. - * Also sets up a timeout to go back into the search state. - */ - public enter_waitForResponse(): void { - this.caption = this.translate.instant(`Wait for response ...`); - this.notifyService.sendToChannels(`c4_search_response`, { name: this.getPlayerName() }, this.replyChannel!); - if (this.waitTimout) { - clearTimeout(this.waitTimout); - } - this.waitTimout = setTimeout(() => { - this.handleEvent(`waitTimeout`); - }, 5000); } - /** - * Sets the caption. - */ - public enter_myTurn(): void { - this.caption = this.translate.instant(`It's your turn!`); - } - - /** - * Sets the caption. - */ - public enter_foreignTurn(): void { - this.caption = this.translate.instant(`It's your partners turn`); + public override enter_search(): void { + super.enter_search(); + this.disableBoard = false; } // Board function @@ -434,7 +186,7 @@ export class C4DialogComponent implements OnInit, OnDestroy { } } if (won !== Player.noPlayer) { - return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.partner; + return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.opponent; } } } @@ -448,7 +200,7 @@ export class C4DialogComponent implements OnInit, OnDestroy { } } if (won !== Player.noPlayer) { - return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.partner; + return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.opponent; } } } @@ -462,7 +214,7 @@ export class C4DialogComponent implements OnInit, OnDestroy { } } if (won !== Player.noPlayer) { - return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.partner; + return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.opponent; } } } @@ -476,7 +228,7 @@ export class C4DialogComponent implements OnInit, OnDestroy { } } if (won !== Player.noPlayer) { - return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.partner; + return won === Player.thisPlayer ? BoardStatus.thisPlayer : BoardStatus.opponent; } } } diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/chess-dialog.module.ts b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/chess-dialog.module.ts new file mode 100644 index 0000000000..3125c69de8 --- /dev/null +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/chess-dialog.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { OpenSlidesTranslationModule } from 'src/app/site/modules/translations'; + +import { ChessDialogComponent } from './components/chess-dialog/chess-dialog.component'; + +@NgModule({ + declarations: [ChessDialogComponent], + imports: [CommonModule, MatButtonModule, MatIconModule, MatDialogModule, OpenSlidesTranslationModule.forChild()] +}) +export class ChessDialogModule { + public static readonly label = `Play chess`; + public static getComponent(): typeof ChessDialogComponent { + return ChessDialogComponent; + } +} diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.html b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.html new file mode 100644 index 0000000000..8093e628c5 --- /dev/null +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.html @@ -0,0 +1,21 @@ + +
+

{{ caption | translate }}

+ +
+ +
{{ 'Playing against' | translate }} {{ opponentName }}
+
+
+ +
+ +
+
+ {{ 'Open a meeting to play chess' | translate }} +
+
diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.scss b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.scss new file mode 100644 index 0000000000..088de9d5bb --- /dev/null +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.scss @@ -0,0 +1,19 @@ +/** cm-chessboard */ +@import '~cm-chessboard/assets/_chessboard-theme.scss'; +@import '~cm-chessboard/assets/chessboard.scss'; +@import '~cm-chessboard/assets/extensions/promotion-dialog/promotion-dialog.scss'; + +.flex-container { + display: flex; + align-items: center; + flex-direction: row; + + .left-align { + flex: 1; + } +} + +.mat-dialog-content { + max-height: 95vh; + overflow: hidden; +} diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.spec.ts b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.spec.ts new file mode 100644 index 0000000000..667acf018e --- /dev/null +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChessDialogComponent } from './chess-dialog.component'; + +xdescribe(`ChessDialogComponent`, () => { + let component: ChessDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ChessDialogComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ChessDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it(`should create`, () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.ts b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.ts new file mode 100644 index 0000000000..f934a0ebc7 --- /dev/null +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/components/chess-dialog/chess-dialog.component.ts @@ -0,0 +1,159 @@ +import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Chess, EVENT_TYPE } from 'cm-chess/src/Chess'; +import { BORDER_TYPE, Chessboard, COLOR, FEN, INPUT_EVENT_TYPE } from 'cm-chessboard/src/Chessboard'; +import { PromotionDialog } from 'cm-chessboard/src/extensions/promotion-dialog/PromotionDialog'; + +import { BaseGameDialogComponent, State } from '../../../../components/base-game-dialog/base-game-dialog'; + +@Component({ + selector: `os-chess-dialog`, + templateUrl: `./chess-dialog.component.html`, + styleUrls: [`./chess-dialog.component.scss`], + encapsulation: ViewEncapsulation.None +}) +export class ChessDialogComponent extends BaseGameDialogComponent implements OnInit, OnDestroy { + protected prefix = `chess`; + + /** + * The chess engine + */ + private chess: Chess = new Chess(); + + /** + * The chess board + */ + private board: Chessboard = null; + + @ViewChild(`chessboard`, { static: true }) + public boardContainer: ElementRef; + + /** + * Color of this user + */ + private ownColor: COLOR = COLOR.white; + + public override ngOnInit(): void { + super.ngOnInit(); + this.board = new Chessboard(this.boardContainer.nativeElement, { + position: FEN.start, + language: this.translate.currentLang == `de` ? `de` : `en`, + assetsUrl: `./chess/`, + style: { + borderType: BORDER_TYPE.frame + }, + extensions: [{ class: PromotionDialog }] + }); + this.chess.addObserver(({ type }) => { + if (type === EVENT_TYPE.initialized || type === EVENT_TYPE.legalMove) { + this.board.setPosition(this.chess.fen(), true); + } + }); + this.caption = this.translate.instant(`Chess`); + } + + protected override reset(): void { + this.chess.load(FEN.start); + } + + protected startGame(message?: any): [any, State] { + let result: any = null; + if (message !== undefined) { + this.ownColor = message; + } else { + this.ownColor = Math.random() < 0.5 ? COLOR.white : COLOR.black; + result = this.ownColor === COLOR.white ? COLOR.black : COLOR.white; + } + this.board.setOrientation(this.ownColor); + return [result, this.ownColor == COLOR.white ? `ownMove` : `opponentMove`]; + } + + protected executeMove(move: any, ownMove?: boolean): State | null { + let moveStr = move.piece + move.squareFrom + move.squareTo; + if (move.promotion) { + moveStr += move.promotion.charAt(1); + } + this.chess.move(moveStr); + const nextState = this.getStateFromBoardStatus(); + return nextState === null ? (ownMove ? `opponentMove` : `ownMove`) : nextState; + } + + private enableMoveInput(): void { + this.board.enableMoveInput(event => { + if (event.type == INPUT_EVENT_TYPE.moveInputStarted) { + return true; + } else if (event.type == INPUT_EVENT_TYPE.validateMoveInput) { + const result = this.chess.validateMove(event.piece + event.squareFrom + event.squareTo); + if (result) { + const move = { piece: event.piece, squareFrom: event.squareFrom, squareTo: event.squareTo }; + if (this.isPromotionMove(event)) { + this.board.showPromotionDialog(event.squareTo, this.ownColor, result => { + if (result?.piece) { + this.handleEvent(`executedMove`, { ...move, promotion: result.piece }); + } else { + this.board.setPosition(this.chess.fen(), true); + } + }); + } else { + this.handleEvent(`executedMove`, move); + } + } + return result; + } + }, this.ownColor); + } + + private isPromotionMove(event): boolean { + return (event.squareTo.charAt(1) === `1` || event.squareTo.charAt(1) === `8`) && event.piece.charAt(1) === `p`; + } + + /** + * Returns null if the game is not finished. + */ + private getStateFromBoardStatus(): State | null { + if (this.chess.gameOver()) { + if (this.chess.inCheckmate()) { + if (this.chess.turn() === this.ownColor) { + this.caption = this.translate.instant(`Checkmate! You lost!`); + } else { + this.caption = this.translate.instant(`Checkmate! You won!`); + } + } else if (this.chess.inStalemate()) { + this.caption = this.translate.instant(`Stalemate! It's a draw!`); + } else if (this.chess.inThreefoldRepetition()) { + this.caption = this.translate.instant(`Threefold repetition! It's a draw!`); + } else if (this.chess.insufficientMaterial()) { + this.caption = this.translate.instant(`Insufficient material! It's a draw!`); + } else if (this.chess.inDraw()) { + this.caption = this.translate.instant(`It's a draw!`); + } + return `start`; + } else { + return null; + } + } + + // Enter state methods + /** + * Resets all attributes of the state machine. + */ + public override enter_start(): void { + super.enter_start(); + this.board.disableMoveInput(); + } + + /** + * Sets the caption. + */ + public override enter_ownMove(): void { + super.enter_ownMove(); + this.enableMoveInput(); + } + + /** + * Sets the caption. + */ + public override enter_opponentMove(): void { + super.enter_opponentMove(); + this.board.disableMoveInput(); + } +} diff --git a/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/index.ts b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/index.ts new file mode 100644 index 0000000000..bbb1ab286d --- /dev/null +++ b/client/src/app/ui/modules/sidenav/modules/easter-egg/modules/chess-dialog/index.ts @@ -0,0 +1 @@ +export * from './chess-dialog.module'; From 412525f8283d8ca0a1c4ef3bb1edc93989e50a7a Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:15:08 +0200 Subject: [PATCH 06/46] Fix search for category in motion edit (#2817) --- client/src/app/infrastructure/definitions/tree.ts | 2 ++ .../motion-content/motion-content.component.html | 1 + .../modules/sorting-tree/services/tree.service.ts | 15 ++++++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/client/src/app/infrastructure/definitions/tree.ts b/client/src/app/infrastructure/definitions/tree.ts index 03f19767c4..d950bacdac 100644 --- a/client/src/app/infrastructure/definitions/tree.ts +++ b/client/src/app/infrastructure/definitions/tree.ts @@ -21,6 +21,7 @@ export interface TreeNodeWithoutItem extends TreeIdNode { export interface OSTreeNode extends TreeNodeWithoutItem { item: T; children?: OSTreeNode[]; + toString: () => string; } /** @@ -46,6 +47,7 @@ export type FlatNode = T & { expandable: boolean; id: number; filtered?: boolean; + toString: () => string; }; function isIdentifiedItemNode(obj: any): boolean { diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-content/motion-content.component.html b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-content/motion-content.component.html index 74912c2ded..82cd9fdf1a 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-content/motion-content.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-content/motion-content.component.html @@ -182,6 +182,7 @@ formControlName="category_id" placeholder="{{ 'Category' | translate }}" [repo]="categoryRepo" + [excludeIds]="true" > diff --git a/client/src/app/ui/modules/sorting/modules/sorting-tree/services/tree.service.ts b/client/src/app/ui/modules/sorting/modules/sorting-tree/services/tree.service.ts index 84fb1933bd..ea6f5d0135 100644 --- a/client/src/app/ui/modules/sorting/modules/sorting-tree/services/tree.service.ts +++ b/client/src/app/ui/modules/sorting/modules/sorting-tree/services/tree.service.ts @@ -31,7 +31,10 @@ export class TreeService { name: item.getTitle(), id: item.id, item, - children + children, + toString: function () { + return this.item.toString(); + } }; } @@ -352,7 +355,10 @@ export class TreeService { isSeen: true, expandable: false, id: item.id, - position: index + oldMaxPosition + 1 + position: index + oldMaxPosition + 1, + toString: function () { + return this.item.toString(); + } })); return tree.concat(items); } @@ -406,7 +412,10 @@ export class TreeService { expandable: !!children, isExpanded: !!children, level, - isSeen: true + isSeen: true, + toString: function () { + return this.item.toString(); + } }; return new Proxy(node, { get: (target: FlatNode, property: keyof Identifiable & Displayable & T) => { From c0635604e6349b2bfa502ed14c2a2763bcde0ce8 Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:26:18 +0200 Subject: [PATCH 07/46] Testing meeting settings definition service (#2795) * Delete two unused services * MeetingSettingsDefinitionService test --- client/cli/generate-settings-defaults.ts | 2 +- .../autoupdate-adapter.service.spec.ts | 16 -- .../gateways/autoupdate-adapter.service.ts | 8 - .../applause-bar-display.service.spec.ts | 16 -- .../applause-bar-display.service.ts | 8 - .../meeting-settings-group-list.component.ts | 2 +- ...eeting-settings-definition.service.spec.ts | 247 +++++++++++++++++- .../meeting-settings-definition.service.ts | 92 ++++--- 8 files changed, 295 insertions(+), 96 deletions(-) delete mode 100644 client/src/app/gateways/autoupdate-adapter.service.spec.ts delete mode 100644 client/src/app/gateways/autoupdate-adapter.service.ts delete mode 100644 client/src/app/site/pages/meetings/pages/interaction/modules/interaction-container/components/applause-bar-display.service.spec.ts delete mode 100644 client/src/app/site/pages/meetings/pages/interaction/modules/interaction-container/components/applause-bar-display.service.ts diff --git a/client/cli/generate-settings-defaults.ts b/client/cli/generate-settings-defaults.ts index 0100020b33..6245fb98a0 100644 --- a/client/cli/generate-settings-defaults.ts +++ b/client/cli/generate-settings-defaults.ts @@ -26,7 +26,7 @@ const FILE_TEMPLATE = dedent` const provider = new MeetingSettingsDefinitionService(); let content = FILE_TEMPLATE + '\n'; - for (const [key, value] of Object.entries(provider.getSettingsMap())) { + for (const [key, value] of Object.entries(provider.settingsMap)) { const defaultValue = meeting[key].default; if (defaultValue !== undefined) { provider.validateDefault(key, defaultValue); diff --git a/client/src/app/gateways/autoupdate-adapter.service.spec.ts b/client/src/app/gateways/autoupdate-adapter.service.spec.ts deleted file mode 100644 index 952c6df470..0000000000 --- a/client/src/app/gateways/autoupdate-adapter.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { AutoupdateAdapterService } from './autoupdate-adapter.service'; - -xdescribe(`AutoupdateAdapterService`, () => { - let service: AutoupdateAdapterService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(AutoupdateAdapterService); - }); - - it(`should be created`, () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/client/src/app/gateways/autoupdate-adapter.service.ts b/client/src/app/gateways/autoupdate-adapter.service.ts deleted file mode 100644 index 440cc0e364..0000000000 --- a/client/src/app/gateways/autoupdate-adapter.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: `root` -}) -export class AutoupdateAdapterService { - constructor() {} -} diff --git a/client/src/app/site/pages/meetings/pages/interaction/modules/interaction-container/components/applause-bar-display.service.spec.ts b/client/src/app/site/pages/meetings/pages/interaction/modules/interaction-container/components/applause-bar-display.service.spec.ts deleted file mode 100644 index 6be545b73d..0000000000 --- a/client/src/app/site/pages/meetings/pages/interaction/modules/interaction-container/components/applause-bar-display.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { ApplauseBarDisplayService } from './applause-bar-display.service'; - -xdescribe(`ApplauseBarDisplayService`, () => { - let service: ApplauseBarDisplayService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(ApplauseBarDisplayService); - }); - - it(`should be created`, () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/client/src/app/site/pages/meetings/pages/interaction/modules/interaction-container/components/applause-bar-display.service.ts b/client/src/app/site/pages/meetings/pages/interaction/modules/interaction-container/components/applause-bar-display.service.ts deleted file mode 100644 index a73d8bd57c..0000000000 --- a/client/src/app/site/pages/meetings/pages/interaction/modules/interaction-container/components/applause-bar-display.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: `root` -}) -export class ApplauseBarDisplayService { - constructor() {} -} diff --git a/client/src/app/site/pages/meetings/pages/meeting-settings/pages/meeting-settings-group-list/components/meeting-settings-group-list/meeting-settings-group-list.component.ts b/client/src/app/site/pages/meetings/pages/meeting-settings/pages/meeting-settings-group-list/components/meeting-settings-group-list/meeting-settings-group-list.component.ts index 6a533e736e..d4a650dd52 100644 --- a/client/src/app/site/pages/meetings/pages/meeting-settings/pages/meeting-settings-group-list/components/meeting-settings-group-list/meeting-settings-group-list.component.ts +++ b/client/src/app/site/pages/meetings/pages/meeting-settings/pages/meeting-settings-group-list/components/meeting-settings-group-list/meeting-settings-group-list.component.ts @@ -24,7 +24,7 @@ export class MeetingSettingsGroupListComponent extends BaseMeetingComponent { ) { super(componentServiceCollector, translate); - this.groups = this.meetingSettingsDefinitionProvider.getSettings(); + this.groups = this.meetingSettingsDefinitionProvider.settings; } /** diff --git a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definition.service.spec.ts b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definition.service.spec.ts index 0d48fb545f..30bdf3ee4c 100644 --- a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definition.service.spec.ts +++ b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definition.service.spec.ts @@ -1,16 +1,253 @@ import { TestBed } from '@angular/core/testing'; +import { meetingSettingsDefaults } from 'src/app/domain/definitions/meeting-settings-defaults'; +import { Settings } from 'src/app/domain/models/meetings/meeting'; +import { Group } from 'src/app/domain/models/users/group'; -import { MeetingSettingsDefinitionService } from './meeting-settings-definition.service'; +import { MeetingSettingsDefinitionService, SettingsMap } from './meeting-settings-definition.service'; +import { meetingSettings, SettingsGroup, SettingsItem, SettingsType } from './meeting-settings-definitions'; -xdescribe(`MeetingSettingsDefinitionService`, () => { +const fakeSettings: SettingsGroup[] = [ + { + label: `Test setting group`, + icon: `test`, + subgroups: [ + { + label: `Test subgroup`, + settings: [ + { + key: `one` as keyof Settings, + label: `Setting one`, + type: `integer` + }, + { + key: [`two` as keyof Settings, `three` as keyof Settings], + label: `Setting two and three`, + type: `daterange` + }, + { + key: `four` as keyof Settings, + label: `Setting four`, + type: `datetime` + }, + { + key: `five` as keyof Settings, + label: `Setting five`, + type: `date` + }, + { + key: `a` as keyof Settings, + label: `Setting a` + }, + { + key: `b` as keyof Settings, + label: `Setting b`, + type: `string` + }, + { + key: `c` as keyof Settings, + label: `Setting c`, + type: `text` + }, + { + key: `d` as keyof Settings, + label: `Setting d`, + type: `markupText` + }, + { + key: `e` as keyof Settings, + label: `Setting e`, + type: `email` + }, + { + key: `bool` as keyof Settings, + label: `Boolean setting`, + type: `boolean` + }, // + { + key: `groups` as keyof Settings, + label: `Group setting`, + type: `groups` + }, + { + key: `translations` as keyof Settings, + label: `Translation setting`, + type: `translations` + }, + { + key: `ranking` as keyof Settings, + label: `Ranking setting`, + type: `ranking` + }, + { + key: `static` as keyof Settings, + label: `Static choice setting`, + type: `choice`, + choices: { + a: `A`, + b: `B`, + c: `C` + } + }, + { + key: `dynamic` as keyof Settings, + label: `Dynamic choice setting`, + type: `choice`, + choicesFunc: { + collection: `model`, + idKey: `id`, + labelKey: `stuff` + } + } + ] + } + ] + } +]; + +const fakeSettingsMap: { [key: string]: SettingsItem } = fakeSettings + .flatMap(group => group.subgroups.flatMap(subgroup => subgroup.settings)) + .filter(setting => !!setting) + .mapToObject(setting => + (Array.isArray(setting.key) ? setting.key : [setting.key]).mapToObject(key => ({ [key]: setting })) + ); + +const fakeSettingsDefaults: { [key: string]: any } = { + ...[`one`, `two`, `four`, `five`].mapToObject((prop, index) => ({ [prop]: index + 1 })), + bool: true, + ...[`a`, `b`, `c`, `d`].mapToObject(letter => ({ [letter]: letter })), + e: `e.mail@email.mail`, + groups: [new Group()], + translations: { a: `b`, c: `d`, e: `f` }, + ranking: [{ id: 1, entry: `a`, allocation: 2 }], + static: `b`, + dynamic: `s` +}; + +const fakeBrokenSettingsDefaults: { [key: string]: any } = { + ...[`one`, `two`, `four`, `five`].mapToObject(letter => ({ [letter]: letter })), + bool: `not a bool`, + ...[`a`, `b`, `c`, `d`].mapToObject((prop, index) => ({ [prop]: index + 1 })), + e: 3, + groups: true, + translations: `abcdefghijklmnopqrstuvwxyz`, + ranking: 123456, + static: [`d`], + dynamic: 4 +}; + +const typeToDefault: { [type in SettingsType]: any } = { + integer: 0, + boolean: false, + groups: [], + translations: {}, + ranking: [], + choice: null, + date: null, + datetime: null, + daterange: [null, null], + string: ``, + text: ``, + markupText: ``, + email: `` +}; + +describe(`MeetingSettingsDefinitionService`, () => { let service: MeetingSettingsDefinitionService; + function mockGetters(settingsDefaults = fakeSettingsDefaults): void { + spyOnProperty(service, `settings`).and.returnValue(fakeSettings); + spyOnProperty(service, `settingsDefaults`).and.returnValue(settingsDefaults); + spyOnProperty(service, `settingsMap`).and.returnValue(fakeSettingsMap as SettingsMap); + } + beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [MeetingSettingsDefinitionService] + }); service = TestBed.inject(MeetingSettingsDefinitionService); }); - it(`should be created`, () => { - expect(service).toBeTruthy(); + it(`test getters`, () => { + expect(service.settings).toBe(meetingSettings); + expect(service.settingsDefaults).toBe(meetingSettingsDefaults); + expect(meetingSettings.length).toBeGreaterThan(0); + expect(service.settingsMap).toBeTruthy(); + const indices = Array.from({ length: 4 }, () => Math.floor(Math.random() * 100)); + indices[0] = indices[0] % meetingSettings.length; + const values: any[] = [meetingSettings[indices[0]]]; + const properties = [`subgroups`, `settings`, `key`]; + for (let i = 1; i < 4; i++) { + if (Array.isArray(values[i - 1][properties[i - 1]])) { + indices[i] = indices[i] % values[i - 1][properties[i - 1]].length; + } + values.push( + Array.isArray(values[i - 1][properties[i - 1]]) + ? values[i - 1][properties[i - 1]][indices[i]] + : values[i - 1][properties[i - 1]] + ); + } + expect(service.settingsMap[values[3]]).toBe(values[2]); + }); + + it(`test normal get methods`, () => { + mockGetters(); + expect(service.getSettingsGroup(`Test setting group`)).toBe(fakeSettings[0]); + expect(service.getSettingsGroup(`test setting group`)).toBe(fakeSettings[0]); + expect(service.getSettingsKeys()).toEqual( + fakeSettings.flatMap(group => + group.subgroups.flatMap(subgroup => subgroup.settings.flatMap(setting => setting.key)) + ) + ); }); + + for (const haveDefaults of [true, false]) { + for (const setting of fakeSettings[0].subgroups[0].settings) { + it(`test getDefaultValue for ${setting.type ?? `no given type`}${ + haveDefaults ? `` : ` if there's no defaults` + }`, () => { + mockGetters(haveDefaults ? fakeSettingsDefaults : {}); + expect(service.getDefaultValue(setting)).toEqual( + haveDefaults + ? setting.type === `daterange` + ? [2, null] + : fakeSettingsDefaults[Array.isArray(setting.key) ? setting.key[0] : setting.key] + : typeToDefault[setting.type ?? `string`] + ); + }); + const keys = Array.isArray(setting.key) ? setting.key : [setting.key]; + for (let i = 0; i < keys.length; i++) { + it(`test getDefaultValue for ${setting.type ?? `no given type`} with settings keys${ + haveDefaults ? `` : ` if there's no defaults` + } ${i + 1}`, () => { + mockGetters(haveDefaults ? fakeSettingsDefaults : {}); + expect(service.getDefaultValue(keys[i])).toEqual( + haveDefaults + ? setting.type === `daterange` + ? [2, null] + : fakeSettingsDefaults[Array.isArray(setting.key) ? setting.key[0] : setting.key] + : typeToDefault[setting.type ?? `string`] + ); + }); + } + } + } + + for (const type of Object.keys(typeToDefault)) { + const setting = Object.values(fakeSettingsMap).find(setting => setting.type === type); + if (setting) { + it(`test getDefaultValueForType for ${type}`, () => { + mockGetters(); + expect(service.getDefaultValueForType(setting)).toEqual(typeToDefault[type]); + }); + } + const key = Array.isArray(setting.key) ? setting.key[0] : setting.key; + it(`test validateDefault for ${type}`, () => { + mockGetters(); + expect(() => service.validateDefault(key, fakeSettingsDefaults[key])).not.toThrowError(); + }); + it(`test validateDefault for ${type} with broken default`, () => { + mockGetters(fakeBrokenSettingsDefaults); + expect(() => service.validateDefault(key, fakeBrokenSettingsDefaults[key])).toThrowError(); + }); + } }); diff --git a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definition.service.ts b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definition.service.ts index fb9cb0c081..c26433208a 100644 --- a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definition.service.ts +++ b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definition.service.ts @@ -4,80 +4,62 @@ import { Settings } from 'src/app/domain/models/meetings/meeting'; import { meetingSettingsDefaults } from '../../../../../domain/definitions/meeting-settings-defaults'; import { meetingSettings, SettingsGroup, SettingsItem } from './meeting-settings-definitions'; -type SettingsMap = { [key in keyof Settings]: SettingsItem }; +export type SettingsMap = { [key in keyof Settings]: SettingsItem }; @Injectable({ providedIn: `root` }) export class MeetingSettingsDefinitionService { - private readonly _settingsMap: SettingsMap; + public get settings(): SettingsGroup[] { + return meetingSettings; + } - public constructor() { - this._settingsMap = this.createSettingsMap(); + public get settingsDefaults(): { + [key: string]: any; + } { + return meetingSettingsDefaults; } - public validateDefault(settingKey: keyof Settings, defaultValue: any): void { - const setting = this.settingsMap[settingKey]; - if ( - ((!setting.type || setting.type === `text`) && typeof defaultValue !== `string`) || - (setting.type === `integer` && typeof defaultValue !== `number`) || - (setting.type === `boolean` && typeof defaultValue !== `boolean`) - ) { - throw new Error(`Invalid default for ${setting.key}: ${defaultValue} (${typeof defaultValue})`); - } - if (setting.type === `choice` && setting.choices && !setting.choices.hasOwnProperty(defaultValue)) { - throw new Error( - `Invalid default for ${setting.key}: ${defaultValue} (valid choices: ${Object.keys(setting.choices)})` - ); - } + public get settingsMap(): SettingsMap { + return this._settingsMap; } - public getSettings(): SettingsGroup[] { - return meetingSettings; + private readonly _settingsMap: SettingsMap; + + public constructor() { + this._settingsMap = this.createSettingsMap(); } public getSettingsGroup(name: string): SettingsGroup | undefined { - return meetingSettings.find(group => group.label.toLowerCase() === name); + return this.settings.find(group => group.label.toLowerCase() === name.toLowerCase()); } public getSettingsKeys(): (keyof Settings)[] { return Object.keys(this.settingsMap) as (keyof Settings)[]; } - public getSettingsMap(): { [key in keyof Settings]: SettingsItem } { - return this.settingsMap; - } - public getDefaultValue(setting: keyof Settings | SettingsItem): any { const settingItem = typeof setting === `string` ? this.settingsMap[setting] : setting; return this.getDefaultValueForItem(settingItem) ?? this.getDefaultValueForType(settingItem); } - private getDefaultValueForItem(item: SettingsItem): any { - const isArray = Array.isArray(item.key); - const value = meetingSettingsDefaults[isArray ? item.key[0] : (item.key as keyof Settings)]; - if (item.type === `daterange`) { - return [value ?? null, meetingSettingsDefaults[item.key[1]] ?? null]; - } - return value; - } - public getDefaultValueForType(setting: SettingsItem): any { switch (setting.type) { case `integer`: return 0; case `boolean`: return false; - case `choice`: - return null; - case `groups`: case `translations`: + return {}; + case `groups`: case `ranking`: return []; + case `choice`: + case `date`: case `datetime`: return null; case `daterange`: - return [Date.now(), Date.now()]; + return [null, null]; case `string`: case `text`: case `markupText`: @@ -87,8 +69,36 @@ export class MeetingSettingsDefinitionService { } } - private get settingsMap(): SettingsMap { - return this._settingsMap; + public validateDefault(settingKey: keyof Settings, defaultValue: any): void { + const setting = this.settingsMap[settingKey]; + if ( + ((!setting.type || [`string`, `text`, `email`, `markupText`].includes(setting.type)) && + typeof defaultValue !== `string`) || + ([`integer`, `date`, `datetime`, `daterange`].includes(setting.type) && typeof defaultValue !== `number`) || + (setting.type === `boolean` && typeof defaultValue !== `boolean`) + ) { + throw new Error(`Invalid default for ${setting.key}: ${defaultValue} (${typeof defaultValue})`); + } + if (setting.type === `choice` && setting.choices && !setting.choices.hasOwnProperty(defaultValue)) { + throw new Error( + `Invalid default for ${setting.key}: ${defaultValue} (valid choices: ${Object.keys(setting.choices)})` + ); + } + if ([`ranking`, `groups`].includes(setting.type) && !Array.isArray(defaultValue)) { + throw new Error(`Invalid default for ${setting.key}: ${defaultValue} is not an array`); + } + if (setting.type === `translations` && typeof defaultValue !== `object`) { + throw new Error(`Invalid default for ${setting.key}: ${defaultValue} is not an object`); + } + } + + private getDefaultValueForItem(item: SettingsItem): any { + const isArray = Array.isArray(item.key); + const value = this.settingsDefaults[isArray ? item.key[0] : (item.key as keyof Settings)]; + if (item.type === `daterange`) { + return [value ?? null, this.settingsDefaults[item.key[1]] ?? null]; + } + return value; } private validateSetting(setting: SettingsItem): void { @@ -101,7 +111,7 @@ export class MeetingSettingsDefinitionService { private createSettingsMap(): SettingsMap { const localSettingsMap: any = {}; - for (const group of meetingSettings) { + for (const group of this.settings) { for (const subgroup of group.subgroups) { for (const setting of subgroup.settings) { this.validateSetting(setting); From 7a71c20293fa66f48f6cd742d20f3a68c45ee599 Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:26:27 +0200 Subject: [PATCH 08/46] Add tests for main menu service (#2830) --- .../services/main-menu.service.spec.ts | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/client/src/app/site/pages/meetings/services/main-menu.service.spec.ts b/client/src/app/site/pages/meetings/services/main-menu.service.spec.ts index 5387619716..76ea3ba5db 100644 --- a/client/src/app/site/pages/meetings/services/main-menu.service.spec.ts +++ b/client/src/app/site/pages/meetings/services/main-menu.service.spec.ts @@ -1,16 +1,54 @@ import { TestBed } from '@angular/core/testing'; +import { firstValueFrom, skip } from 'rxjs'; -import { MainMenuService } from './main-menu.service'; +import { MainMenuEntry, MainMenuService } from './main-menu.service'; -xdescribe(`MainMenuService`, () => { +const menuEntries: MainMenuEntry[] = [ + { route: `page1`, displayName: `A page`, icon: `add`, weight: 300 }, + { route: `start`, displayName: `Start page`, icon: `start`, weight: 100 }, + { route: `page2`, displayName: `Another page`, icon: `search`, weight: 200 } +]; + +function getNumberOfTogglesPromise(service: MainMenuService, requiredToggleNumber: number): Promise { + return firstValueFrom(service.toggleMenuSubject.pipe(skip(requiredToggleNumber - 1))); +} + +describe(`MainMenuService`, () => { let service: MainMenuService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [MainMenuService] + }); + service = TestBed.inject(MainMenuService); }); - it(`should be created`, () => { - expect(service).toBeTruthy(); + it(`test registerEntries method`, () => { + service.registerEntries([menuEntries[0]]); + expect(service.entries).toEqual([menuEntries[0]]); + }); + + it(`test registerEntries method with multiple entries at once`, () => { + service.registerEntries(menuEntries.slice(1)); + expect(service.entries).toEqual(menuEntries.slice(1)); + }); + + it(`test consecutive calls of registerEntries method`, () => { + service.registerEntries(menuEntries.slice(0, 2)); + service.registerEntries([menuEntries[2]]); + expect(service.entries).toEqual(menuEntries.sort((a, b) => a.weight - b.weight)); }); + + for (let i = 1; i <= 5; i++) { + it(`test toggleSubject with ${i} call(s)`, async () => { + const promise1 = getNumberOfTogglesPromise(service, i); + const promise2 = getNumberOfTogglesPromise(service, i + 1); + for (let j = 0; j < i; j++) { + service.toggleMenu(); + } + await expectAsync(promise1).toBeResolved(); + await expectAsync(promise2).toBePending(); + }); + } }); From 21e5f12825c4d8301b2fdd04e30c6636ad051248 Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:26:38 +0200 Subject: [PATCH 09/46] Add tests for duration service (#2831) * Delete unused service * Add tests for duration service --- .../committee-detail.module.ts | 9 +---- .../committee-detail-service.module.ts | 4 --- ...ttee-detail-shared-context.service.spec.ts | 16 --------- ...committee-detail-shared-context.service.ts | 15 --------- .../index.ts | 1 - .../pages/committee-detail/services/index.ts | 2 -- .../site/services/duration.service.spec.ts | 33 ++++++++++++++++--- .../src/app/site/services/duration.service.ts | 7 +++- 8 files changed, 36 insertions(+), 51 deletions(-) delete mode 100644 client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-service.module.ts delete mode 100644 client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/committee-detail-shared-context.service.spec.ts delete mode 100644 client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/committee-detail-shared-context.service.ts delete mode 100644 client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/index.ts delete mode 100644 client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/index.ts diff --git a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/committee-detail.module.ts b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/committee-detail.module.ts index 0fd76dc279..ad976f7a34 100644 --- a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/committee-detail.module.ts +++ b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/committee-detail.module.ts @@ -5,16 +5,9 @@ import { RouterModule } from '@angular/router'; import { CommitteeCommonServiceModule } from '../../services/committee-common-service.module'; import { CommitteeDetailRoutingModule } from './committee-detail-routing.module'; import { CommitteeDetailComponent } from './components/committee-detail/committee-detail.component'; -import { CommitteeDetailServiceModule } from './services'; @NgModule({ declarations: [CommitteeDetailComponent], - imports: [ - CommonModule, - CommitteeDetailRoutingModule, - CommitteeDetailServiceModule, - CommitteeCommonServiceModule, - RouterModule - ] + imports: [CommonModule, CommitteeDetailRoutingModule, CommitteeCommonServiceModule, RouterModule] }) export class CommitteeDetailModule {} diff --git a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-service.module.ts b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-service.module.ts deleted file mode 100644 index e16663f363..0000000000 --- a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-service.module.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { NgModule } from '@angular/core'; - -@NgModule() -export class CommitteeDetailServiceModule {} diff --git a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/committee-detail-shared-context.service.spec.ts b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/committee-detail-shared-context.service.spec.ts deleted file mode 100644 index cdf0eb26e6..0000000000 --- a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/committee-detail-shared-context.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { CommitteeDetailSharedContextService } from './committee-detail-shared-context.service'; - -xdescribe(`CommitteeDetailSharedContextService`, () => { - let service: CommitteeDetailSharedContextService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(CommitteeDetailSharedContextService); - }); - - it(`should be created`, () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/committee-detail-shared-context.service.ts b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/committee-detail-shared-context.service.ts deleted file mode 100644 index c9686d2afb..0000000000 --- a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/committee-detail-shared-context.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; - -import { CommitteeDetailServiceModule } from '../committee-detail-service.module'; - -@Injectable({ - providedIn: CommitteeDetailServiceModule -}) -export class CommitteeDetailSharedContextService { - public readonly currentCommitteeId = new BehaviorSubject(null); - - public constructor() { - console.log(`create constructor`); - } -} diff --git a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/index.ts b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/index.ts deleted file mode 100644 index c064f594d7..0000000000 --- a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/committee-detail-shared-context.service/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './committee-detail-shared-context.service'; diff --git a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/index.ts b/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/index.ts deleted file mode 100644 index 6c0bdfecc9..0000000000 --- a/client/src/app/site/pages/organization/pages/committees/pages/committee-detail/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './committee-detail-service.module'; -export * from './committee-detail-shared-context.service'; diff --git a/client/src/app/site/services/duration.service.spec.ts b/client/src/app/site/services/duration.service.spec.ts index 0ea9b47b03..5430d8225b 100644 --- a/client/src/app/site/services/duration.service.spec.ts +++ b/client/src/app/site/services/duration.service.spec.ts @@ -2,15 +2,40 @@ import { TestBed } from '@angular/core/testing'; import { DurationService } from './duration.service'; -xdescribe(`DurationService`, () => { +describe(`DurationService`, () => { let service: DurationService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [DurationService] }); service = TestBed.inject(DurationService); }); - it(`should be created`, () => { - expect(service).toBeTruthy(); + it(`test stringToDuration`, () => { + expect(service.stringToDuration(`1:23h`, `h`)).toBe(83); + expect(service.stringToDuration(`1:23m`, `m`)).toBe(83); + expect(service.stringToDuration(`123m`, `m`)).toBe(123); + expect(service.stringToDuration(`1:23`, `h`)).toBe(83); + expect(service.stringToDuration(`1:23`, `m`)).toBe(83); + expect(service.stringToDuration(`123`, `m`)).toBe(123); + expect(service.stringToDuration()).toBe(0); + expect(service.stringToDuration(`1:23 h`, `h`)).toBe(83); + }); + + it(`test durationToStringWithHours`, () => { + expect(service.durationToStringWithHours(83)).toBe(`0:01:23 h`); + expect(service.durationToStringWithHours(7345)).toBe(`2:02:25 h`); + expect(service.durationToStringWithHours(-1)).toBe(`-0:00:01 h`); + expect(service.durationToStringWithHours(undefined)).toBe(``); + }); + + it(`test durationToString`, () => { + expect(service.durationToString(83, `h`)).toBe(`1:23 h`); + expect(service.durationToString(7345, `h`)).toBe(`122:25 h`); + expect(service.durationToString(-1, `h`)).toBe(`-0:01 h`); + expect(service.durationToString(undefined, `h`)).toBe(``); + expect(service.durationToString(83, `m`)).toBe(`1:23 m`); + expect(service.durationToString(7345, `m`)).toBe(`122:25 m`); + expect(service.durationToString(-1, `m`)).toBe(`-0:01 m`); + expect(service.durationToString(undefined, `m`)).toBe(``); }); }); diff --git a/client/src/app/site/services/duration.service.ts b/client/src/app/site/services/duration.service.ts index 28a1eaacda..24498fa6bb 100644 --- a/client/src/app/site/services/duration.service.ts +++ b/client/src/app/site/services/duration.service.ts @@ -70,11 +70,16 @@ export class DurationService { * @returns A readable time-string. */ public durationToStringWithHours(duration: number): string { + let prefix = ``; + if (duration < 0) { + prefix = `-`; + duration *= -1; + } const hours = Math.floor(duration / 3600); const minutes = `0${Math.floor((duration % 3600) / 60)}`.slice(-2); const seconds = `0${Math.floor(duration % 60)}`.slice(-2); if (!isNaN(+minutes) && !isNaN(+seconds)) { - return `${hours}:${minutes}:${seconds} h`; + return `${prefix}${hours}:${minutes}:${seconds} h`; } else { return ``; } From f9e292f67ed76ec1bca9c4fbef60c53f7eb6efda Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:26:50 +0200 Subject: [PATCH 10/46] Add tests for RepositoryServiceCollectorServices (#2833) --- ...-meeting-service-collector.service.spec.ts | 104 +++++++++++++++++- ...pository-service-collector.service.spec.ts | 17 +-- 2 files changed, 101 insertions(+), 20 deletions(-) diff --git a/client/src/app/gateways/repositories/repository-meeting-service-collector.service.spec.ts b/client/src/app/gateways/repositories/repository-meeting-service-collector.service.spec.ts index 1c802b27bc..d6af6e7f95 100644 --- a/client/src/app/gateways/repositories/repository-meeting-service-collector.service.spec.ts +++ b/client/src/app/gateways/repositories/repository-meeting-service-collector.service.spec.ts @@ -1,16 +1,112 @@ import { TestBed } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { firstValueFrom, skip } from 'rxjs'; +import { ActiveMeetingService } from 'src/app/site/pages/meetings/services/active-meeting.service'; +import { ActiveMeetingIdService } from 'src/app/site/pages/meetings/services/active-meeting-id.service'; +import { MeetingSettingsService } from 'src/app/site/pages/meetings/services/meeting-settings.service'; +import { CollectionMapperService } from 'src/app/site/services/collection-mapper.service'; +import { DataStoreService } from 'src/app/site/services/data-store.service'; +import { RelationManagerService } from 'src/app/site/services/relation-manager.service'; +import { ViewModelStoreService } from 'src/app/site/services/view-model-store.service'; +import { ActionService } from '../actions'; import { RepositoryMeetingServiceCollectorService } from './repository-meeting-service-collector.service'; +import { RepositoryServiceCollectorService } from './repository-service-collector.service'; -xdescribe(`RepositoryMeetingServiceCollectorService`, () => { +abstract class MockService { + public abstract readonly name: keyof RepositoryMeetingServiceCollectorService; +} +class MockDataStore extends MockService { + public readonly name = `DS`; +} +class MockAction extends MockService { + public readonly name = `actionService`; +} +class MockCollectionMapper extends MockService { + public readonly name = `collectionMapperService`; +} +class MockViewModelStore extends MockService { + public readonly name = `viewModelStoreService`; +} +class MockTranslate extends MockService { + public readonly name = `translate`; +} +class MockRelationManager extends MockService { + public readonly name = `relationManager`; +} +class MockActiveMeetingId extends MockService { + public readonly name = `activeMeetingIdService`; +} +class MockActiveMeeting extends MockService { + public readonly name = `activeMeetingService`; +} +class MockMeetingSettings extends MockService { + public readonly name = `meetingSettingsService`; +} + +describe(`RepositoryMeetingServiceCollectorService and MeetingServiceCollectorService`, () => { let service: RepositoryMeetingServiceCollectorService; + const serviceGetterTestCases: (keyof RepositoryMeetingServiceCollectorService)[] = [ + `DS`, + `actionService`, + `collectionMapperService`, + `viewModelStoreService`, + `translate`, + `relationManager`, + `activeMeetingIdService`, + `activeMeetingService`, + `meetingSettingsService` + ]; + beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [ + RepositoryMeetingServiceCollectorService, + RepositoryServiceCollectorService, + { provide: ActiveMeetingIdService, useClass: MockActiveMeetingId }, + { provide: ActiveMeetingService, useClass: MockActiveMeeting }, + { provide: MeetingSettingsService, useClass: MockMeetingSettings }, + { provide: DataStoreService, useClass: MockDataStore }, + { provide: ActionService, useClass: MockAction }, + { provide: CollectionMapperService, useClass: MockCollectionMapper }, + { provide: ViewModelStoreService, useClass: MockViewModelStore }, + { provide: TranslateService, useClass: MockTranslate }, + { provide: RelationManagerService, useClass: MockRelationManager } + ] + }); service = TestBed.inject(RepositoryMeetingServiceCollectorService); }); - it(`should be created`, () => { - expect(service).toBeTruthy(); + for (const testCase of serviceGetterTestCases) { + it(`test service getter '${testCase}'`, () => { + expect(service[testCase][`name`]).toBe(testCase); + }); + } + + it(`test collectionToKeyUpdatesObservableMap`, async () => { + service.registerNewKeyUpdates(`A collection`, [`key1`, `key2`, `abc`]); + service.registerNewKeyUpdates(`B collection`, [`1`, `2`, `something`]); + expect( + Object.keys(service.collectionToKeyUpdatesObservableMap).flatMap(key => [ + key, + service.collectionToKeyUpdatesObservableMap[key].value + ]) + ).toEqual([`A collection`, [`key1`, `key2`, `abc`], `B collection`, [`1`, `2`, `something`]]); + const promise = firstValueFrom(service.getNewKeyUpdatesObservable(`A collection`).pipe(skip(1))); + service.registerNewKeyUpdates(`A collection`, [`a`, `b`, `c`]); + await expectAsync(promise).toBeResolvedTo([`a`, `b`, `c`]); + }); + + it(`test collectionToKeyUpdatesObservableMap for yet unknown collection`, async () => { + const promise = firstValueFrom(service.getNewKeyUpdatesObservable(`C collection`).pipe(skip(1))); + expect( + Object.keys(service.collectionToKeyUpdatesObservableMap).flatMap(key => [ + key, + service.collectionToKeyUpdatesObservableMap[key].value + ]) + ).toEqual([`C collection`, []]); + service.registerNewKeyUpdates(`C collection`, [`a`, `b`, `c`]); + await expectAsync(promise).toBeResolvedTo([`a`, `b`, `c`]); }); }); diff --git a/client/src/app/gateways/repositories/repository-service-collector.service.spec.ts b/client/src/app/gateways/repositories/repository-service-collector.service.spec.ts index 1af800921f..343101fe65 100644 --- a/client/src/app/gateways/repositories/repository-service-collector.service.spec.ts +++ b/client/src/app/gateways/repositories/repository-service-collector.service.spec.ts @@ -1,16 +1 @@ -import { TestBed } from '@angular/core/testing'; - -import { RepositoryServiceCollectorService } from './repository-service-collector.service'; - -xdescribe(`RepositoryServiceCollectorService`, () => { - let service: RepositoryServiceCollectorService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(RepositoryServiceCollectorService); - }); - - it(`should be created`, () => { - expect(service).toBeTruthy(); - }); -}); +// See repository-meeting-service-collector.service.spec.ts From b29d2b47975949afb1c9e13fbfc32e99b3bf4977 Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:27:01 +0200 Subject: [PATCH 11/46] Add tests for HttpStreamEndpointService (#2832) --- .../http-stream-endpoint.service.spec.ts | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/client/src/app/gateways/http-stream/http-stream-endpoint.service.spec.ts b/client/src/app/gateways/http-stream/http-stream-endpoint.service.spec.ts index 00421775df..bcd55fa8c2 100644 --- a/client/src/app/gateways/http-stream/http-stream-endpoint.service.spec.ts +++ b/client/src/app/gateways/http-stream/http-stream-endpoint.service.spec.ts @@ -1,16 +1,95 @@ +import { HttpHeaders } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; +import { HttpMethod, QueryParams, ResponseType } from 'src/app/infrastructure/definitions/http'; +import { HttpService } from '../http.service'; +import { EndpointConfiguration } from './endpoint-configuration'; import { HttpStreamEndpointService } from './http-stream-endpoint.service'; -xdescribe(`HttpStreamEndpointService`, () => { +class MockHttpService { + public returnValue: any = undefined; + public lastRequests: { + path: string; + data?: any; + queryParams?: QueryParams; + header?: HttpHeaders; + responseType?: ResponseType; + }[] = []; + public async get( + path: string, + data?: any, + queryParams?: QueryParams, + header?: HttpHeaders, + responseType?: ResponseType + ): Promise { + if (this.returnValue) { + this.lastRequests.push({ path, data, queryParams, header, responseType }); + return this.returnValue; + } + throw new Error(`I am an error thrown by the MockHttpService`); + } +} + +describe(`HttpStreamEndpointService`, () => { let service: HttpStreamEndpointService; + let http: MockHttpService; + + const healthEndpointConfig = new EndpointConfiguration(`url`, `healthUrl`, HttpMethod.PATCH); + const expectedHealthRequests = [ + { + path: `healthUrl`, + data: undefined, + queryParams: undefined, + header: undefined, + responseType: undefined + } + ]; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [HttpStreamEndpointService, { provide: HttpService, useClass: MockHttpService }] + }); service = TestBed.inject(HttpStreamEndpointService); + http = TestBed.inject(HttpService) as unknown as MockHttpService; }); - it(`should be created`, () => { - expect(service).toBeTruthy(); + it(`test registerEndpoint with configuration`, () => { + const configuration = new EndpointConfiguration(`url1`, `healthUrl1`, HttpMethod.GET); + service.registerEndpoint(`endpoint1`, configuration); + expect(service.getEndpoint(`endpoint1`)).toBe(configuration); }); + + it(`test registerEndpoint with urls and method`, () => { + service.registerEndpoint(`endpoint2`, `url2`, `healthUrl2`, HttpMethod.POST); + expect(service.getEndpoint(`endpoint2`)).toEqual({ + url: `url2`, + healthUrl: `healthUrl2`, + method: HttpMethod.POST + }); + }); + + it(`test registerEndpoint with healtUrl, method and configuration`, () => { + service.registerEndpoint(`endpoint4`, `url4`, `healthUrl4`, HttpMethod.DELETE); + expect(service.getEndpoint(`endpoint4`)).toEqual({ + url: `url4`, + healthUrl: `healthUrl4`, + method: HttpMethod.DELETE + }); + }); + + it(`test getEndpoint error response`, () => { + expect(() => service.getEndpoint(`endpoint`)).toThrowError(`Endpoint endpoint unknown!`); + }); + + for (const date of [ + { title: `success`, returnValue: { healthy: true }, expected: true, expectRequests: expectedHealthRequests }, + { title: `failure`, returnValue: { healthy: false }, expected: false, expectRequests: expectedHealthRequests }, + { title: `error`, returnValue: undefined, expected: false, expectRequests: [] } + ]) { + it(`test isEndpointHealthy ${date.title}`, async () => { + http.returnValue = date.returnValue; + await expectAsync(service.isEndpointHealthy(healthEndpointConfig)).toBeResolvedTo(date.expected); + expect(http.lastRequests).toEqual(date.expectRequests); + }); + } }); From e237b394761af5683f57f8f0cb44193de39f71f8 Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Tue, 26 Sep 2023 16:34:05 +0200 Subject: [PATCH 12/46] Livestream window should not overlap other elements (#2411) --- .../components/banner/banner.component.html | 36 +- .../components/banner/banner.component.ts | 18 +- .../site-wrapper/site-wrapper.module.ts | 2 + .../user-detail-view.component.html | 2 +- .../list-of-speakers-content.component.html | 2 +- .../list-of-speakers-content.component.ts | 3 + .../projectable-list.component.html | 1 + .../projectable-list.component.ts | 9 +- ...meetings-navigation-wrapper.component.html | 4 +- ...meetings-navigation-wrapper.component.scss | 2 +- .../list-of-speakers.component.html | 1 + .../topic-detail/topic-detail.component.html | 2 +- .../agenda-sort/agenda-sort.component.html | 2 +- .../mediafile-list.component.html | 1 + .../mediafile-list.component.ts | 10 +- .../mediafile-upload.component.html | 2 +- ...meeting-settings-group-list.component.html | 29 +- .../category-detail-sort.component.html | 2 +- .../category-detail.component.html | 2 +- .../category-list-sort.component.html | 2 +- .../comment-section-list.component.html | 118 ++--- .../comment-section-sort.component.html | 2 +- .../motion-call-list.component.html | 2 +- .../amendment-create-wizard.component.html | 2 +- .../amendment-create-wizard.component.scss | 10 +- .../motion-detail-view.component.html | 2 +- .../motion-list/motion-list.component.html | 56 ++- .../tag-list/tag-list.component.html | 1 + .../components/tag-list/tag-list.component.ts | 9 +- .../workflow-detail-sort.component.html | 2 +- .../workflow-detail.component.scss | 2 +- .../participant-create-wizard.component.html | 4 +- .../participant-list.component.html | 1 + .../participant-list.component.ts | 8 +- .../projector-detail.component.html | 462 +++++++++--------- .../projector-list.component.html | 3 +- .../file-list/file-list.component.html | 1 + .../file-list/file-list.component.ts | 3 + .../head-bar/head-bar.component.scss | 2 +- .../filter-menu/filter-menu.component.html | 2 +- .../filter-menu/filter-menu.component.ts | 3 + .../list/components/list/list.component.html | 1 + .../list/components/list/list.component.ts | 7 +- .../sort-filter-bar.component.html | 1 + .../sort-filter-bar.component.ts | 3 + .../view-list/view-list.component.html | 2 + .../view-list/view-list.component.ts | 3 + .../scrolling-table.component.html | 6 +- .../scrolling-table.component.ts | 18 +- client/src/styles.scss | 3 +- 50 files changed, 476 insertions(+), 395 deletions(-) diff --git a/client/src/app/site/modules/site-wrapper/components/banner/banner.component.html b/client/src/app/site/modules/site-wrapper/components/banner/banner.component.html index 0d9d282258..5ed51daca6 100644 --- a/client/src/app/site/modules/site-wrapper/components/banner/banner.component.html +++ b/client/src/app/site/modules/site-wrapper/components/banner/banner.component.html @@ -1,18 +1,20 @@ -