Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
939e0b2
Fix error when navigating away from assignment detail
bastianjoel Sep 3, 2026
173f62b
Fix error when navigating to dashboard
bastianjoel Sep 3, 2026
5abf81e
Update poll autopilot links
bastianjoel Sep 3, 2026
4977894
Prefill poll title with poll insted ballot
bastianjoel Sep 3, 2026
2bdf5d6
Respect meeting setting for disabling on max yes votes
bastianjoel Sep 3, 2026
a3c767d
Display entitled users in result
bastianjoel Sep 3, 2026
3ef6400
Fix poll update on started polls
bastianjoel Sep 3, 2026
05f9f75
Fix voting banner not hidden on poll reset
bastianjoel Sep 3, 2026
1fada0b
Improve poll form validation reactivity
bastianjoel Sep 3, 2026
0d34af8
Rename Nominate to Candidate
bastianjoel Sep 3, 2026
e5dbaca
Fix delegates not displayed
bastianjoel Sep 3, 2026
e2af267
Align remove majority selection button
bastianjoel Sep 7, 2026
c55caaa
Fix tracking expression
bastianjoel Sep 7, 2026
51009c5
Skip unnecessary rerenders in poll progress
bastianjoel Sep 7, 2026
de0c478
Increase repository throttling
bastianjoel Sep 7, 2026
650b626
Update strike out vote method naming
bastianjoel Sep 10, 2026
2a29b6a
Update analog poll keys
bastianjoel Sep 10, 2026
6e57498
Fix method config info outdated in edit result
bastianjoel Sep 10, 2026
241adae
Fix single votes and entitled users filters
bastianjoel Sep 10, 2026
3f6c022
Allow to view single votes on live vote
bastianjoel Sep 10, 2026
b9d883e
Fix topic method preselection
bastianjoel Sep 14, 2026
84606bd
Implement required majority
bastianjoel Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion client/src/app/domain/models/poll/poll-config-approval.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HasMeetingId } from '../../interfaces/has-meeting-id';
import { PollRequiredMajority } from '.';
import { BasePollConfigModel } from './base-poll-config';
import { BaseOnehundredPercentBase } from './poll-config-types';

Expand All @@ -9,6 +10,7 @@ export class PollConfigApproval extends BasePollConfigModel<PollConfigApproval>

public allow_abstain!: boolean;
public onehundred_percent_base!: ApprovalOnehundredPercentBase;
public required_majority!: PollRequiredMajority;

public constructor(input?: Partial<PollConfigApproval>) {
super(PollConfigApproval.COLLECTION, input);
Expand All @@ -18,7 +20,8 @@ export class PollConfigApproval extends BasePollConfigModel<PollConfigApproval>
`id`,
`poll_id`,
`allow_abstain`,
`onehundred_percent_base`
`onehundred_percent_base`,
`required_majority`
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HasMeetingId } from '../../interfaces/has-meeting-id';
import { PollRequiredMajority } from '.';
import { BasePollConfigModel } from './base-poll-config';
import { BaseOnehundredPercentBase } from './poll-config-types';

Expand All @@ -12,6 +13,7 @@ export class PollConfigRatingApproval extends BasePollConfigModel<PollConfigRati
public max_yes_amount!: number;
public allow_abstain!: boolean;
public onehundred_percent_base!: RatingApprovalOnehundredPercentBase;
public required_majority!: PollRequiredMajority;

public get max_vote_sum(): number {
return this.max_options_amount;
Expand All @@ -32,7 +34,8 @@ export class PollConfigRatingApproval extends BasePollConfigModel<PollConfigRati
`min_options_amount`,
`max_yes_amount`,
`allow_abstain`,
`onehundred_percent_base`
`onehundred_percent_base`,
`required_majority`
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HasMeetingId } from '../../interfaces/has-meeting-id';
import { PollRequiredMajority } from '.';
import { BasePollConfigModel } from './base-poll-config';
import { BaseOnehundredPercentBase } from './poll-config-types';

Expand All @@ -13,6 +14,7 @@ export class PollConfigRatingScore extends BasePollConfigModel<PollConfigRatingS
public max_vote_sum!: number;
public min_vote_sum!: number;
public onehundred_percent_base!: RatingScoreOnehundredPercentBase;
public required_majority!: PollRequiredMajority;

public constructor(input?: Partial<PollConfigRatingScore>) {
super(PollConfigRatingScore.COLLECTION, input);
Expand All @@ -26,7 +28,8 @@ export class PollConfigRatingScore extends BasePollConfigModel<PollConfigRatingS
`max_votes_per_option`,
`max_vote_sum`,
`min_vote_sum`,
`onehundred_percent_base`
`onehundred_percent_base`,
`required_majority`
];
}

