diff --git a/client/src/app/gateways/repositories/base-repository.ts b/client/src/app/gateways/repositories/base-repository.ts index 818478c76d..8caaaa9bc4 100644 --- a/client/src/app/gateways/repositories/base-repository.ts +++ b/client/src/app/gateways/repositories/base-repository.ts @@ -721,6 +721,10 @@ export abstract class BaseRepository { + obj.viewModelUpdateTimestamp = Date.now(); + return Reflect.set(obj, ...args); } }); this._createViewModelPipes.forEach(fn => fn(viewModel)); diff --git a/client/src/app/site/base/base-view-model.ts b/client/src/app/site/base/base-view-model.ts index 34ec957402..dd6e2c6c12 100644 --- a/client/src/app/site/base/base-view-model.ts +++ b/client/src/app/site/base/base-view-model.ts @@ -23,6 +23,8 @@ export interface ViewModelConstructor { * Base class for view models. */ export abstract class BaseViewModel implements DetailNavigable { + public viewModelUpdateTimestamp = Date.now(); + public get fqid(): Fqid { return this.getModel().fqid; } diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/change-recommendations/services/motion-change-recommendation-controller.service/motion-change-recommendation-controller.service.ts b/client/src/app/site/pages/meetings/pages/motions/modules/change-recommendations/services/motion-change-recommendation-controller.service/motion-change-recommendation-controller.service.ts index 38ddf58d2f..ac5abdb4d7 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/change-recommendations/services/motion-change-recommendation-controller.service/motion-change-recommendation-controller.service.ts +++ b/client/src/app/site/pages/meetings/pages/motions/modules/change-recommendations/services/motion-change-recommendation-controller.service/motion-change-recommendation-controller.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { map, Observable } from 'rxjs'; +import { distinctUntilChanged, map, Observable } from 'rxjs'; import { Id } from 'src/app/domain/definitions/key-types'; import { Identifiable } from 'src/app/domain/interfaces'; import { MotionChangeRecommendation } from 'src/app/domain/models/motions/motion-change-recommendation'; @@ -47,7 +47,13 @@ export class MotionChangeRecommendationControllerService extends BaseMeetingCont */ public getChangeRecosOfMotionObservable(motionId: Id): Observable { return this.getViewModelListObservable().pipe( - map((recos: ViewMotionChangeRecommendation[]) => recos.filter(reco => reco.motion_id === motionId)) + map((recos: ViewMotionChangeRecommendation[]) => recos.filter(reco => reco.motion_id === motionId)), + distinctUntilChanged( + (prev, curr) => + prev?.length === curr?.length && + Math.max(...prev.map(e => e.viewModelUpdateTimestamp)) === + Math.max(...curr.map(e => e.viewModelUpdateTimestamp)) + ) ); } 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 3c9782285b..af1c5c76a4 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 @@ -1,6 +1,6 @@ import { ChangeDetectorRef, Directive, inject, Input } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { filter, Subscription } from 'rxjs'; +import { filter, Observable, Subscription } from 'rxjs'; import { ChangeRecoMode, LineNumberingMode } from 'src/app/domain/models/motions/motions.constants'; import { BaseMeetingComponent } from 'src/app/site/pages/meetings/base/base-meeting.component'; import { ViewMotion, ViewMotionChangeRecommendation } from 'src/app/site/pages/meetings/pages/motions'; @@ -62,10 +62,18 @@ export abstract class BaseMotionDetailChildComponent extends BaseMeetingComponen return this.viewService.currentLineNumberingMode; } + public get lineNumberingMode$(): Observable { + return this.viewService.lineNumberingModeSubject; + } + public get changeRecoMode(): ChangeRecoMode { return this.viewService.currentChangeRecommendationMode; } + public get changeRecoMode$(): Observable { + return this.viewService.changeRecommendationModeSubject; + } + public get hasChangingObjects(): boolean { if (this.sortedChangingObjects !== null) { return this.sortedChangingObjects.length > 0; @@ -88,6 +96,10 @@ export abstract class BaseMotionDetailChildComponent extends BaseMeetingComponen return this.viewService.currentShowAllAmendmentsState; } + public get showAllAmendments$(): Observable { + return this.viewService.showAllAmendmentsStateSubject; + } + /////////////////////////////////////////////// /////// Getter to repos & services /////////////////////////////////////////////// @@ -156,12 +168,19 @@ export abstract class BaseMotionDetailChildComponent extends BaseMeetingComponen * All change recommendations to this motion */ public changeRecommendations: ViewUnifiedChange[] = []; + public get changeRecommendations$(): Observable { + return this.changeRecoRepo.getChangeRecosOfMotionObservable(this.motion.id).pipe(filter(value => !!value)); + } + /** * Value for os-motion-detail-diff: when this is set, that component scrolls to the given change */ public scrollToChange: ViewUnifiedChange | null = null; protected amendments: ViewMotion[] = []; + protected get amendments$(): Observable { + return this.amendmentRepo.getViewModelListObservableFor(this.motion).pipe(filter(value => !!value)); + } private _isEditing = false; private _motion: ViewMotion | null = null; @@ -233,21 +252,17 @@ export abstract class BaseMotionDetailChildComponent extends BaseMeetingComponen private getSharedSubscriptionsToRepositories(): Subscription[] { return [ - this.changeRecoRepo - .getChangeRecosOfMotionObservable(this.motion.id) - .pipe(filter(value => !!value)) - .subscribe(changeRecos => { - this.changeRecommendations = changeRecos; - this.sortedChangingObjects = null; - }), - this.amendmentRepo - .getViewModelListObservableFor(this.motion) - .pipe(filter(value => !!value)) - .subscribe((amendments: ViewMotion[]): void => { - this.amendments = amendments; - this.motionLineNumbering.resetAmendmentChangeRecoListeners(amendments); - this.sortedChangingObjects = null; - }) + this.changeRecommendations$.subscribe(changeRecos => { + console.log(`crs updated`); + this.changeRecommendations = changeRecos; + this.sortedChangingObjects = null; + }), + this.amendments$.subscribe((amendments: ViewMotion[]): void => { + console.log(`amendments updated`); + this.amendments = amendments; + this.motionLineNumbering.resetAmendmentChangeRecoListeners(amendments); + this.sortedChangingObjects = null; + }) ]; } 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 9ba46c46fa..4075a21680 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 @@ -1,12 +1,16 @@ -@if (!motion.isParagraphBasedAmendment()) { - - @if (showPreamble && changeRecoMode !== ChangeRecoMode.Diff) { - - {{ preamble }} - - } - @if (changeRecoMode !== ChangeRecoMode.Diff && !isFinalEdit) { +@if (!isParagraphBasedAmendment) { + @let changeRecoMode = changeRecoMode$ | async; + @let lineNumberingMode = lineNumberingMode$ | async; + + @if (changeRecoMode !== ChangeRecoMode.Diff) { + + @if (showPreamble) { + + {{ preamble }} + + } + - } - @if (changeRecoMode === ChangeRecoMode.Diff) { + } @else { } -} @else if (isParagraphBasedAmendment) { +} @else { } diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-content/motion-content.component.ts b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-content/motion-content.component.ts index b67bf83013..5995c7974f 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-content/motion-content.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/components/motion-content/motion-content.component.ts @@ -1,48 +1,14 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, distinctUntilChanged, map, Subscription } from 'rxjs'; -import { Id, UnsafeHtml } from 'src/app/domain/definitions/key-types'; -import { Mediafile } from 'src/app/domain/models/mediafiles/mediafile'; -import { Settings } from 'src/app/domain/models/meetings/meeting'; -import { Motion } from 'src/app/domain/models/motions/motion'; import { ChangeRecoMode, LineNumberingMode } from 'src/app/domain/models/motions/motions.constants'; -import { RawUser } from 'src/app/gateways/repositories/users'; -import { deepCopy } from 'src/app/infrastructure/utils/transform-functions'; -import { isUniqueAmong } from 'src/app/infrastructure/utils/validators/is-unique-among'; -import { ViewMotion, ViewMotionCategory, ViewMotionWorkflow } from 'src/app/site/pages/meetings/pages/motions'; import { LineRange } from 'src/app/site/pages/meetings/pages/motions/definitions'; import { ViewUnifiedChange } from 'src/app/site/pages/meetings/pages/motions/modules/change-recommendations/view-models/view-unified-change'; -import { ParticipantListSortService } from '../../../../../participants/pages/participant-list/services/participant-list-sort/participant-list-sort.service'; -import { getParticipantMinimalSubscriptionConfig } from '../../../../../participants/participants.subscription'; -import { MotionControllerService } from '../../../../services/common/motion-controller.service'; import { MotionPermissionService } from '../../../../services/common/motion-permission.service/motion-permission.service'; import { BaseMotionDetailChildComponent } from '../../base/base-motion-detail-child.component'; import { MotionContentChangeRecommendationDialogComponentData } from '../../modules/motion-change-recommendation-dialog/components/motion-content-change-recommendation-dialog/motion-content-change-recommendation-dialog.component'; import { MotionChangeRecommendationDialogService } from '../../modules/motion-change-recommendation-dialog/services/motion-change-recommendation-dialog.service'; -/** - * fields that are required for the motion form but are not part of any motion payload - */ -interface MotionFormFields { - // from update payload - modified_final_version: string; - // apparently from no payload - parent_id: string; - - // For agenda creations - agenda_parent_id: Id; - - // Motion - workflow_id: Id; -} - -type MotionFormControlsConfig = { [key in keyof MotionFormFields]?: any } & { [key in keyof Motion]?: any } & { - supporter_ids?: any; -}; - @Component({ selector: `os-motion-content`, templateUrl: `./motion-content.component.html`, @@ -50,130 +16,36 @@ type MotionFormControlsConfig = { [key in keyof MotionFormFields]?: any } & { [k changeDetection: ChangeDetectionStrategy.OnPush }) export class MotionContentComponent extends BaseMotionDetailChildComponent { - @Output() - public save = new EventEmitter(); - - @Output() - public formChanged = new EventEmitter(); - - @Output() - public validStateChanged = new EventEmitter(); - - private finalEditMode = false; + public readonly ChangeRecoMode = ChangeRecoMode; + public readonly LineNumberingMode = LineNumberingMode; public get showPreamble(): boolean { - return this.motion?.showPreamble; + return this.motion.showPreamble; } public get canChangeMetadata(): boolean { return this.perms.isAllowed(`change_metadata`, this.motion); } - /** - * check if the 'final version edit mode' is active - * - * @returns true if active - */ - public get isFinalEdit(): boolean { - return this.finalEditMode; - } - public get isParagraphBasedAmendment(): boolean { - return this.isExisting && this.motion.isParagraphBasedAmendment(); + return this.motion.isParagraphBasedAmendment(); } public get hasAttachments(): boolean { - return this.isExisting && this.motion?.hasAttachments(); - } - - public get isExisting(): boolean { - return this.motion instanceof ViewMotion; - } - - public get motionValues(): Partial { - return this.contentForm.value; + return this.motion.hasAttachments(); } - public get hasCategories(): boolean { - return this.categoryRepo.getViewModelList().length > 0; - } - - /** - * Constant to identify the notification-message. - */ - public NOTIFICATION_EDIT_MOTION = `notifyEditMotion`; - - public readonly ChangeRecoMode = ChangeRecoMode; - - public readonly LineNumberingMode = LineNumberingMode; - - public contentForm!: UntypedFormGroup; - - public workflows: ViewMotionWorkflow[] = []; - - public categories: ViewMotionCategory[] = []; - /** * Indicates the currently highlighted line, if any. */ public highlightedLine!: number; - public set canSaveParagraphBasedAmendment(can: boolean) { - this._canSaveParagraphBasedAmendment = can; - this.propagateChanges(); - } - - public set paragraphBasedAmendmentContent(content: { - amendment_paragraphs: { [paragraph_number: number]: UnsafeHtml }; - }) { - this._paragraphBasedAmendmentContent = content; - this.propagateChanges(); - } - - public participantSubscriptionConfig = getParticipantMinimalSubscriptionConfig(this.activeMeetingId); - - private titleFieldUpdateSubscription: Subscription; - - private _canSaveParagraphBasedAmendment = true; - private _paragraphBasedAmendmentContent: any = {}; - private _motionContent: any = {}; - private _initialState: any = {}; - - private _editSubscriptions: Subscription[] = []; - - private _motionNumbersSubject = new BehaviorSubject([]); - public constructor( protected override translate: TranslateService, - private fb: UntypedFormBuilder, private dialog: MotionChangeRecommendationDialogService, - private route: ActivatedRoute, - private perms: MotionPermissionService, - private motionController: MotionControllerService, - public participantSortService: ParticipantListSortService + private perms: MotionPermissionService ) { super(); - this.motionController - .getViewModelListObservable() - .subscribe(motions => this.updateMotionNumbersSubject(motions)); - } - - /** - * Click handler for attachments - * - * @param attachment the selected file - */ - public onClickAttachment(attachment: Mediafile): void { - window.open(attachment.url); - } - - /** - * Handler for upload errors - * - * @param error the error message passed by the upload component - */ - public showUploadError(error: any): void { - this.raiseError(error); } /** @@ -250,187 +122,7 @@ export class MotionContentComponent extends BaseMotionDetailChildComponent { }); } - public async createNewSubmitter(username: string): Promise { - const newUserObj = await this.createNewUser(username); - this.addNewUserToFormCtrl(newUserObj, `submitter_ids`); - } - - public async createNewSupporter(username: string): Promise { - const newUserObj = await this.createNewUser(username); - this.addNewUserToFormCtrl(newUserObj, `supporters_id`); - } - - public getDefaultWorkflowKeyOfSettingsByParagraph(_paragraph: number): keyof Settings { - let configKey: keyof Settings = `motions_default_workflow_id`; - if (!!this.route.snapshot.queryParams[`parent`]) { - configKey = `motions_default_amendment_workflow_id`; - } - return configKey; - } - - protected override onEnterEditMode(): void { - this.patchForm(); - this.initContentFormSubscription(); - this.propagateChanges(); - } - - /** - * Async load the values of the motion in the Form. - */ - protected patchForm(): void { - if (!this.contentForm) { - this.contentForm = this.createForm(); - } - if (this.isExisting) { - const contentPatch: { [key: string]: any } = {}; - Object.keys(this.contentForm.controls).forEach(ctrl => { - contentPatch[ctrl] = this.motion[ctrl]; - }); - - if (this.isParagraphBasedAmendment) { - this.contentForm.get(`text`)?.clearValidators(); // manually adjust validators - } - - this._initialState = deepCopy(contentPatch); - this.contentForm.patchValue(contentPatch); - } else { - const parentId = Number(this.route.snapshot.queryParams[`parent`]); - if (parentId && !Number.isNaN(parentId)) { - if (!this.titleFieldUpdateSubscription) { - this.titleFieldUpdateSubscription = this.repo - .getViewModelObservable(parentId) - .pipe( - map(parent => { - return { number: parent?.number, text: parent?.text }; - }), - distinctUntilChanged() - ) - .subscribe(data => { - if (!this.contentForm.get(`title`).value) { - const title = this.translate.instant(`Amendment to`) + ` ${data.number}`; - this.contentForm.patchValue({ - title: title - }); - this._motionContent[`title`] = title; - this.propagateChanges(); - } - if ( - !this.contentForm.get(`text`).value && - this.meetingSettingsService.instant(`motions_amendments_text_mode`) === `fulltext` - ) { - this.contentForm.patchValue({ - text: data.text - }); - this._motionContent[`text`] = data.text; - this.propagateChanges(); - } - }); - } - } - } - } - - protected override onInitTextBasedAmendment(): void { - this.patchForm(); - this.propagateChanges(); - } - - protected override getSubscriptions(): Subscription[] { - // since updates are usually not coming at the same time, every change to - // any subject has to mark the view for checking - if (this.motion) { - return [this.participantRepo.getViewModelListObservable().subscribe(() => this.cd.markForCheck())]; - } - return []; - } - - protected override onAfterInit(): void { - this.updateMotionNumbersSubject(); - } - - private updateMotionNumbersSubject(motions?: ViewMotion[]): void { - this._motionNumbersSubject.next( - (motions ?? this.motionController.getViewModelList()) - .filter( - motion => motion.number !== this.motion?.number && (!motion.id || motion.id !== this.motion?.id) - ) - .map(motion => motion.number) - ); - } - - private initContentFormSubscription(): void { - for (const subscription of this._editSubscriptions) { - subscription.unsubscribe(); - } - this._editSubscriptions = []; - for (const controlName of Object.keys(this.contentForm.controls)) { - this._editSubscriptions.push( - this.contentForm.get(controlName)!.valueChanges.subscribe(value => { - if (JSON.stringify(value) !== JSON.stringify(this._initialState[controlName])) { - this._motionContent[controlName] = value; - } else { - delete this._motionContent[controlName]; - } - this.propagateChanges(); - }) - ); - } - } - - private propagateChanges(): void { - setTimeout(() => { - this.formChanged.emit({ ...this._motionContent, ...this._paragraphBasedAmendmentContent }); - this.validStateChanged.emit(this.contentForm.valid && this._canSaveParagraphBasedAmendment); - }); - } - - private addNewUserToFormCtrl(newUserObj: RawUser, controlName: string): void { - const control = this.contentForm.get(controlName)!; - let currentSubmitters: number[] = control.value; - if (currentSubmitters?.length) { - currentSubmitters.push(newUserObj.id); - } else { - currentSubmitters = [newUserObj.id]; - } - control.setValue(currentSubmitters); - } - - private createNewUser(username: string): Promise { - return this.participantRepo.createFromString(username); - } - private getAllTextChangingObjects(): ViewUnifiedChange[] { return this.getAllChangingObjectsSorted().filter((obj: ViewUnifiedChange) => !obj.isTitleChange()); } - - /** - * Creates the forms for the Motion and the MotionVersion - */ - private createForm(): UntypedFormGroup { - const motionFormControls: MotionFormControlsConfig = { - title: [``, Validators.required], - text: [``, this.isParagraphBasedAmendment ? null : Validators.required], - reason: [``, this.reasonRequired ? Validators.required : null], - category_id: [], - attachment_ids: [[]], - agenda_parent_id: [], - submitter_ids: [[]], - supporter_ids: [[]], - workflow_id: [+this.meetingSettingsService.instant(`motions_default_workflow_id`)], - tag_ids: [[]], - block_id: [], - parent_id: [], - modified_final_version: [``], - ...(this.canChangeMetadata && { - number: [ - ``, - isUniqueAmong(this._motionNumbersSubject, (a, b) => a === b, [``, null, undefined]) - ], - agenda_create: [``], - agenda_type: [``] - }) - }; - - return this.fb.group(motionFormControls); - } } diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-meta-data/motion-meta-data.component.ts b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-meta-data/motion-meta-data.component.ts index 94c4227a75..81c7b964e3 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-meta-data/motion-meta-data.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-meta-data/motion-meta-data.component.ts @@ -85,11 +85,6 @@ export class MotionMetaDataComponent extends BaseMotionDetailChildComponent impl public motionTransformFn = (value: ViewMotion): string => `[${value.fqid}]`; - /** - * All amendments to this motion - */ - public amendments$: Observable = null; - public get referencingMotions$(): Observable { return this.motion?.referenced_in_motion_recommendation_extensions$.pipe( map(motions => motions.naturalSort(this.translate.currentLang, [`number`, `title`])) @@ -184,7 +179,6 @@ export class MotionMetaDataComponent extends BaseMotionDetailChildComponent impl protected override onAfterSetMotion(previous: ViewMotion, current: ViewMotion): void { super.onAfterSetMotion(previous, current); - this.amendments$ = this.amendmentRepo.getViewModelListObservableFor(current); this.updateSupportersSubject(); } diff --git a/client/src/app/site/pages/meetings/pages/motions/services/common/amendment-controller.service/amendment-controller.service.ts b/client/src/app/site/pages/meetings/pages/motions/services/common/amendment-controller.service/amendment-controller.service.ts index af67de8d84..2491e5cf78 100644 --- a/client/src/app/site/pages/meetings/pages/motions/services/common/amendment-controller.service/amendment-controller.service.ts +++ b/client/src/app/site/pages/meetings/pages/motions/services/common/amendment-controller.service/amendment-controller.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { map, Observable } from 'rxjs'; +import { distinctUntilChanged, map, Observable } from 'rxjs'; import { Id } from 'src/app/domain/definitions/key-types'; import { Identifiable } from 'src/app/domain/interfaces'; import { Motion } from 'src/app/domain/models/motions/motion'; @@ -42,7 +42,13 @@ export class AmendmentControllerService { public getViewModelListObservableFor(motion: Identifiable): Observable { return this.getViewModelListObservable().pipe( - map(_motions => _motions.filter(_motion => _motion.lead_motion_id === motion.id)) + map(_motions => _motions.filter(_motion => _motion.lead_motion_id === motion.id)), + distinctUntilChanged( + (prev, curr) => + prev?.length === curr?.length && + Math.max(...prev.map(e => e.viewModelUpdateTimestamp)) === + Math.max(...curr.map(e => e.viewModelUpdateTimestamp)) + ) ); }