diff --git a/client/src/app/gateways/repositories/meeting-repository.service.ts b/client/src/app/gateways/repositories/meeting-repository.service.ts index 38d009bf52..34c44769d4 100644 --- a/client/src/app/gateways/repositories/meeting-repository.service.ts +++ b/client/src/app/gateways/repositories/meeting-repository.service.ts @@ -137,6 +137,13 @@ export class MeetingRepositoryService extends BaseRepository +
+ You have to be present to add yourself. +
+
diff --git a/client/src/app/site/pages/meetings/modules/list-of-speakers-content/components/list-of-speakers-content/list-of-speakers-content.component.scss b/client/src/app/site/pages/meetings/modules/list-of-speakers-content/components/list-of-speakers-content/list-of-speakers-content.component.scss index 51e68baf0f..f601040671 100644 --- a/client/src/app/site/pages/meetings/modules/list-of-speakers-content/components/list-of-speakers-content/list-of-speakers-content.component.scss +++ b/client/src/app/site/pages/meetings/modules/list-of-speakers-content/components/list-of-speakers-content/list-of-speakers-content.component.scss @@ -107,3 +107,7 @@ padding: 15px 25px 0 25px; width: auto; } + +.centered-text { + text-align: center; +} diff --git a/client/src/app/site/pages/meetings/modules/projector/modules/countdown-time/countdown-time.component.scss b/client/src/app/site/pages/meetings/modules/projector/modules/countdown-time/countdown-time.component.scss index cb8369c65a..fc86c71ec7 100644 --- a/client/src/app/site/pages/meetings/modules/projector/modules/countdown-time/countdown-time.component.scss +++ b/client/src/app/site/pages/meetings/modules/projector/modules/countdown-time/countdown-time.component.scss @@ -69,7 +69,7 @@ #countdown { white-space: nowrap; font-family: $font-monospace; - font-weight: bold; + font-weight: 800; text-align: center; } } diff --git a/client/src/app/site/pages/meetings/pages/history/components/history-list/history-list.component.ts b/client/src/app/site/pages/meetings/pages/history/components/history-list/history-list.component.ts index f4262b06ab..726e5f7e4f 100644 --- a/client/src/app/site/pages/meetings/pages/history/components/history-list/history-list.component.ts +++ b/client/src/app/site/pages/meetings/pages/history/components/history-list/history-list.component.ts @@ -205,7 +205,7 @@ export class HistoryListComponent extends BaseMeetingComponent implements OnInit private filterHistoryData(positions: HistoryPosition[], fqid: Fqid): HistoryPosition[] { return positions.filter(position => { const newInformation = []; - if (!Array.isArray(position.information)) { + if (position.information && !Array.isArray(position.information)) { position.information = position.information[fqid]; } if (!position.information) { diff --git a/client/src/app/site/pages/meetings/pages/motions/motions-routing.module.ts b/client/src/app/site/pages/meetings/pages/motions/motions-routing.module.ts index ba53978edd..640f98281e 100644 --- a/client/src/app/site/pages/meetings/pages/motions/motions-routing.module.ts +++ b/client/src/app/site/pages/meetings/pages/motions/motions-routing.module.ts @@ -48,7 +48,8 @@ const routes: Routes = [ }, { path: `tags`, - loadChildren: () => import(`./pages/tags/tags.module`).then(m => m.TagsModule) + loadChildren: () => import(`./pages/tags/tags.module`).then(m => m.TagsModule), + data: { meetingPermissions: [Permission.tagCanManage] } }, { path: `polls`, diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/amendment-create-wizard/amendment-create-wizard.component.ts b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/amendment-create-wizard/amendment-create-wizard.component.ts index f2cac30d59..30df9d84f3 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/amendment-create-wizard/amendment-create-wizard.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/amendment-create-wizard/amendment-create-wizard.component.ts @@ -181,7 +181,10 @@ export class AmendmentCreateWizardComponent extends BaseMeetingComponent impleme }; const { sequential_number } = await this.repo.createParagraphBased(motionCreate); - this.router.navigate([this.activeMeetingId, `motions`, sequential_number]); + this.router.navigate([this.activeMeetingId, `motions`, sequential_number], { + replaceUrl: true, + state: { canGoBack: true } + }); } /** diff --git a/client/src/app/site/pages/meetings/pages/motions/services/export/motion-pdf.service/motion-pdf.service.ts b/client/src/app/site/pages/meetings/pages/motions/services/export/motion-pdf.service/motion-pdf.service.ts index 12ab6fa0d5..2421cbe928 100644 --- a/client/src/app/site/pages/meetings/pages/motions/services/export/motion-pdf.service/motion-pdf.service.ts +++ b/client/src/app/site/pages/meetings/pages/motions/services/export/motion-pdf.service/motion-pdf.service.ts @@ -324,7 +324,10 @@ export class MotionPdfService { } // referring motions - if (!infoToExport || infoToExport.includes(`referring_motions`)) { + if ( + infoToExport?.includes(`referring_motions`) || + (!infoToExport && this.meetingSettingsService.instant(`motions_show_referring_motions`)) + ) { if (motion.referenced_in_motion_recommendation_extensions.length) { const referringMotions = motion.referenced_in_motion_recommendation_extensions .naturalSort(this.translate.currentLang, [`number`, `title`]) 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 4429bb9f76..478368eab4 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,12 @@ export class MeetingEditComponent extends BaseComponent implements OnInit { public committee!: ViewCommittee; public get meetingUsers(): ViewUser[] { - return this.editMeeting?.calculated_users || []; + const users = this.editMeeting?.calculated_users; + if (users && !users.some(u => u.id === this.operator.operatorId)) { + users.push(this.operator.user); + } + + return users || []; } private meetingId: Id | null = null; diff --git a/client/src/app/site/services/autoupdate/autoupdate.service.ts b/client/src/app/site/services/autoupdate/autoupdate.service.ts index ff86be5a95..7a69fee45c 100644 --- a/client/src/app/site/services/autoupdate/autoupdate.service.ts +++ b/client/src/app/site/services/autoupdate/autoupdate.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { marker as _ } from '@colsen1991/ngx-translate-extract-marker'; +import { firstValueFrom } from 'rxjs'; import { ModelRequest } from 'src/app/domain/interfaces/model-request'; import { Collection, Id, Ids } from '../../../domain/definitions/key-types'; @@ -8,6 +9,7 @@ import { EndpointConfiguration } from '../../../gateways/http-stream/endpoint-co import { HttpMethod, QueryParams } from '../../../infrastructure/definitions/http'; import { Mutex } from '../../../infrastructure/utils/promises'; import { BannerDefinition, BannerService } from '../../modules/site-wrapper/services/banner.service'; +import { LifecycleService } from '../lifecycle.service'; import { ModelRequestObject } from '../model-request-builder'; import { ViewModelStoreUpdateService } from '../view-model-store-update.service'; import { WindowVisibilityService } from '../window-visibility.service'; @@ -82,7 +84,8 @@ export class AutoupdateService { private viewmodelStoreUpdate: ViewModelStoreUpdateService, private communication: AutoupdateCommunicationService, private bannerService: BannerService, - private visibilityService: WindowVisibilityService + private visibilityService: WindowVisibilityService, + private lifecycle: LifecycleService ) { this.setAutoupdateConfig(null); this.httpEndpointService.registerEndpoint( @@ -97,9 +100,12 @@ export class AutoupdateService { this.communication.listenShouldReconnect().subscribe(() => { this.pauseUntilVisible(); }); - this.visibilityService.hiddenFor(PAUSE_ON_INACTIVITY_TIMEOUT).subscribe(() => { - this.pauseUntilVisible(); - }); + + firstValueFrom(this.lifecycle.appLoaded).then(() => + this.visibilityService.hiddenFor(PAUSE_ON_INACTIVITY_TIMEOUT).subscribe(() => { + this.pauseUntilVisible(); + }) + ); window.addEventListener(`beforeunload`, () => { for (const id of Object.keys(this._activeRequestObjects)) { diff --git a/client/src/app/ui/modules/head-bar/services/routing-state.service.ts b/client/src/app/ui/modules/head-bar/services/routing-state.service.ts index 0717d34d78..81f1345b5b 100644 --- a/client/src/app/ui/modules/head-bar/services/routing-state.service.ts +++ b/client/src/app/ui/modules/head-bar/services/routing-state.service.ts @@ -11,6 +11,7 @@ import { filter, pairwise, startWith } from 'rxjs'; providedIn: `root` }) export class RoutingStateService { + private skipUnsafeRouteCheck = false; /** * Hold the previous URL */ @@ -32,7 +33,7 @@ export class RoutingStateService { * If this fails, the open nav button should be shown */ public get isSafePrevUrl(): boolean { - if (this._previousUrl) { + if (this._previousUrl && !this.skipUnsafeRouteCheck) { return !this.unsafeUrls.some(unsafeUrl => this._previousUrl?.includes(unsafeUrl)); } else { return true; @@ -62,6 +63,9 @@ export class RoutingStateService { pairwise() ) .subscribe((event: any[]) => { + this.skipUnsafeRouteCheck = + router.getCurrentNavigation()?.extras?.state && + router.getCurrentNavigation()?.extras?.state[`canGoBack`]; this._previousUrl = event[0]?.urlAfterRedirects ?? this._currentUrl; const currentNavigationExtras = router.getCurrentNavigation()?.extras; if (currentNavigationExtras && currentNavigationExtras.state && currentNavigationExtras.state[`back`]) { diff --git a/client/src/app/ui/modules/list/services/list-search.service.spec.ts b/client/src/app/ui/modules/list/services/list-search.service.spec.ts index f6d483c890..feb935e6e0 100644 --- a/client/src/app/ui/modules/list/services/list-search.service.spec.ts +++ b/client/src/app/ui/modules/list/services/list-search.service.spec.ts @@ -1,16 +1,195 @@ -import { TestBed } from '@angular/core/testing'; +import { BehaviorSubject, filter, firstValueFrom, map, skip } from 'rxjs'; import { ListSearchService } from './list-search.service'; -xdescribe(`ListSearchService`, () => { - let service: ListSearchService; +class MockIdentifiable { + public constructor( + public id: number, + public en: string, + public ti: boolean, + private _fi: (originItem: MockIdentifiable) => string, + public ab: { data: string }, + public le?: number[] + ) {} - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(ListSearchService); + public fi(): string { + return this._fi(this); + } +} + +describe(`ListSearchService`, () => { + let service: ListSearchService; + + const data = { + strings: [`an`, `apple`, `a`, `day`, `keeps`, `the`, `doctor`, `away`], + numbers: [1, 2, 3, 5, 8, 13, 21, 34, 55, 89], + booleans: [true, false, false, true, true, true, false, true, false], + functions: [ + (originItem: MockIdentifiable) => originItem.en, + () => `banana`, + (originItem: MockIdentifiable) => originItem.ab.data + ], + generate: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 92] + }; + + function getSourceData(length = 25): MockIdentifiable[] { + return Array.from({ length }, (value, index) => index).map( + id => + new MockIdentifiable( + id, + data.strings[id % data.strings.length], + data.booleans[id % data.booleans.length], + data.functions[id % data.functions.length], + { + data: data.strings[(id + 3) % data.strings.length].split(``).reverse().join(``) + }, + data.generate.slice( + data.generate[id % data.generate.length] % data.generate.length, + Math.max( + id % data.generate.length, + data.generate[id % data.generate.length] % data.generate.length + ) + 1 + ) + ) + ); + } + + function generateService(filterProps: string[], alsoFilterByProperties: string[] = []): void { + service = new ListSearchService(filterProps, alsoFilterByProperties); + } + + function getWaitUntilPromiseForService( + condition: (data: MockIdentifiable[]) => boolean, + skipOver = 0 + ): Promise { + return firstValueFrom( + service.outputObservable.pipe( + filter(condition), + skip(skipOver), + map(arr => arr.map(item => item.id)) + ) + ); + } + + afterEach(() => { + service.exitSearchService(); + }); + + it(`test search without properties`, async () => { + generateService([]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`apple`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25, 1)).toBePending(); + }); + + it(`test search without data`, async () => { + generateService([`en`]); + service.initSearchService(new BehaviorSubject([])); + expect(() => service.search(`apple`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 0, 1)).toBePending(); + }); + + it(`test search with data and properites`, async () => { + generateService([`ti`]); + service.initSearchService(new BehaviorSubject(getSourceData(10))); + expect(() => service.search(`true`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([0, 3, 4, 5, 7, 9]); + }); + + it(`test search with alsoFilterByProperties`, async () => { + generateService([`ti`], [`en`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`doctor`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([6, 14, 22]); + }); + + it(`test search trim`, async () => { + generateService([`ti`], [`en`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`doctor `)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([6, 14, 22]); + }); + + it(`test search function`, async () => { + generateService([`fi`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`doctor`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([6]); + service.exitSearchService(); + generateService([`fi`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`banana`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([ + 1, 4, 7, 10, 13, 16, 19, 22 + ]); + }); + + it(`test search number`, async () => { + generateService([`id`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`5`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([5, 15]); + }); + + it(`test search array`, async () => { + generateService([`le`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`5`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([ + 4, 5, 6, 8, 9, 10, 11, 12, 17, 18, 19, 21, 22, 23, 24 + ]); + }); + + it(`test if search doesn't include other property values`, async () => { + generateService([`ti`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`doctor`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([]); + }); + + it(`test search with multiple properties`, async () => { + generateService([`le`, `id`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`5`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([ + 4, 5, 6, 8, 9, 10, 11, 12, 15, 17, 18, 19, 21, 22, 23, 24 + ]); + }); + + it(`test search with multiple properties of different types`, async () => { + generateService([`en`, `id`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`5`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([5, 15]); + }); + + it(`test if really only the first additional property is searched`, async () => { + generateService([`ti`], [`en`, `id`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`5`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([]); + }); + + it(`test if the second additional property is used, if the first isn't filled`, async () => { + generateService([`ti`], [`le`, `en`]); + service.initSearchService( + new BehaviorSubject( + getSourceData(25).map((val, index) => + index % 2 ? val : new MockIdentifiable(val.id, val.en, val.ti, val.fi, val.ab) + ) + ) + ); + expect(() => service.search(`a`)).not.toThrow(); + // does not contain uneven ids, even though `en` for 1 should be `apple` + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([ + 0, 2, 8, 10, 16, 18, 24 + ]); }); - it(`should be created`, () => { - expect(service).toBeTruthy(); + it(`test search with nested properties`, async () => { + generateService([`ab.data`]); + service.initSearchService(new BehaviorSubject(getSourceData(25))); + expect(() => service.search(`elppa`)).not.toThrow(); + await expectAsync(getWaitUntilPromiseForService(data => data.length !== 25)).toBeResolvedTo([6, 14, 22]); }); }); diff --git a/client/src/app/ui/modules/search-selector/components/base-search-selector/base-search-selector.component.ts b/client/src/app/ui/modules/search-selector/components/base-search-selector/base-search-selector.component.ts index a1a4ba5d63..967ce8e1f4 100644 --- a/client/src/app/ui/modules/search-selector/components/base-search-selector/base-search-selector.component.ts +++ b/client/src/app/ui/modules/search-selector/components/base-search-selector/base-search-selector.component.ts @@ -219,7 +219,6 @@ export abstract class BaseSearchSelectorComponent extends BaseFormFieldControlCo } this._selectableItemsList = this.sortFn ? allItems.sort(this.sortFn) : allItems; this.filteredItemsSubject.next(this.getFilteredItemsBySearchValue()); - this.updateOptionFocus(); } protected get selectableItems(): Selectable[] { @@ -261,15 +260,6 @@ export abstract class BaseSearchSelectorComponent extends BaseFormFieldControlCo document.body.appendChild(sheet); } - private updateOptionFocus(): void { - setTimeout(() => { - if (!this.matSelect?.options.some(o => o.active)) { - this.matSelect.options.first.focus(); - this.matSelect.options.first.setActiveStyles(); - } - }, 200); - } - public onChipRemove(itemId: Id): void { this.addOrRemoveId(itemId); @@ -351,7 +341,6 @@ export abstract class BaseSearchSelectorComponent extends BaseFormFieldControlCo protected onSearchValueUpdated(nextValue: string): void { this.filteredItemsSubject.next(this.getFilteredItemsBySearchValue(nextValue.toLowerCase())); - this.updateOptionFocus(); } protected initializeForm(): void { 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 4daa33bbd9..c7b084156b 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 @@ -177,6 +177,7 @@ export class TreeService { /** * Removes the `item`-property from any node in the given tree. + * Deletes empty children-arrays. * * @param tree The tree with items * @returns The tree without items @@ -188,7 +189,7 @@ export class TreeService { id: node.id }; if (node.children) { - nodeWithoutItem.children = this.stripTree(node.children); + nodeWithoutItem.children = node.children.length ? this.stripTree(node.children) : undefined; } return nodeWithoutItem; }); diff --git a/client/src/assets/styles/font-variables.scss b/client/src/assets/styles/font-variables.scss index 6ade6ecea4..5f832655e2 100644 --- a/client/src/assets/styles/font-variables.scss +++ b/client/src/assets/styles/font-variables.scss @@ -24,7 +24,7 @@ $font-weight-condensed-regular: 400; /** Monospace Font */ $font-monospace: 'OSFont Monospace'; $font-monospace-src: url('../fonts/roboto-condensed-bold.woff') format('woff'); -$font-weight-monospace: 400; +$font-weight-monospace: 800; /** Special Chyron Name Font */ $font-chyronname: 'OSFont ChyronName';