Expand Down
3 changes: 3 additions & 0 deletions client/src/app/domain/models/poll/poll-config-selection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HasMeetingId } from '../../interfaces/has-meeting-id';
import { PollRequiredMajority } from '.';
import { BasePollConfigModel } from './base-poll-config';
import { BaseOnehundredPercentBase } from './poll-config-types';

Expand All @@ -13,6 +14,7 @@ export class PollConfigSelection extends BasePollConfigModel<PollConfigSelection
public allow_nota!: boolean;
public display_chart!: string;
public onehundred_percent_base!: SelectionOnehundredPercentBase;
public required_majority!: PollRequiredMajority;

public get max_vote_sum(): number {
return this.max_options_amount;
Expand All @@ -34,6 +36,7 @@ export class PollConfigSelection extends BasePollConfigModel<PollConfigSelection
`allow_nota`,
`strike_out`,
`onehundred_percent_base`,
`required_majority`,
`display_chart`
];
}
Expand Down
6 changes: 6 additions & 0 deletions client/src/app/domain/models/poll/poll-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export enum PollVisibility {
Secret = `secret`
}

export enum PollRequiredMajority {
NoMajority = `no_majority`,
TwoThirdMajority = `two_third_majority`,
AbsoluteMajority = `absolute_majority`
}

