Add controls controls to agenda list
Adds information about duration, visibility and comment to agenda list view Allows channg these information over a dialog component on the information column
This commit is contained in:
parent
12ce434db5
commit
a338884a62
@ -61,6 +61,17 @@ export class Item extends ProjectableBaseModel {
|
||||
return this.speakers.filter(speaker => speaker.state === SpeakerState.WAITING).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type as string
|
||||
*/
|
||||
public get verboseType(): string {
|
||||
if (this.type) {
|
||||
return itemVisibilityChoices.find(visibilityType => visibilityType.key === this.type).name;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public getTitle(): string {
|
||||
return this.title;
|
||||
}
|
||||
|
@ -5,12 +5,14 @@ import { AgendaRoutingModule } from './agenda-routing.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
|
||||
import { TopicDetailComponent } from './components/topic-detail/topic-detail.component';
|
||||
import { ItemInfoDialogComponent } from './components/item-info-dialog/item-info-dialog.component';
|
||||
|
||||
/**
|
||||
* AppModule for the agenda and it's children.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [CommonModule, AgendaRoutingModule, SharedModule],
|
||||
declarations: [AgendaListComponent, TopicDetailComponent]
|
||||
entryComponents: [ ItemInfoDialogComponent ],
|
||||
declarations: [AgendaListComponent, TopicDetailComponent, ItemInfoDialogComponent]
|
||||
})
|
||||
export class AgendaModule {}
|
||||
|
@ -21,7 +21,7 @@
|
||||
<!-- selector column -->
|
||||
<ng-container matColumnDef="selector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let item" class="checkbox-cell">
|
||||
<mat-cell (click)="selectItem(item, $event)" *matCellDef="let item" class="checkbox-cell">
|
||||
<mat-icon>{{ isSelected(item) ? 'check_circle' : '' }}</mat-icon>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
@ -29,13 +29,32 @@
|
||||
<!-- title column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell>
|
||||
<mat-cell *matCellDef="let item">{{ item.getListTitle() }}</mat-cell>
|
||||
<!-- <mat-cell (click)="onTitleColumn(item)" *matCellDef="let item"> -->
|
||||
<mat-cell (click)="selectItem(item, $event)" *matCellDef="let item">
|
||||
<span *ngIf="item.closed"> <mat-icon class="done-check">check</mat-icon> </span>
|
||||
<span> {{ item.getListTitle() }} </span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Duration column -->
|
||||
<ng-container matColumnDef="duration">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Duration</mat-header-cell>
|
||||
<mat-cell *matCellDef="let item">{{ item.duration }}</mat-cell>
|
||||
<!-- Info column -->
|
||||
<ng-container matColumnDef="info">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Info</mat-header-cell>
|
||||
<mat-cell (click)="openEditInfo(item)" *matCellDef="let item">
|
||||
<div class="info-col-items">
|
||||
<div *ngIf="item.verboseType">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
{{ item.verboseType | translate }}
|
||||
</div>
|
||||
<div *ngIf="item.duration">
|
||||
<mat-icon>access_time</mat-icon>
|
||||
{{ durationService.durationToString(item.duration) }}
|
||||
</div>
|
||||
<div *ngIf="item.comment">
|
||||
<mat-icon>comment</mat-icon>
|
||||
{{ item.comment }}
|
||||
</div>
|
||||
</div>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Speakers column -->
|
||||
@ -50,10 +69,20 @@
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- menu -->
|
||||
<ng-container matColumnDef="menu">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Menu</mat-header-cell>
|
||||
<mat-cell *matCellDef="let item">
|
||||
<button mat-icon-button [matMenuTriggerFor]="singleItemMenu" [matMenuTriggerData]="{ item: item }">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
|
||||
<mat-row
|
||||
class="lg"
|
||||
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
|
||||
(click)="selectItem(row, $event)"
|
||||
*matRowDef="let row; columns: getColumnDefinition()"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
@ -61,46 +90,75 @@
|
||||
|
||||
<mat-menu #agendaMenu="matMenu">
|
||||
<div *ngIf="!isMultiSelect">
|
||||
<button mat-menu-item *osPerms="'agenda.can_manage'" (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>Multiselect</span>
|
||||
</button>
|
||||
<div *osPerms="'agenda.can_manage'">
|
||||
<!-- Enable multi select -->
|
||||
<button mat-menu-item (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>Multiselect</span>
|
||||
</button>
|
||||
|
||||
<!-- automatic numbering -->
|
||||
<button mat-menu-item *ngIf="isNumberingAllowed" (click)="onAutoNumbering()">
|
||||
<mat-icon>format_list_numbered</mat-icon>
|
||||
<span translate>Numbering</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isMultiSelect">
|
||||
<!-- Exit multi select -->
|
||||
<button mat-menu-item (click)="toggleMultiSelect()">
|
||||
<mat-icon>library_add</mat-icon>
|
||||
<span translate>Exit multiselect</span>
|
||||
</button>
|
||||
|
||||
<!-- Select all -->
|
||||
<button mat-menu-item (click)="selectAll()">
|
||||
<mat-icon>done_all</mat-icon>
|
||||
<span translate>Select all</span>
|
||||
</button>
|
||||
|
||||
<!-- Deselect all -->
|
||||
<button mat-menu-item (click)="deselectAll()">
|
||||
<mat-icon>clear</mat-icon>
|
||||
<span translate>Deselect all</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<div *osPerms="'agenda.can_manage'">
|
||||
<!-- Close selected -->
|
||||
<button mat-menu-item (click)="setClosedSelected(true)">
|
||||
<mat-icon>done</mat-icon>
|
||||
<span translate>Close</span>
|
||||
</button>
|
||||
|
||||
<!-- Open selected -->
|
||||
<button mat-menu-item (click)="setClosedSelected(false)">
|
||||
<mat-icon>redo</mat-icon>
|
||||
<span translate>Open</span>
|
||||
</button>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="setVisibilitySelected(true)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<span translate>Set visible</span>
|
||||
|
||||
<!-- Set multiple to public -->
|
||||
<button mat-menu-item (click)="setAgendaType(1)">
|
||||
<mat-icon>public</mat-icon>
|
||||
<span translate>Set public</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="setVisibilitySelected(false)">
|
||||
|
||||
<!-- Set multiple to internal -->
|
||||
<button mat-menu-item (click)="setAgendaType(2)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<span translate>Set internal</span>
|
||||
</button>
|
||||
|
||||
<!-- Set multiple to hidden -->
|
||||
<button mat-menu-item (click)="setAgendaType(3)">
|
||||
<mat-icon>visibility_off</mat-icon>
|
||||
<span translate>Set invisible</span>
|
||||
<span translate>Set hidden</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- Delete selected -->
|
||||
<button mat-menu-item class="red-warning-text" (click)="deleteSelected()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
@ -108,3 +166,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #singleItemMenu="matMenu">
|
||||
<ng-template matMenuContent let-item="item">
|
||||
<!-- Done check -->
|
||||
<button mat-menu-item (click)="onDoneSingleButton(item)">
|
||||
<mat-icon color="accent"> {{ item.closed ? 'check_box' : 'check_box_outline_blank' }} </mat-icon>
|
||||
<span translate>Done</span>
|
||||
</button>
|
||||
|
||||
<!-- List of speakers for mobile -->
|
||||
<button mat-menu-item (click)="onSpeakerIcon(item)" *ngIf="vp.isMobile">
|
||||
<mat-icon [matBadge]="item.speakerAmount > 0 ? item.speakerAmount : null" matBadgeColor="accent">
|
||||
mic
|
||||
</mat-icon>
|
||||
<span translate>List of speakers</span>
|
||||
</button>
|
||||
|
||||
<!-- Edit button -->
|
||||
<button mat-menu-item (click)="openEditInfo(item)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span translate>Edit</span>
|
||||
</button>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<button mat-menu-item class="red-warning-text" (click)="onDelete(item)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
|
@ -3,17 +3,42 @@
|
||||
/** Title */
|
||||
.mat-column-title {
|
||||
padding-left: 26px;
|
||||
flex: 1 0 200px;
|
||||
flex: 2 0 0;
|
||||
|
||||
.done-check {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/** Duration */
|
||||
.mat-column-duration {
|
||||
flex: 0 0 100px;
|
||||
.mat-column-info {
|
||||
flex: 2 0 0;
|
||||
|
||||
.info-col-items {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
|
||||
font-size: 14px;
|
||||
.mat-icon {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
$icon-size: 18px;
|
||||
font-size: $icon-size;
|
||||
height: $icon-size;
|
||||
width: $icon-size;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/** Speakers indicator */
|
||||
.mat-column-speakers {
|
||||
flex: 0 0 100px;
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
/** menu indicator */
|
||||
.mat-column-menu {
|
||||
flex: 0 0 50px;
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { MatSnackBar, MatDialog } from '@angular/material';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ViewItem } from '../../models/view-item';
|
||||
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
||||
import { AgendaRepositoryService } from '../../services/agenda-repository.service';
|
||||
import { PromptService } from '../../../../core/services/prompt.service';
|
||||
import { ItemInfoDialogComponent } from '../item-info-dialog/item-info-dialog.component';
|
||||
import { ViewportService } from 'app/core/services/viewport.service';
|
||||
import { DurationService } from 'app/site/core/services/duration.service';
|
||||
import { ConfigService } from 'app/core/services/config.service';
|
||||
|
||||
/**
|
||||
* List view for the agenda.
|
||||
@ -18,6 +22,18 @@ import { PromptService } from '../../../../core/services/prompt.service';
|
||||
styleUrls: ['./agenda-list.component.scss']
|
||||
})
|
||||
export class AgendaListComponent extends ListViewBaseComponent<ViewItem> implements OnInit {
|
||||
/**
|
||||
* Determine the display columns in desktop view
|
||||
*/
|
||||
public displayedColumnsDesktop: string[] = ['title', 'info', 'speakers', 'menu'];
|
||||
|
||||
/**
|
||||
* Determine the display columns in mobile view
|
||||
*/
|
||||
public displayedColumnsMobile: string[] = ['title', 'menu'];
|
||||
|
||||
public isNumberingAllowed: boolean;
|
||||
|
||||
/**
|
||||
* The usual constructor for components
|
||||
* @param titleService Setting the browser tab title
|
||||
@ -26,8 +42,11 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
* @param route Angulars ActivatedRoute
|
||||
* @param router Angulars router
|
||||
* @param repo the agenda repository,
|
||||
* promptService:
|
||||
*
|
||||
* @param promptService the delete prompt
|
||||
* @param dialog to change info values
|
||||
* @param config read out config values
|
||||
* @param vp determine the viewport
|
||||
* @param durationService Converts numbers to readable duration strings
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
@ -36,7 +55,11 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private repo: AgendaRepositoryService,
|
||||
private promptService: PromptService
|
||||
private promptService: PromptService,
|
||||
private dialog: MatDialog,
|
||||
private config: ConfigService,
|
||||
public vp: ViewportService,
|
||||
public durationService: DurationService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
@ -55,12 +78,17 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
this.dataSource.data = newAgendaItem;
|
||||
this.checkSelection();
|
||||
});
|
||||
|
||||
this.config
|
||||
.get('agenda_enable_numbering')
|
||||
.subscribe(autoNumbering => (this.isNumberingAllowed = autoNumbering));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for click events on an agenda item row. Links to the content object
|
||||
* Links to the content object.
|
||||
* Gets content object from the repository rather than from the model
|
||||
* to avoid race conditions
|
||||
*
|
||||
* @param item the item that was selected from the list view
|
||||
*/
|
||||
public singleSelectAction(item: ViewItem): void {
|
||||
@ -68,8 +96,48 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
this.router.navigate([contentObject.getDetailStateURL()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the item-info-dialog.
|
||||
* Enable direct changing of various information
|
||||
*
|
||||
* @param item The view item that was clicked
|
||||
*/
|
||||
public openEditInfo(item: ViewItem): void {
|
||||
const dialogRef = this.dialog.open(ItemInfoDialogComponent, {
|
||||
width: '400px',
|
||||
data: item
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
if (result.durationText) {
|
||||
result.duration = this.durationService.stringToDuration(result.durationText);
|
||||
}
|
||||
this.repo.update(result, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Click handler for the numbering button to enable auto numbering
|
||||
*/
|
||||
public async onAutoNumbering(): Promise<void> {
|
||||
const content = this.translate.instant('Are you sure you want to number all agenda items?');
|
||||
if (await this.promptService.open('', content)) {
|
||||
await this.repo.autoNumbering().then(null, this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click handler for the done button in the dot-menu
|
||||
*/
|
||||
public async onDoneSingleButton(item: ViewItem): Promise<void> {
|
||||
await this.repo.update({ closed: !item.closed }, item).then(null, this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the speakers button
|
||||
*
|
||||
* @param item indicates the row that was clicked on
|
||||
*/
|
||||
public onSpeakerIcon(item: ViewItem): void {
|
||||
@ -84,6 +152,18 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
this.router.navigate(['topics/new'], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete handler for a single item
|
||||
*
|
||||
* @param item The item to delete
|
||||
*/
|
||||
public async onDelete(item: ViewItem): Promise<void> {
|
||||
const content = this.translate.instant('Delete') + ` ${item.getTitle()}?`;
|
||||
if (await this.promptService.open('Are you sure?', content)) {
|
||||
await this.repo.delete(item).then(null, this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for deleting multiple entries. Needs items in selectedRows, which
|
||||
* is only filled with any data in multiSelect mode
|
||||
@ -110,19 +190,24 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets multiple entries' visibility. Needs items in selectedRows, which
|
||||
* Sets multiple entries' agenda type. Needs items in selectedRows, which
|
||||
* is only filled with any data in multiSelect mode.
|
||||
*
|
||||
* @param visible true if the item is to be shown
|
||||
*/
|
||||
public async setVisibilitySelected(visible: boolean): Promise<void> {
|
||||
public async setAgendaType(agendaType: number): Promise<void> {
|
||||
for (const agenda of this.selectedRows) {
|
||||
await this.repo.update({ is_hidden: visible }, agenda);
|
||||
await this.repo.update({ type: agendaType }, agenda).then(null, this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what columns to show
|
||||
*
|
||||
* @returns an array of strings with the dialogs to show
|
||||
*/
|
||||
public getColumnDefinition(): string[] {
|
||||
const list = ['title', 'duration', 'speakers'];
|
||||
const list = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
|
||||
if (this.isMultiSelect) {
|
||||
return ['selector'].concat(list);
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
<h1 mat-dialog-title>{{ 'Change values for' | translate }} {{ item.getTitle() }}</h1>
|
||||
<div mat-dialog-content>
|
||||
<form class="itemDialogForm" [formGroup]="agendaInfoForm" (keydown)="onKeyDown($event)" (ngSubmit)="saveItemInfo()">
|
||||
<!-- Visibility -->
|
||||
<mat-form-field>
|
||||
<mat-select formControlName="type" placeholder="{{ 'Agenda visibility' | translate }}">
|
||||
<mat-option *ngFor="let type of itemVisibility" [value]="type.key">
|
||||
<span>{{ type.name | translate }}</span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Duration -->
|
||||
<mat-form-field>
|
||||
<input type="string" matInput placeholder="{{ 'Duration' | translate }}" formControlName="durationText" />
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Item number (prefix) -->
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="{{ 'Item number' | translate }}" formControlName="item_number" />
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Comment -->
|
||||
<mat-form-field>
|
||||
<textarea
|
||||
matInput
|
||||
formControlName="comment"
|
||||
placeholder="{{ 'Comment' | translate }}"
|
||||
cdkTextareaAutosize
|
||||
cdkAutosizeMinRows="3"
|
||||
cdkAutosizeMaxRows="5"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="saveItemInfo()"><span translate>Save</span></button>
|
||||
<button mat-button (click)="onCancelButton()"><span translate>Cancel</span></button>
|
||||
</div>
|
@ -0,0 +1,8 @@
|
||||
.itemDialogForm {
|
||||
display: inline-block;
|
||||
::ng-deep {
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
|
||||
// import { ItemInfoDialogComponent } from './item-info-dialog.component';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('ItemInfoDialogComponent', () => {
|
||||
// let component: ItemInfoDialogComponent;
|
||||
// let fixture: ComponentFixture<ItemInfoDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
// TODO: You cannot create this component in the standard way. Needs different testing.
|
||||
beforeEach(() => {
|
||||
/*fixture = TestBed.createComponent(ItemInfoDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();*/
|
||||
});
|
||||
|
||||
/*it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});*/
|
||||
});
|
@ -0,0 +1,81 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
|
||||
import { ViewItem } from '../../models/view-item';
|
||||
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||
import { DurationService } from 'app/site/core/services/duration.service';
|
||||
|
||||
/**
|
||||
* Dialog component to change agenda item details
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-item-info-dialog',
|
||||
templateUrl: './item-info-dialog.component.html',
|
||||
styleUrls: ['./item-info-dialog.component.scss']
|
||||
})
|
||||
export class ItemInfoDialogComponent {
|
||||
/**
|
||||
* Holds the agenda item form
|
||||
*/
|
||||
public agendaInfoForm: FormGroup;
|
||||
|
||||
/**
|
||||
* Hold item visibility
|
||||
*/
|
||||
public itemVisibility = itemVisibilityChoices;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param formBuilder construct the form
|
||||
* @param durationService Converts numbers to readable duration strings
|
||||
* @param dialogRef the dialog reference
|
||||
* @param item the item that was selected
|
||||
*/
|
||||
public constructor(
|
||||
public formBuilder: FormBuilder,
|
||||
public durationService: DurationService,
|
||||
public dialogRef: MatDialogRef<ItemInfoDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
public item: ViewItem
|
||||
) {
|
||||
this.agendaInfoForm = this.formBuilder.group({
|
||||
type: [''],
|
||||
durationText: [''],
|
||||
item_number: [''],
|
||||
comment: ['']
|
||||
});
|
||||
|
||||
// load current values
|
||||
this.agendaInfoForm.get('type').setValue(item.type);
|
||||
this.agendaInfoForm.get('durationText').setValue(this.durationService.durationToString(item.duration));
|
||||
this.agendaInfoForm.get('item_number').setValue(item.itemNumber);
|
||||
this.agendaInfoForm.get('comment').setValue(item.comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to save the item
|
||||
*/
|
||||
public saveItemInfo(): void {
|
||||
this.dialogRef.close(this.agendaInfoForm.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on cancel button
|
||||
*/
|
||||
public onCancelButton(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* clicking Shift and Enter will save the form
|
||||
*
|
||||
* @param event the key that was clicked
|
||||
*/
|
||||
public onKeyDown(event: KeyboardEvent): void {
|
||||
if (event.key === 'Enter' && event.shiftKey) {
|
||||
this.saveItemInfo();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
<os-head-bar
|
||||
[mainButton]="isAllowed('edit')"
|
||||
mainButtonIcon="edit"
|
||||
[nav]="false"
|
||||
[goBack]="true"
|
||||
[editMode]="editTopic"
|
||||
@ -29,16 +31,22 @@
|
||||
<h2 *ngIf="editTopic">{{ topicForm.get('title').value }}</h2>
|
||||
</div>
|
||||
|
||||
<mat-card *ngIf="topic || editTopic" class="topic-text">
|
||||
<mat-card *ngIf="topic.text || topic.hasAttachments() || editTopic" class="topic-text">
|
||||
<div>
|
||||
<span *ngIf="!editTopic">
|
||||
<!-- Render topic text as HTML -->
|
||||
<div [innerHTML]="topic.text"></div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="topic.attachments && topic.attachments.length > 0">
|
||||
<div *ngIf="topic.hasAttachments() && !editTopic">
|
||||
<h3>
|
||||
<span translate>Attachments</span>:
|
||||
<mat-list dense>
|
||||
<mat-list-item *ngFor="let file of topic.attachments">
|
||||
<a [routerLink]="file.getDownloadUrl()" target="_blank">{{ file.title }}</a>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
<!-- TODO: Mediafiles and attachments are not fully implemented -->
|
||||
</h3>
|
||||
</div>
|
||||
@ -52,14 +60,49 @@
|
||||
osAutofocus
|
||||
required
|
||||
formControlName="title"
|
||||
placeholder="{{ 'Title' | translate}}"
|
||||
placeholder="{{ 'Title' | translate }}"
|
||||
/>
|
||||
<mat-error *ngIf="topicForm.invalid" translate>A name is required</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- The editor -->
|
||||
<div><editor formControlName="text" [init]="tinyMceSettings"></editor></div>
|
||||
<!-- TODO: Select Mediafiles as attachments here -->
|
||||
<editor formControlName="text" [init]="tinyMceSettings"></editor>
|
||||
|
||||
<!-- Attachments -->
|
||||
<os-search-value-selector
|
||||
ngDefaultControl
|
||||
[form]="topicForm"
|
||||
[formControl]="topicForm.get('attachments_id')"
|
||||
[multiple]="true"
|
||||
listname="{{ 'Attachments' | translate }}"
|
||||
[InputListValues]="mediafilesObserver"
|
||||
></os-search-value-selector>
|
||||
|
||||
<div *ngIf="newTopic">
|
||||
<!-- Visibility -->
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-select formControlName="agenda_type" placeholder="{{ 'Agenda visibility' | translate }}">
|
||||
<mat-option *ngFor="let type of itemVisibility" [value]="type.key">
|
||||
<span>{{ type.name | translate }}</span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Parent item -->
|
||||
<div>
|
||||
<os-search-value-selector
|
||||
ngDefaultControl
|
||||
[form]="topicForm"
|
||||
[formControl]="topicForm.get('agenda_parent_id')"
|
||||
[multiple]="false"
|
||||
[includeNone]="true"
|
||||
listname="{{ 'Parent Item' | translate }}"
|
||||
[InputListValues]="agendaItemObserver"
|
||||
></os-search-value-selector>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -11,6 +11,11 @@ import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { PromptService } from 'app/core/services/prompt.service';
|
||||
import { TopicRepositoryService } from '../../services/topic-repository.service';
|
||||
import { ViewTopic } from '../../models/view-topic';
|
||||
import { OperatorService } from 'app/core/services/operator.service';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { DataStoreService } from 'app/core/services/data-store.service';
|
||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||
import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item';
|
||||
|
||||
/**
|
||||
* Detail page for topics.
|
||||
@ -41,8 +46,24 @@ export class TopicDetailComponent extends BaseViewComponent {
|
||||
*/
|
||||
public topicForm: FormGroup;
|
||||
|
||||
/**
|
||||
* Subject for mediafiles
|
||||
*/
|
||||
public mediafilesObserver: BehaviorSubject<Mediafile[]>;
|
||||
|
||||
/**
|
||||
* Subject for agenda items
|
||||
*/
|
||||
public agendaItemObserver: BehaviorSubject<Item[]>;
|
||||
|
||||
/**
|
||||
* Determine visibility states for the agenda that will be created implicitly
|
||||
*/
|
||||
public itemVisibility = itemVisibilityChoices;
|
||||
|
||||
/**
|
||||
* Constructor for the topic detail page.
|
||||
*
|
||||
* @param title Setting the browsers title
|
||||
* @param matSnackBar display errors and other messages
|
||||
* @param translate Handles translations
|
||||
@ -52,6 +73,8 @@ export class TopicDetailComponent extends BaseViewComponent {
|
||||
* @param formBuilder Angulars FormBuilder
|
||||
* @param repo The topic repository
|
||||
* @param promptService Allows warning before deletion attempts
|
||||
* @param operator The current user
|
||||
* @param DS Data Store
|
||||
*/
|
||||
public constructor(
|
||||
title: Title,
|
||||
@ -62,15 +85,29 @@ export class TopicDetailComponent extends BaseViewComponent {
|
||||
private location: Location,
|
||||
private formBuilder: FormBuilder,
|
||||
private repo: TopicRepositoryService,
|
||||
private promptService: PromptService
|
||||
private promptService: PromptService,
|
||||
private operator: OperatorService,
|
||||
private DS: DataStoreService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
this.getTopicByUrl();
|
||||
this.createForm();
|
||||
|
||||
this.mediafilesObserver = new BehaviorSubject(this.DS.getAll(Mediafile));
|
||||
this.agendaItemObserver = new BehaviorSubject(this.DS.getAll(Item));
|
||||
|
||||
this.DS.changeObservable.subscribe(newModel => {
|
||||
if (newModel instanceof Item) {
|
||||
this.agendaItemObserver.next(DS.getAll(Item));
|
||||
} else if (newModel instanceof Mediafile) {
|
||||
this.mediafilesObserver.next(DS.getAll(Mediafile));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the edit mode to the given Status
|
||||
*
|
||||
* @param mode
|
||||
*/
|
||||
public setEditMode(mode: boolean): void {
|
||||
@ -88,13 +125,17 @@ export class TopicDetailComponent extends BaseViewComponent {
|
||||
*/
|
||||
public async saveTopic(): Promise<void> {
|
||||
if (this.newTopic && this.topicForm.valid) {
|
||||
if (!this.topicForm.value.agenda_parent_id) {
|
||||
delete this.topicForm.value.agenda_parent_id;
|
||||
}
|
||||
|
||||
const response = await this.repo.create(this.topicForm.value);
|
||||
this.router.navigate([`/agenda/topics/${response.id}`]);
|
||||
// after creating a new topic, go "back" to agenda list view
|
||||
this.location.replaceState('/agenda/');
|
||||
} else {
|
||||
await this.repo.update(this.topicForm.value, this.topic);
|
||||
this.setEditMode(false);
|
||||
await this.repo.update(this.topicForm.value, this.topic);
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,9 +144,14 @@ export class TopicDetailComponent extends BaseViewComponent {
|
||||
*/
|
||||
public createForm(): void {
|
||||
this.topicForm = this.formBuilder.group({
|
||||
title: ['', Validators.required],
|
||||
text: ['']
|
||||
agenda_type: [],
|
||||
agenda_parent_id: [],
|
||||
attachments_id: [[]],
|
||||
text: [''],
|
||||
title: ['', Validators.required]
|
||||
});
|
||||
|
||||
this.topicForm.get('agenda_type').setValue(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,6 +162,7 @@ export class TopicDetailComponent extends BaseViewComponent {
|
||||
Object.keys(this.topicForm.controls).forEach(ctrl => {
|
||||
topicPatch[ctrl] = this.topic[ctrl];
|
||||
});
|
||||
|
||||
this.topicForm.patchValue(topicPatch);
|
||||
}
|
||||
|
||||
@ -139,6 +186,7 @@ export class TopicDetailComponent extends BaseViewComponent {
|
||||
|
||||
/**
|
||||
* Loads a top from the repository
|
||||
*
|
||||
* @param id the id of the required topic
|
||||
*/
|
||||
public loadTopic(id: number): void {
|
||||
@ -172,14 +220,31 @@ export class TopicDetailComponent extends BaseViewComponent {
|
||||
/**
|
||||
* Handler for the delete button. Uses the PromptService
|
||||
*/
|
||||
public async onDeleteButton(): Promise<any> {
|
||||
public async onDeleteButton(): Promise<void> {
|
||||
const content = this.translate.instant('Delete') + ` ${this.topic.title}?`;
|
||||
if (await this.promptService.open('Are you sure?', content)) {
|
||||
await this.repo.delete(this.topic);
|
||||
await this.repo.delete(this.topic).then(null, this.raiseError);
|
||||
this.router.navigate(['/agenda']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the operator is allowed to perform one of the given actions
|
||||
*
|
||||
* @param action the desired action
|
||||
* @returns true if the operator has the correct permissions, false of not
|
||||
*/
|
||||
public isAllowed(action: string): boolean {
|
||||
switch (action) {
|
||||
case 'see':
|
||||
return this.operator.hasPerms('agenda.can_manage');
|
||||
case 'edit':
|
||||
return this.operator.hasPerms('agenda.can_see');
|
||||
case 'default':
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* clicking Shift and Enter will save automatically
|
||||
* Hitting escape while in topicForm should cancel editing
|
||||
|
@ -18,6 +18,10 @@ export class ViewItem extends BaseViewModel {
|
||||
return this.item ? this.item.id : null;
|
||||
}
|
||||
|
||||
public get itemNumber(): string {
|
||||
return this.item ? this.item.item_number : null;
|
||||
}
|
||||
|
||||
public get duration(): number {
|
||||
return this.item ? this.item.duration : null;
|
||||
}
|
||||
@ -26,6 +30,22 @@ export class ViewItem extends BaseViewModel {
|
||||
return this.item ? this.item.speakerAmount : null;
|
||||
}
|
||||
|
||||
public get type(): number {
|
||||
return this.item ? this.item.type : null;
|
||||
}
|
||||
|
||||
public get verboseType(): string {
|
||||
return this.item.verboseType;
|
||||
}
|
||||
|
||||
public get comment(): string {
|
||||
return this.item ? this.item.comment : null;
|
||||
}
|
||||
|
||||
public get closed(): boolean {
|
||||
return this.item ? this.item.closed : null;
|
||||
}
|
||||
|
||||
public constructor(item: Item, contentObject: AgendaBaseModel) {
|
||||
super();
|
||||
this._item = item;
|
||||
@ -40,12 +60,20 @@ export class ViewItem extends BaseViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the list view title.
|
||||
* If a number was given, 'whitespac-dot-whitespace' will be added to the prefix number
|
||||
*
|
||||
* @returns the agenda list title as string
|
||||
*/
|
||||
public getListTitle(): string {
|
||||
const contentObject: AgendaBaseModel = this.contentObject;
|
||||
const numberPrefix = this.itemNumber ? `${this.itemNumber} · ` : '';
|
||||
|
||||
if (contentObject) {
|
||||
return contentObject.getAgendaTitleWithType();
|
||||
return numberPrefix + contentObject.getAgendaTitleWithType();
|
||||
} else {
|
||||
return this.item ? this.item.title_with_type : null;
|
||||
return this.item ? numberPrefix + this.item.title_with_type : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,11 @@ import { BaseViewModel } from '../../base/base-view-model';
|
||||
import { Topic } from 'app/shared/models/topics/topic';
|
||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||
import { Item } from 'app/shared/models/agenda/item';
|
||||
import { BaseModel } from 'app/shared/models/base/base-model';
|
||||
|
||||
/**
|
||||
* Provides "safe" access to topic with all it's components
|
||||
* @ignore
|
||||
*/
|
||||
export class ViewTopic extends BaseViewModel {
|
||||
private _topic: Topic;
|
||||
@ -31,6 +33,10 @@ export class ViewTopic extends BaseViewModel {
|
||||
return this.topic ? this.topic.agenda_item_id : null;
|
||||
}
|
||||
|
||||
public get attachments_id(): number[] {
|
||||
return this.topic ? this.topic.attachments_id : null;
|
||||
}
|
||||
|
||||
public get title(): string {
|
||||
return this.topic ? this.topic.title : null;
|
||||
}
|
||||
@ -50,9 +56,16 @@ export class ViewTopic extends BaseViewModel {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
public updateValues(update: Topic): void {
|
||||
if (this.id === update.id) {
|
||||
this._topic = update;
|
||||
public hasAttachments(): boolean {
|
||||
return this.attachments && this.attachments.length > 0;
|
||||
}
|
||||
|
||||
public updateValues(update: BaseModel): void {
|
||||
if (update instanceof Mediafile) {
|
||||
if (this.topic && this.attachments_id && this.attachments_id.includes(update.id)) {
|
||||
const attachmentIndex = this.attachments.findIndex(mediafile => mediafile.id === update.id);
|
||||
this.attachments[attachmentIndex] = update as Mediafile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { Speaker } from 'app/shared/models/agenda/speaker';
|
||||
import { User } from 'app/shared/models/users/user';
|
||||
import { HttpService } from 'app/core/services/http.service';
|
||||
import { ConfigService } from 'app/core/services/config.service';
|
||||
import { DataSendService } from 'app/core/services/data-send.service';
|
||||
|
||||
/**
|
||||
* Repository service for users
|
||||
@ -27,16 +28,19 @@ import { ConfigService } from 'app/core/services/config.service';
|
||||
export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
/**
|
||||
* Contructor for agenda repository.
|
||||
*
|
||||
* @param DS The DataStore
|
||||
* @param httpService OpenSlides own HttpService
|
||||
* @param mapperService OpenSlides mapping service for collection strings
|
||||
* @param config Read config variables
|
||||
* @param dataSend send models to the server
|
||||
*/
|
||||
public constructor(
|
||||
protected DS: DataStoreService,
|
||||
private httpService: HttpService,
|
||||
mapperService: CollectionStringModelMapperService,
|
||||
private config: ConfigService
|
||||
private config: ConfigService,
|
||||
private dataSend: DataSendService
|
||||
) {
|
||||
super(DS, mapperService, Item);
|
||||
}
|
||||
@ -44,6 +48,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
/**
|
||||
* Returns the corresponding content object to a given {@link Item} as an {@link AgendaBaseModel}
|
||||
* Used dynamically because of heavy race conditions
|
||||
*
|
||||
* @param agendaItem the target agenda Item
|
||||
* @returns the content object of the given item. Might be null if it was not found.
|
||||
*/
|
||||
@ -68,6 +73,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
|
||||
/**
|
||||
* Generate viewSpeaker objects from a given agenda Item
|
||||
*
|
||||
* @param item agenda Item holding speakers
|
||||
* @returns the list of view speakers corresponding to the given item
|
||||
*/
|
||||
@ -88,8 +94,8 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
/**
|
||||
* Add a new speaker to an agenda item.
|
||||
* Sends the users ID to the server
|
||||
*
|
||||
* Might need another repo
|
||||
*
|
||||
* @param id {@link User} id of the new speaker
|
||||
* @param agenda the target agenda item
|
||||
*/
|
||||
@ -100,6 +106,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
|
||||
/**
|
||||
* Sets the given speaker ID to Speak
|
||||
*
|
||||
* @param id the speakers id
|
||||
* @param agenda the target agenda item
|
||||
*/
|
||||
@ -110,6 +117,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
|
||||
/**
|
||||
* Stops the current speaker
|
||||
*
|
||||
* @param agenda the target agenda item
|
||||
*/
|
||||
public async stopSpeaker(agenda: Item): Promise<void> {
|
||||
@ -119,6 +127,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
|
||||
/**
|
||||
* Marks the current speaker
|
||||
*
|
||||
* @param id {@link User} id of the new speaker
|
||||
* @param mark determine if the user was marked or not
|
||||
* @param agenda the target agenda item
|
||||
@ -130,6 +139,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
|
||||
/**
|
||||
* Deletes the given speaker for the agenda
|
||||
*
|
||||
* @param id the speakers id
|
||||
* @param agenda the target agenda item
|
||||
*/
|
||||
@ -140,6 +150,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
|
||||
/**
|
||||
* Posts an (manually) sorted speaker list to the server
|
||||
*
|
||||
* @param ids array of speaker id numbers
|
||||
* @param Item the target agenda item
|
||||
*/
|
||||
@ -149,34 +160,48 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* Updates an agenda item
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
* @param update contains the update data
|
||||
* @param viewItem the item to update
|
||||
*/
|
||||
public async update(item: Partial<Item>, viewUser: ViewItem): Promise<void> {
|
||||
return null;
|
||||
public async update(update: Partial<Item>, viewItem: ViewItem): Promise<void> {
|
||||
const updateItem = viewItem.item;
|
||||
updateItem.patchValues(update);
|
||||
return await this.dataSend.partialUpdateModel(updateItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the automatic numbering sequence on the server
|
||||
*/
|
||||
public async autoNumbering(): Promise<void> {
|
||||
await this.httpService.post('/rest/agenda/item/numbering/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
* TODO: Usually, agenda items are deleted with their corresponding content object
|
||||
* However, deleting an agenda item might be interpretet with "removing an item
|
||||
* from the agenda" permanently. Usually, items might juse be hidden but not
|
||||
* deleted (right now)
|
||||
*/
|
||||
public async delete(item: ViewItem): Promise<void> {
|
||||
return null;
|
||||
public delete(item: ViewItem): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
* TODO: used over not-yet-existing detail view
|
||||
* Agenda items are created implicitly and do not have on create functions
|
||||
*/
|
||||
public async create(item: Item): Promise<Identifiable> {
|
||||
return null;
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the viewItem out of a given item
|
||||
*
|
||||
* @param item the item that should be converted to view item
|
||||
* @returns a new view item
|
||||
*/
|
||||
|
@ -34,6 +34,7 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
|
||||
|
||||
/**
|
||||
* Creates a new viewModel out of the given model
|
||||
*
|
||||
* @param topic The topic that shall be converted into a view topic
|
||||
* @returns a new view topic
|
||||
*/
|
||||
@ -56,6 +57,7 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
|
||||
|
||||
/**
|
||||
* Save a new topic
|
||||
*
|
||||
* @param topicData Partial topic data to be created
|
||||
* @returns an Identifiable (usually id) as promise
|
||||
*/
|
||||
@ -81,6 +83,7 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
|
||||
|
||||
/**
|
||||
* Delete a topic
|
||||
*
|
||||
* @param viewTopic the topic that should be removed
|
||||
*/
|
||||
public async delete(viewTopic: ViewTopic): Promise<void> {
|
||||
|
@ -7,7 +7,7 @@ import { BaseViewComponent } from './base-view';
|
||||
|
||||
export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseViewComponent {
|
||||
/**
|
||||
* The data source for a table. Requires to be initialised with a BaseViewModel
|
||||
* The data source for a table. Requires to be initialized with a BaseViewModel
|
||||
*/
|
||||
public dataSource: MatTableDataSource<V>;
|
||||
|
||||
@ -17,12 +17,12 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
|
||||
protected canMultiSelect = false;
|
||||
|
||||
/**
|
||||
* Current state of the multiSelect mode. TODO Could be merged with edit mode?
|
||||
* Current state of the multi select mode. TODO Could be merged with edit mode?
|
||||
*/
|
||||
private _multiSelectModus = false;
|
||||
private _multiSelectMode = false;
|
||||
|
||||
/**
|
||||
* An array of currently selected items, upon which multiselect actions can be performed
|
||||
* An array of currently selected items, upon which multi select actions can be performed
|
||||
* see {@link selectItem}.
|
||||
*/
|
||||
public selectedRows: V[];
|
||||
@ -75,7 +75,7 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
|
||||
*/
|
||||
public selectItem(row: V, event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
if (!this._multiSelectModus) {
|
||||
if (!this._multiSelectMode) {
|
||||
this.singleSelectAction(row);
|
||||
} else {
|
||||
const idx = this.selectedRows.indexOf(row);
|
||||
@ -99,10 +99,10 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
|
||||
*/
|
||||
public toggleMultiSelect(): void {
|
||||
if (!this.canMultiSelect || this.isMultiSelect) {
|
||||
this._multiSelectModus = false;
|
||||
this._multiSelectMode = false;
|
||||
this.clearSelection();
|
||||
} else {
|
||||
this._multiSelectModus = true;
|
||||
this._multiSelectMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +121,7 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
|
||||
* Returns the current state of the multiSelect modus
|
||||
*/
|
||||
public get isMultiSelect(): boolean {
|
||||
return this._multiSelectModus;
|
||||
return this._multiSelectMode;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,7 +129,7 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
|
||||
* @param item The row's entry
|
||||
*/
|
||||
public isSelected(item: V): boolean {
|
||||
if (!this._multiSelectModus) {
|
||||
if (!this._multiSelectMode) {
|
||||
return false;
|
||||
}
|
||||
return this.selectedRows.indexOf(item) >= 0;
|
||||
|
15
client/src/app/site/core/services/duration.service.spec.ts
Normal file
15
client/src/app/site/core/services/duration.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DurationService } from './duration.service';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('DurationService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: DurationService = TestBed.get(DurationService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
72
client/src/app/site/core/services/duration.service.ts
Normal file
72
client/src/app/site/core/services/duration.service.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Helper service to convert numbers to time representation
|
||||
* and vice versa
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // will result in 70
|
||||
* const a = this.durationService.stringToDuration('01:10h');
|
||||
*
|
||||
* // will also result in 70
|
||||
* const b = this.durationService.stringToDuration('01:10');
|
||||
*
|
||||
* // will result in 0
|
||||
* const c = this.durationService.stringToDuration('01:10b');
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // will result in 01:10 h
|
||||
* const a = this.durationService.durationToString(70);
|
||||
* ```
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DurationService {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
/**
|
||||
* Transform a duration string to duration in minutes.
|
||||
*
|
||||
* @param durationText the text to be transformed into a duration
|
||||
* @returns time in minutes or 0 if values are below 0 or no parsable numbers
|
||||
*/
|
||||
public stringToDuration(durationText: string): number {
|
||||
const splitDuration = durationText.replace('h', '').split(':');
|
||||
let time: number;
|
||||
if (splitDuration.length > 1 && !isNaN(+splitDuration[0]) && !isNaN(+splitDuration[1])) {
|
||||
time = +splitDuration[0] * 60 + +splitDuration[1];
|
||||
} else if (splitDuration.length === 1 && !isNaN(+splitDuration[0])) {
|
||||
time = +splitDuration[0];
|
||||
}
|
||||
|
||||
if (!time || time < 0) {
|
||||
time = 0;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a duration number (given in minutes)
|
||||
* To a string in HH:MM format
|
||||
*
|
||||
* @param duration value in minutes
|
||||
* @returns a more human readable time representation
|
||||
*/
|
||||
public durationToString(duration: number): string {
|
||||
const hours = Math.floor(duration / 60);
|
||||
const minutes = `0${Math.floor(duration - hours * 60)}`.slice(-2);
|
||||
if (!isNaN(+hours) && !isNaN(+minutes)) {
|
||||
return `${hours}:${minutes} h`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
@ -309,7 +309,6 @@
|
||||
<form
|
||||
class="motion-content"
|
||||
[formGroup]="contentForm"
|
||||
(clickdown)="onKeyDown($event)"
|
||||
(keydown)="onKeyDown($event)"
|
||||
(ngSubmit)="saveMotion()"
|
||||
*ngIf="motion"
|
||||
@ -492,7 +491,7 @@
|
||||
<os-search-value-selector
|
||||
ngDefaultControl
|
||||
[form]="contentForm"
|
||||
[formControl]="contentForm.get('parent_id')"
|
||||
[formControl]="contentForm.get('agenda_parent_id')"
|
||||
[multiple]="false"
|
||||
[includeNone]="true"
|
||||
listname="{{ 'Parent Item' | translate }}"
|
||||
|
@ -449,7 +449,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
reason: [''],
|
||||
category_id: [''],
|
||||
attachments_id: [[]],
|
||||
parent_id: [],
|
||||
agenda_parent_id: [],
|
||||
agenda_type: [''],
|
||||
submitters_id: [],
|
||||
supporters_id: [[]],
|
||||
@ -495,6 +495,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
);
|
||||
motionValues.text = '';
|
||||
}
|
||||
|
||||
motion.deserialize(motionValues);
|
||||
return motion;
|
||||
}
|
||||
@ -504,6 +505,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
*/
|
||||
public async createMotion(): Promise<void> {
|
||||
const newMotionValues = { ...this.contentForm.value };
|
||||
|
||||
if (!newMotionValues.agenda_parent_id) {
|
||||
delete newMotionValues.agenda_parent_id;
|
||||
}
|
||||
|
||||
const motion = this.prepareMotionForSave(newMotionValues, CreateMotion);
|
||||
|
||||
try {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<os-head-bar
|
||||
[mainButton]="isAllowed('manage')"
|
||||
[mainButton]="isAllowed('changePersonal')"
|
||||
mainButtonIcon="edit"
|
||||
[nav]="false"
|
||||
[editMode]="editUser"
|
||||
|
Loading…
Reference in New Issue
Block a user