From 82ac8b16d4c44761c44cc2c97ad40afb60ea30bb Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Thu, 23 Jul 2026 18:47:00 +0200 Subject: [PATCH 1/6] WIP: Migrate poll form to signal form --- .../poll/base/base-poll-dialog.component.ts | 12 +- .../poll-form/poll-form.component.html | 8 +- .../poll-form/poll-form.component.ts | 116 ++++++++---------- .../topic-poll-dialog.component.html | 6 +- .../topic-poll-dialog.component.ts | 12 +- .../assignment-poll-dialog.component.html | 2 +- .../motion-poll-dialog.component.html | 2 +- 7 files changed, 70 insertions(+), 88 deletions(-) diff --git a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-dialog.component.ts b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-dialog.component.ts index be5ec9ecf0d..0db89116664 100644 --- a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-dialog.component.ts @@ -1,4 +1,4 @@ -import { Directive, inject, signal, viewChild } from '@angular/core'; +import { computed, Directive, inject, signal, viewChild } from '@angular/core'; import { rxResource } from '@angular/core/rxjs-interop'; import { UntypedFormBuilder } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @@ -47,16 +47,12 @@ export abstract class BasePollDialogComponent extends BaseUiComponent { return false; } - return this.pollForm().pollForm.valid; + return this.pollForm().form().valid(); } public analogPollFormOpen = signal(false); - public isAnalogPoll = rxResource({ - params: () => this.pollForm(), - defaultValue: false, - stream({ params }) { - return params.pollForm.get(`visibility`).valueChanges.pipe(map(v => v === PollVisibility.Manually)); - } + public isAnalogPoll = computed(() => { + return this.pollForm().form.visibility().value() === PollVisibility.Manually; }); protected formBuilder = inject(UntypedFormBuilder); diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html index 8e70ae53fde..a3610476179 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html @@ -1,6 +1,6 @@
-
+ {{ 'Title' | translate }}

@@ -30,7 +30,7 @@

} - @if (isEVotingSelected) { + @if (isEVotingSelected()) { {{ 'Entitled to vote' | translate }}
- @if (isLiveVotingAvailable) { + @if (isLiveVotingAvailable()) { {{ 'Live voting' | translate }} @@ -67,7 +67,7 @@

{{ 'Options' | translate }}

diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts index a25b66d345b..3758f8b2015 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts @@ -1,6 +1,7 @@ import { KeyValuePipe } from '@angular/common'; -import { Component, effect, inject, input, OnInit, ViewEncapsulation } from '@angular/core'; -import { AbstractControl, ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Component, computed, effect, inject, input, signal, ViewEncapsulation } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { form, required } from '@angular/forms/signals'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -14,7 +15,6 @@ import { EditableListComponent } from '@app/ui/modules/editable-list'; import { SearchSelectorModule } from '@app/ui/modules/search-selector'; import { PipesModule } from '@app/ui/pipes'; import { TranslatePipe } from '@ngx-translate/core'; -import { combineLatest, startWith } from 'rxjs'; import { GroupControllerService, ViewGroup } from '../../../../pages/participants'; import { ViewPoll } from '../../../../pages/polls'; @@ -39,13 +39,13 @@ import { VotingPrivacyWarningDialogComponent } from '../voting-privacy-warning/v ], encapsulation: ViewEncapsulation.None }) -export class PollFormComponent extends BaseComponent implements OnInit { - public pollForm: UntypedFormGroup; - +export class PollFormComponent extends BaseComponent { public readonly visibilityOptions = PollVisibility; public showNonNominalWarning = false; + public methods = input([`selection`, `rating_approval`, `rating_score`, `approval`, `list`]); + public optionType = input<'meeting_user' | 'text'>('text'); public optionEdit = input(false); public isEVotingEnabled = input.required(); @@ -58,57 +58,38 @@ export class PollFormComponent extends BaseComponent implements OnInit { return !this.data()?.state || this.data().isCreated; } - public get isOpenVotingSelected(): boolean { - return this.pollTypeControl?.value === PollVisibility.Open || false; - } - - public get isNamedVotingSelected(): boolean { - return this.pollTypeControl?.value === PollVisibility.Named || false; - } + public isOpenVotingSelected = computed(() => { + return this.form.visibility().value() === PollVisibility.Open || false; + }); - public get isEVotingSelected(): boolean { - return this.isEVotingEnabled() && this.pollTypeControl?.value !== PollVisibility.Manually; - } + public isNamedVotingSelected = computed(() => { + return this.form.visibility().value() === PollVisibility.Named || false; + }); - public get isLiveVotingAvailable(): boolean { - return this.isEVotingSelected && (this.isNamedVotingSelected || this.isOpenVotingSelected); - } + public isEVotingSelected = computed(() => { + return this.isEVotingEnabled() && this.form.visibility().value() !== PollVisibility.Manually; + }); - private get pollTypeControl(): AbstractControl { - return this.pollForm.get(`visibility`); - } + public isLiveVotingAvailable = computed(() => { + return this.isEVotingSelected() && (this.isNamedVotingSelected() || this.isOpenVotingSelected()); + }); - private get liveVotingControl(): AbstractControl { - return this.pollForm.get(`live_voting_enabled`); - } - - private fb = inject(UntypedFormBuilder); public groupRepo = inject(GroupControllerService); private dialog = inject(MatDialog); public constructor() { super(); - this.initContentForm(); + // this.initContentForm(); effect(() => { this.updateData(); + this.updateLiveVotingEnabled(); + this.setWarning(); }); } - public ngOnInit(): void { - this.subscriptions.push( - combineLatest([ - this.pollForm.valueChanges.pipe(startWith(``)), - this.pollTypeControl.valueChanges.pipe(startWith(``)) - ]).subscribe(() => { - this.updateLiveVotingEnabled(); - this.setWarning(); - }) - ); - } - public getValues(): Partial<{ [place in keyof ViewPoll]: any }> { - return { ...this.data, ...this.serializeForm(this.pollForm) }; + return { ...this.data, ...this.serializeForm() }; } public openVotingWarning(event: MouseEvent): void { @@ -117,46 +98,52 @@ export class PollFormComponent extends BaseComponent implements OnInit { } public onOptionsChange(items: string[]): void { - this.pollForm.get('options').setValue(items); + this.form.options().value.set(items); } private updateLiveVotingEnabled(): void { - if (!this.isLiveVotingAvailable) { - this.liveVotingControl.setValue(false, { emitEvent: false }); + if (!this.isLiveVotingAvailable()) { + this.form.live_voting_enabled().value.set(false); } } private setWarning(): void { - this.showNonNominalWarning = this.pollTypeControl.value === PollVisibility.Secret; + this.showNonNominalWarning = this.pollModel().visibility === PollVisibility.Secret; } - private serializeForm(formGroup: UntypedFormGroup): Partial { + private serializeForm(): Partial { // getRawValue() includes disabled controls - return { ...formGroup.getRawValue() }; + return { ...this.pollModel() }; } - private initContentForm(): void { - this.pollForm = this.fb.group({ - title: [``, Validators.required], - visibility: [PollVisibility.Open, Validators.required], - entitled_group_ids: [], - live_voting_enabled: [false], - option_type: ['text'], - options: [[]] - }); - } + private pollModel = signal({ + title: ``, + visibility: PollVisibility.Open, + entitled_group_ids: [], + live_voting_enabled: false, + option_type: 'text', + options: [], + method: null + }); + + public form = form(this.pollModel, schemaPath => { + required(schemaPath.title); + required(schemaPath.visibility); + }); private updateData(): void { const data = this.data(); - if (data && this.pollForm) { + if (data && this.form) { const patch: Record = {}; - if (data.entitled_group_ids !== undefined) patch['entitled_group_ids'] = data.entitled_group_ids; - if (data.live_voting_enabled !== undefined) patch['live_voting_enabled'] = !!data.live_voting_enabled; - if (data.title !== undefined) patch['title'] = data.title; - if (data.visibility !== undefined) patch['visibility'] = data.visibility; + if (data.entitled_group_ids !== undefined) + this.form['entitled_group_ids']().value.set(data.entitled_group_ids); + if (data.live_voting_enabled !== undefined) + this.form['live_voting_enabled']().value.set(!!data.live_voting_enabled); + if (data.title !== undefined) this.form['title']().value.set(data.title); + if (data.visibility !== undefined) this.form['visibility']().value.set(data.visibility); if (data.options !== undefined && !data.options.some(option => option.meeting_user_id)) - patch['options'] = data.options.map(option => option.text); + this.form['options']().value.set(data.options.map(option => option.text)); if (data.config?.allow_abstain !== undefined) patch['allow_abstain'] = data.config.allow_abstain; if (data.config?.allow_nota !== undefined) patch['allow_nota'] = data.config.allow_nota; if (data.config?.strike_out !== undefined) patch['strike_out'] = data.config.strike_out; @@ -164,7 +151,8 @@ export class PollFormComponent extends BaseComponent implements OnInit { if (data.config?.onehundred_percent_base !== undefined) patch['onehundred_percent_base'] = data.config.onehundred_percent_base; - this.pollForm.patchValue(patch); + // TODO: Patch form + // this.pollForm.patchValue(patch); } } } diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.html index 25f16b0ca55..f66bff75e11 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.html @@ -7,14 +7,14 @@ } @case ('selection') { - + } } } @else {
- +
@@ -53,7 +53,7 @@

- @if (isAnalogPoll.value()) { + @if (isAnalogPoll()) { @if (analogPollFormOpen()) {
- @if (isAnalogPoll.value()) { + @if (isAnalogPoll()) { @if (analogPollFormOpen()) {
- @if (isAnalogPoll.value()) { + @if (isAnalogPoll()) { @if (analogPollFormOpen()) { diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts index 67fc7c04545..5b0a1bb06ef 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, computed, inject, signal, viewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTabsModule } from '@angular/material/tabs'; @@ -11,8 +11,6 @@ import { } from '@app/site/pages/meetings/modules/poll/base/base-poll-dialog.component'; import { PollEditResultComponent } from '@app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component'; import { PollFormComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component'; -import { PollFormApprovalComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component'; -import { PollFormSelectionComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component'; import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service'; import { TranslatePipe } from '@ngx-translate/core'; @@ -25,8 +23,6 @@ const TAB_METHOD_MAP = [`selection`, `approval`]; imports: [ PollEditResultComponent, PollFormComponent, - PollFormApprovalComponent, - PollFormSelectionComponent, MatTabsModule, MatDialogModule, MatButtonModule, @@ -37,23 +33,10 @@ const TAB_METHOD_MAP = [`selection`, `approval`]; export class TopicPollDialogComponent extends BasePollDialogComponent { public majority: string; - private approvalForm = viewChild(PollFormApprovalComponent); - private selectionPollForm = viewChild.required(PollFormSelectionComponent); - public get isEVotingEnabled(): boolean { return this.pollService.isElectronicVotingEnabled; } - public override get formsValid(): boolean { - if (!super.formsValid) { - return false; - } - - return this.getSelectedMethod() === `approval` - ? this.approvalForm().form.valid - : this.selectionPollForm().form.valid && this.options().length > 0; - } - public selectedTab = signal(0); public options = computed(() => { @@ -75,13 +58,13 @@ export class TopicPollDialogComponent extends BasePollDialogComponent { public override methodPayload(): PollMethodPayload { return { - method: this.getSelectedMethod(), - method_config: this.getMethodConfig() + method: this.pollForm().selectedMethod(), + method_config: this.pollForm().methodConfig() }; } public override optionsPayload(): PollOptionsPayload { - if (this.getSelectedMethod() === `approval`) { + if (this.pollForm().selectedMethod() === `approval`) { return {}; } @@ -94,7 +77,7 @@ export class TopicPollDialogComponent extends BasePollDialogComponent { public analogPollOptions(): { key: string; title: string }[] { const options = []; - if (this.getSelectedMethod() === `approval`) { + if (this.pollForm().selectedMethod() === `approval`) { options.push([{ key: `approval`, title: null }]); } else { for (const option of this.options()) { @@ -104,18 +87,4 @@ export class TopicPollDialogComponent extends BasePollDialogComponent { return options; } - - public getMethodConfig(): unknown { - switch (this.getSelectedMethod()) { - case `approval`: - return { ...this.approvalForm()?.getSerialzedForm() }; - case `selection`: - return { ...this.selectionPollForm()?.getSerialzedForm() }; - } - return {}; - } - - public getSelectedMethod(): string { - return TAB_METHOD_MAP[this.selectedTab()]; - } } diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html index fa3f816895b..7c821d4f6cb 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html @@ -1,63 +1,10 @@
- - @if (pollData.isCreated === false) { - @switch (this.getSelectedMethod()) { - @case ('approval') { - - } - @case ('selection') { - - } - @case ('rating_approval') { - - } - @case ('rating_score') { - - } - } - } @else if (hasMultipleOptions) { - - -
- -
-
- -
- -
-
- @if (allowCumulative()) { - -
- -
-
- } - -
- -
-
-
- } @else if (allowCumulative()) { - - -
- -
-
- -
- -
-
-
- } @else { - - } -
+
@if (analogPollFormOpen()) { @@ -76,7 +23,7 @@
- diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts index 8b7cc2d7dfb..b76f2c93fbc 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts @@ -27,10 +27,6 @@ import { TranslatePipe } from '@ngx-translate/core'; imports: [ PollEditResultComponent, PollFormComponent, - PollFormApprovalComponent, - PollFormSelectionComponent, - PollFormRatingApprovalComponent, - PollFormRatingScoreComponent, MatDialogModule, MatButtonModule, MatTabsModule, @@ -66,25 +62,6 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent { return this.pollService.isElectronicVotingEnabled; } - public override get formsValid(): boolean { - if (!super.formsValid) { - return false; - } - - switch (this.getSelectedMethod()) { - case `approval`: - return this.approvalForm()?.form.valid; - case `selection`: - return this.selectionForm()?.form.valid; - case `rating_approval`: - return this.ratingApprovalForm()?.form.valid; - case `rating_score`: - return this.ratingScoreForm()?.form.valid; - } - - return false; - } - public get hasMultipleOptions(): boolean { const assignment = this.pollData?.content_object as ViewAssignment; return assignment.candidates.length > 1; diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html index 67b45d5b2c9..9ef0e48e7d4 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html @@ -1,6 +1,6 @@
- +
@@ -20,7 +20,7 @@
- diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.ts b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.ts index d35f3389d82..cfe35802f5c 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, viewChild } from '@angular/core'; +import { Component, computed, inject, viewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; import { PollConfigApproval } from '@app/domain/models/poll/poll-config-approval'; @@ -33,13 +33,9 @@ export class MotionPollDialogComponent extends BasePollDialogComponent { return this.pollService.isElectronicVotingEnabled; } - public override get formsValid(): boolean { - if (!super.formsValid) { - return false; - } - + public approvalFormValid = computed(() => { return this.approvalForm().form.valid; - } + }); public get approvalFormValue(): Partial { return this.approvalForm().form.value; From 0657ae567c424052aa0c3d3b1b6d05b47e61383d Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 24 Jul 2026 20:09:52 +0200 Subject: [PATCH 5/6] Fix config form validity check --- .../poll/components/poll-config-form-base.component.ts | 8 ++++++-- .../poll/components/poll-form/poll-form.component.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts index 242b2897db9..d8c6410c79a 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts @@ -1,4 +1,5 @@ -import { Component, computed, effect, inject, input } from '@angular/core'; +import { Component, computed, effect, inject, input, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { PollState } from '@app/domain/models/poll'; @@ -10,16 +11,19 @@ export abstract class PollFormBaseComponent { public data = input.required>(); + public formValid = signal(false); + public pollStarted = computed(() => { return this.data().state && this.data().state !== PollState.Created; }); - protected fb = inject(UntypedFormBuilder); + protected readonly fb = inject(UntypedFormBuilder); public constructor() { this.initForm(); effect(this.onDataUpdated.bind(this)); + this.form.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => this.formValid.set(this.form.valid)); } protected abstract initForm(): void; diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts index 037fd60f902..b527c08f952 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts @@ -198,7 +198,7 @@ export class PollFormComponent extends BaseComponent { }); public isValid = computed(() => { - return this.form().valid() && this.methodForm()?.form.valid; + return this.form().valid() && this.methodForm()?.formValid(); }); private updateData(): void { From 72e84de23943867ed734b9cc1b3d387f4aa069ef Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 24 Jul 2026 20:24:18 +0200 Subject: [PATCH 6/6] Cleanup assignment poll dialog --- .../poll-form/poll-form.component.html | 16 ++-- .../poll-form/poll-form.component.ts | 68 ++++++++++----- .../assignment-poll-dialog.component.html | 5 +- .../assignment-poll-dialog.component.ts | 83 +------------------ 4 files changed, 62 insertions(+), 110 deletions(-) diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html index 318233bd1e9..c714b6b43b4 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html @@ -81,9 +81,11 @@

{{ 'Yes/No/Abstain per option' | translate }} - - {{ 'Amount per option' | translate }} - + @if (allowCumulative()) { + + {{ 'Amount per option' | translate }} + + } } @else { {{ 'Yes per candidate' | translate }} @@ -97,9 +99,11 @@

{{ 'Yes/No/Abstain per candidate' | translate }} - - {{ 'Amount per candidate' | translate }} - + @if (allowCumulative()) { + + {{ 'Amount per candidate' | translate }} + + } } } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts index b527c08f952..7f82d65bee5 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts @@ -1,5 +1,6 @@ import { KeyValuePipe } from '@angular/common'; import { Component, computed, effect, inject, input, signal, viewChild, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ReactiveFormsModule } from '@angular/forms'; import { form, FormField, FormRoot, required } from '@angular/forms/signals'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -11,7 +12,9 @@ import { MatSelectModule } from '@angular/material/select'; import { Ids } from '@app/domain/definitions/key-types'; import { PollVisibility } from '@app/domain/models/poll'; import { infoDialogSettings } from '@app/infrastructure/utils/dialog-settings'; +import { collectionFromFqid } from '@app/infrastructure/utils/transform-functions'; import { BaseComponent } from '@app/site/base/base.component'; +import { MeetingSettingsService } from '@app/site/pages/meetings/services/meeting-settings.service'; import { DirectivesModule } from '@app/ui/directives'; import { EditableListComponent } from '@app/ui/modules/editable-list'; import { SearchSelectorModule } from '@app/ui/modules/search-selector'; @@ -83,6 +86,30 @@ export class PollFormComponent extends BaseComponent { public readonly data = input>({}); + public allowCumulative = signal(false); + private pollModel = signal({ + title: ``, + visibility: PollVisibility.Open, + entitled_group_ids: [], + live_voting_enabled: false, + option_type: 'text', + options: [], + method: null, + method_preselection: `selection.yes` + }); + + public form = form(this.pollModel, schemaPath => { + required(schemaPath.title); + required(schemaPath.visibility); + if (!this.customConfigForm()) { + required(schemaPath.method_preselection); + } + }); + + public isValid = computed(() => { + return this.form().valid() && this.methodForm()?.formValid(); + }); + public isCreated = computed(() => { return !this.data()?.state || this.data().isCreated; }); @@ -137,6 +164,7 @@ export class PollFormComponent extends BaseComponent { public groupRepo = inject(GroupControllerService); private dialog = inject(MatDialog); + private meetingSettingsService = inject(MeetingSettingsService); public constructor() { super(); @@ -148,6 +176,23 @@ export class PollFormComponent extends BaseComponent { effect(this.updateData.bind(this)); effect(this.changeMethod.bind(this)); + + this.allowCumulative.set(this.meetingSettingsService.instant(`poll_enable_max_votes_per_option`)); + let method = this.data()?.config?.method; + if (this.data()?.config_id) { + const collection = collectionFromFqid(this.data()?.config_id); + method = collection.replace(`poll_config_`, ``); + } + if (method === `rating_score`) { + this.allowCumulative.set(true); + } + + this.meetingSettingsService + .get(`poll_enable_max_votes_per_option`) + .pipe(takeUntilDestroyed()) + .subscribe(v => { + this.allowCumulative.set(v); + }); } public getValues(): Partial<{ [place in keyof ViewPoll]: any }> { @@ -178,29 +223,6 @@ export class PollFormComponent extends BaseComponent { return { ...this.pollModel() }; } - private pollModel = signal({ - title: ``, - visibility: PollVisibility.Open, - entitled_group_ids: [], - live_voting_enabled: false, - option_type: 'text', - options: [], - method: null, - method_preselection: `selection.yes` - }); - - public form = form(this.pollModel, schemaPath => { - required(schemaPath.title); - required(schemaPath.visibility); - if (!this.customConfigForm()) { - required(schemaPath.method_preselection); - } - }); - - public isValid = computed(() => { - return this.form().valid() && this.methodForm()?.formValid(); - }); - private updateData(): void { const data = this.data(); if (data && this.form) { diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html index 7c821d4f6cb..313b073aee3 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html @@ -1,6 +1,7 @@
diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts index b76f2c93fbc..b2c36292226 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts @@ -1,9 +1,7 @@ -import { ChangeDetectionStrategy, Component, computed, inject, signal, viewChild } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTabsModule } from '@angular/material/tabs'; -import { collectionFromFqid } from '@app/infrastructure/utils/transform-functions'; import { BasePollDialogComponent, PollMethodPayload, @@ -11,13 +9,8 @@ import { } from '@app/site/pages/meetings/modules/poll/base/base-poll-dialog.component'; import { PollEditResultComponent } from '@app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component'; import { PollFormComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component'; -import { PollFormApprovalComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component'; -import { PollFormRatingApprovalComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component'; -import { PollFormRatingScoreComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component'; -import { PollFormSelectionComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component'; import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service'; import { ViewAssignment } from '@app/site/pages/meetings/pages/assignments'; -import { MeetingSettingsService } from '@app/site/pages/meetings/services/meeting-settings.service'; import { TranslatePipe } from '@ngx-translate/core'; @Component({ @@ -35,29 +28,6 @@ import { TranslatePipe } from '@ngx-translate/core'; changeDetection: ChangeDetectionStrategy.Eager }) export class AssignmentPollDialogComponent extends BasePollDialogComponent { - private approvalForm = viewChild(PollFormApprovalComponent); - private selectionForm = viewChild(PollFormSelectionComponent); - private ratingApprovalForm = viewChild(PollFormRatingApprovalComponent); - private ratingScoreForm = viewChild(PollFormRatingScoreComponent); - - private tabMethodMap = computed(() => { - const methods = []; - if (!this.hasMultipleOptions) { - methods.push(`approval`); - if (this.allowCumulative()) { - methods.push(`rating_score`); - } - } else { - methods.push(`selection`, `rating_approval`); - if (this.allowCumulative()) { - methods.push(`rating_score`); - } - methods.push(`approval`); - } - - return methods; - }); - public get isEVotingEnabled(): boolean { return this.pollService.isElectronicVotingEnabled; } @@ -73,39 +43,12 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent { return assignment.candidates.length; } - public selectedTab = signal(0); - public allowCumulative = signal(false); - private pollService = inject(PollService); - private meetingSettingsService = inject(MeetingSettingsService); - - public constructor() { - super(); - - this.allowCumulative.set(this.meetingSettingsService.instant(`poll_enable_max_votes_per_option`)); - let method = this.pollData?.config?.method; - if (this.pollData?.config_id) { - const collection = collectionFromFqid(this.pollData?.config_id); - method = collection.replace(`poll_config_`, ``); - } - if (method === `rating_score`) { - this.allowCumulative.set(true); - } - - this.selectedTab.set(this.tabMethodMap().indexOf(method)); - - this.meetingSettingsService - .get(`poll_enable_max_votes_per_option`) - .pipe(takeUntilDestroyed()) - .subscribe(v => { - this.allowCumulative.set(v); - }); - } public override methodPayload(): PollMethodPayload { return { - method: this.getSelectedMethod(), - method_config: this.getMethodConfig() + method: this.pollForm().selectedMethod(), + method_config: this.pollForm().methodConfig() }; } @@ -122,7 +65,7 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent { const assignment = this.pollData?.content_object as ViewAssignment; const options = []; - if (this.getSelectedMethod() === `approval`) { + if (this.pollForm().selectedMethod() === `approval`) { options.push([{ key: `approval`, title: null }]); } else { for (const option of assignment.candidates) { @@ -132,22 +75,4 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent { return options; } - - public getMethodConfig(): unknown { - switch (this.getSelectedMethod()) { - case `approval`: - return { ...this.approvalForm()?.getSerialzedForm() }; - case `selection`: - return { ...this.selectionForm()?.getSerialzedForm() }; - case `rating_approval`: - return { ...this.ratingApprovalForm()?.getSerialzedForm() }; - case `rating_score`: - return { ...this.ratingScoreForm()?.getSerialzedForm() }; - } - return {}; - } - - public getSelectedMethod(): string { - return this.tabMethodMap()[this.selectedTab()]; - } }