/**
* Colors for chart color generation
* Keys are freely invented and not in sync with html color names
Expand Down
6 changes: 3 additions & 3 deletions client/src/app/gateways/repositories/base-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Deferred } from '@app/infrastructure/utils/promises';
import { OsSortProperty } from '@app/site/base/base-sort.service';
import { SortListService } from '@app/ui/modules/list';
import { TranslateService } from '@ngx-translate/core';
import { auditTime, BehaviorSubject, filter, Observable, Subject, Subscription } from 'rxjs';
import { BehaviorSubject, filter, Observable, Subject, Subscription, throttleTime } from 'rxjs';

import { Id } from '../../domain/definitions/key-types';
import { BaseModel, ModelConstructor } from '../../domain/models/base/base-model';
Expand Down Expand Up @@ -154,10 +154,10 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
private repositoryServiceCollector = inject(RepositoryServiceCollectorService);

public constructor() {
// All data is piped through an auditTime of 1ms. This is to prevent massive
// All data is piped through an throttleTime of 20ms. This is to prevent massive
// updates, if e.g. an autoupdate with a lot motions come in. The result is just one
// update of the new list instead of many unnecessary updates.
this.unsafeViewModelListSubject.pipe(auditTime(1)).subscribe(models => {
this.unsafeViewModelListSubject.pipe(throttleTime(20)).subscribe(models => {
if (models) {
this.updateViewModelListSubject(models);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,29 @@ export class PollRepositoryService extends BaseMeetingRelatedRepository<ViewPoll
}

private async updateAnalogPoll(poll: ViewPoll, payload: PollUpdatePayload): Promise<void> {
if (poll.config.METHOD_NAME === payload.method) {
if (poll.config.method === payload.method) {
delete payload[`method`];
}

return this.voteApi.update(poll.id, payload);
}

private async updateElectronicPoll(poll: ViewPoll, payload: PollUpdatePayload): Promise<void> {
if (poll.config.METHOD_NAME === payload.method) {
if (poll.state !== PollState.Created) {
const methodConfig: {
display_chart?: string;
onehundred_percent_base?: string;
required_majority?: string;
} = payload.method_config || {};
return this.voteApi.update(poll.id, {
title: payload.title,
method_config: {
display_chart: methodConfig.display_chart,
onehundred_percent_base: methodConfig.onehundred_percent_base,
required_majority: methodConfig.required_majority
}
});
} else if (poll.config.method === payload.method) {
delete payload[`method`];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export class VotingBannerService {

private async updateVotablePollSubscription(polls: ViewPoll[]): Promise<void> {
this.pollsToVoteSubscription?.unsubscribe();
if (!polls.length) {
this.updateBanner(polls, {});
return;
}

this.pollsToVoteSubscription = this.sendVotesService.subscribeVoted(...polls).subscribe(voted => {
this.updateBanner(polls, voted);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
@if (isReadyToShow) {
<c-chart
[customTooltips]="false"
[data]="chartData"
[options]="chartOptions"
[style.height]="calcBarChartHeight()"
[type]="type"
></c-chart>
}
<div class="chart-wrapper">
@if (isReadyToShow) {
<c-chart
[customTooltips]="false"
[data]="chartData"
[options]="chartOptions"
[style.height]="calcBarChartHeight()"
[type]="type"
></c-chart>
@if (majority !== undefined) {
<div class="chart-icon" [class.color-green]="majority" [class.color-red]="!majority">
<mat-icon>{{ majority ? 'check' : 'close' }}</mat-icon>
</div>
}
}
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
.charts-wrapper {
.chart-wrapper {
position: relative;
display: block;
height: 100%;
margin: auto;
}

.chart-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
& mat-icon {
font-size: 80px;
width: 80px;
min-width: 80px;
height: 80px;
margin-top: 0 !important;
}
&.color-green {
color: var(--theme-yes);
}
&.color-red {
color: var(--theme-no);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { MatIcon } from '@angular/material/icon';
import { ChartjsComponent } from '@coreui/angular-chartjs';
import { ChartData as NgChartData, ChartOptions, ChartType } from 'chart.js';

Expand All @@ -25,7 +26,7 @@ type Label = SingleLineLabel | MultiLineLabel;
templateUrl: `./chart.component.html`,
styleUrls: [`./chart.component.scss`],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [ChartjsComponent]
imports: [ChartjsComponent, MatIcon]
})
export class ChartComponent {
/**
Expand Down Expand Up @@ -60,6 +61,9 @@ export class ChartComponent {

public colors: { backgroundColor?: string[]; hoverBackgroundColor?: string[] }[];

@Input()
public majority: boolean | undefined;

/**
* The general data for the chart.
* This is only needed for `type == 'bar' || 'line'`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,21 @@ <h3>{{ option.title }}</h3>
<input matInput type="number" [formField]="resultForm.options[i].value[key]" />
</mat-form-field>

<mat-radio-button class="option-majority" [value]="key">
{{ 'majority' | translate }}
</mat-radio-button>
<div class="majority-container">
<mat-radio-button class="option-majority" [value]="key">
{{ 'majority' | translate }}
</mat-radio-button>

@if (resultForm.options[i].majority_value().value() === key) {
<button
mat-icon-button
[attr.aria-label]="'Unselect majority' | translate"
(click)="resultForm.options[i].majority_value().value.set(null)"
>
<mat-icon>close</mat-icon>
</button>
}
@if (resultForm.options[i].majority_value().value() === key) {
<button
mat-icon-button
[attr.aria-label]="'Unselect majority' | translate"
(click)="resultForm.options[i].majority_value().value.set(null)"
>
<mat-icon>close</mat-icon>
</button>
}
</div>
</div>
</ng-template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@
.option-amount {
min-width: 50%;
}

.option {
display: flex;
}

.majority-container {
display: flex;
align-items: center;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@
}
</mat-select>
</mat-form-field>
<div></div>

<mat-form-field>
<mat-label>{{ 'Required majority' | translate }}</mat-label>
<mat-select formControlName="required_majority" panelClass="required-majority-panel">
<mat-option value="no_majority">
{{ 'no_majority' | translateKey: 'poll_required_majority' }}
</mat-option>
<mat-option value="two_third_majority">
{{ 'two_third_majority' | translateKey: 'poll_required_majority' }}
</mat-option>
<mat-option value="absolute_majority">
{{ 'absolute_majority' | translateKey: 'poll_required_majority' }}
</mat-option>
</mat-select>
@if (form.get('required_majority').getRawValue() === 'absolute_majority') {
<mat-hint
[innerText]="
'Indicates if a majority of more than 50% was reached, dependent on the 100%-base' | translate
"
></mat-hint>
} @else if (form.get('required_majority').getRawValue() === 'two_third_majority') {
<mat-hint
[innerText]="
'Indicates if a majority of two-thirds or more was reached, dependent on the 100%-base'
| translate
"
></mat-hint>
}
</mat-form-field>
</div>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { ApprovalOnehundredPercentBase } from '@app/domain/models/poll/poll-config-approval';
import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models';
import { TranslateKeyPipe } from '@app/ui/pipes/translate-key/translate-key.pipe';
import { _, TranslatePipe } from '@ngx-translate/core';

import { PollFormBaseComponent } from '../poll-config-form-base.component';
Expand All @@ -16,7 +17,14 @@ export interface PollFormApproval {

@Component({
selector: 'os-poll-form-approval',
imports: [ReactiveFormsModule, MatFormFieldModule, MatSelectModule, MatCheckboxModule, TranslatePipe],
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatSelectModule,
MatCheckboxModule,
TranslatePipe,
TranslateKeyPipe
],
templateUrl: './poll-form-approval.component.html',
styleUrl: './poll-form-approval.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
Expand All @@ -37,7 +45,8 @@ export class PollFormApprovalComponent extends PollFormBaseComponent {
protected initForm(): void {
this.form = this.fb.group({
onehundred_percent_base: [`valid`],
allow_abstain: [false]
allow_abstain: [false],
required_majority: [`no_majority`]
});
}

Expand All @@ -46,6 +55,7 @@ export class PollFormApprovalComponent extends PollFormBaseComponent {
if (data.config?.allow_abstain !== undefined) patch[`allow_abstain`] = data.config.allow_abstain;
if (data.config?.onehundred_percent_base !== undefined)
patch[`onehundred_percent_base`] = data.config.onehundred_percent_base;
if (data.config?.required_majority !== undefined) patch[`required_majority`] = data.config.required_majority;

return patch;
}
Expand Down
Loading
Loading