Merge pull request #4059 from tsiegleauq/agenda-list-controls

Add controls to agenda list
This commit is contained in:
Sean 2018-12-16 20:55:28 +01:00 committed by GitHub
commit 3a2df3b731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 704 additions and 70 deletions

View File

@ -61,6 +61,17 @@ export class Item extends ProjectableBaseModel {
return this.speakers.filter(speaker => speaker.state === SpeakerState.WAITING).length; 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 { public getTitle(): string {
return this.title; return this.title;
} }

View File

@ -5,12 +5,14 @@ import { AgendaRoutingModule } from './agenda-routing.module';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { AgendaListComponent } from './components/agenda-list/agenda-list.component'; import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
import { TopicDetailComponent } from './components/topic-detail/topic-detail.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. * AppModule for the agenda and it's children.
*/ */
@NgModule({ @NgModule({
imports: [CommonModule, AgendaRoutingModule, SharedModule], imports: [CommonModule, AgendaRoutingModule, SharedModule],
declarations: [AgendaListComponent, TopicDetailComponent] entryComponents: [ ItemInfoDialogComponent ],
declarations: [AgendaListComponent, TopicDetailComponent, ItemInfoDialogComponent]
}) })
export class AgendaModule {} export class AgendaModule {}

View File

@ -21,7 +21,7 @@
<!-- selector column --> <!-- selector column -->
<ng-container matColumnDef="selector"> <ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell> <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-icon>{{ isSelected(item) ? 'check_circle' : '' }}</mat-icon>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
@ -29,13 +29,32 @@
<!-- title column --> <!-- title column -->
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Topic</mat-header-cell> <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> </ng-container>
<!-- Duration column --> <!-- Info column -->
<ng-container matColumnDef="duration"> <ng-container matColumnDef="info">
<mat-header-cell *matHeaderCellDef mat-sort-header>Duration</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Info</mat-header-cell>
<mat-cell *matCellDef="let item">{{ item.duration }}</mat-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> </ng-container>
<!-- Speakers column --> <!-- Speakers column -->
@ -50,10 +69,20 @@
</mat-cell> </mat-cell>
</ng-container> </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-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row <mat-row
class="lg"
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''" [ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
(click)="selectItem(row, $event)"
*matRowDef="let row; columns: getColumnDefinition()" *matRowDef="let row; columns: getColumnDefinition()"
></mat-row> ></mat-row>
</mat-table> </mat-table>
@ -61,46 +90,75 @@
<mat-menu #agendaMenu="matMenu"> <mat-menu #agendaMenu="matMenu">
<div *ngIf="!isMultiSelect"> <div *ngIf="!isMultiSelect">
<button mat-menu-item *osPerms="'agenda.can_manage'" (click)="toggleMultiSelect()"> <div *osPerms="'agenda.can_manage'">
<!-- Enable multi select -->
<button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
<span translate>Multiselect</span> <span translate>Multiselect</span>
</button> </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>
<div *ngIf="isMultiSelect"> <div *ngIf="isMultiSelect">
<!-- Exit multi select -->
<button mat-menu-item (click)="toggleMultiSelect()"> <button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon> <mat-icon>library_add</mat-icon>
<span translate>Exit multiselect</span> <span translate>Exit multiselect</span>
</button> </button>
<!-- Select all -->
<button mat-menu-item (click)="selectAll()"> <button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>
<span translate>Select all</span> <span translate>Select all</span>
</button> </button>
<!-- Deselect all -->
<button mat-menu-item (click)="deselectAll()"> <button mat-menu-item (click)="deselectAll()">
<mat-icon>clear</mat-icon> <mat-icon>clear</mat-icon>
<span translate>Deselect all</span> <span translate>Deselect all</span>
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div *osPerms="'agenda.can_manage'"> <div *osPerms="'agenda.can_manage'">
<!-- Close selected -->
<button mat-menu-item (click)="setClosedSelected(true)"> <button mat-menu-item (click)="setClosedSelected(true)">
<mat-icon>done</mat-icon> <mat-icon>done</mat-icon>
<span translate>Close</span> <span translate>Close</span>
</button> </button>
<!-- Open selected -->
<button mat-menu-item (click)="setClosedSelected(false)"> <button mat-menu-item (click)="setClosedSelected(false)">
<mat-icon>redo</mat-icon> <mat-icon>redo</mat-icon>
<span translate>Open</span> <span translate>Open</span>
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)="setVisibilitySelected(true)">
<mat-icon>visibility</mat-icon> <!-- Set multiple to public -->
<span translate>Set visible</span> <button mat-menu-item (click)="setAgendaType(1)">
<mat-icon>public</mat-icon>
<span translate>Set public</span>
</button> </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> <mat-icon>visibility_off</mat-icon>
<span translate>Set invisible</span> <span translate>Set hidden</span>
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<!-- Delete selected -->
<button mat-menu-item class="red-warning-text" (click)="deleteSelected()"> <button mat-menu-item class="red-warning-text" (click)="deleteSelected()">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span translate>Delete</span> <span translate>Delete</span>
@ -108,3 +166,33 @@
</div> </div>
</div> </div>
</mat-menu> </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>

View File

@ -3,17 +3,42 @@
/** Title */ /** Title */
.mat-column-title { .mat-column-title {
padding-left: 26px; padding-left: 26px;
flex: 1 0 200px; flex: 2 0 0;
.done-check {
margin-right: 10px;
}
} }
/** Duration */ /** Duration */
.mat-column-duration { .mat-column-info {
flex: 0 0 100px; 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 */ /** Speakers indicator */
.mat-column-speakers { .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; justify-content: flex-end !important;
} }
} }

View File

@ -1,13 +1,17 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar, MatDialog } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ViewItem } from '../../models/view-item'; import { ViewItem } from '../../models/view-item';
import { ListViewBaseComponent } from 'app/site/base/list-view-base'; import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { AgendaRepositoryService } from '../../services/agenda-repository.service'; import { AgendaRepositoryService } from '../../services/agenda-repository.service';
import { PromptService } from '../../../../core/services/prompt.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. * List view for the agenda.
@ -18,6 +22,18 @@ import { PromptService } from '../../../../core/services/prompt.service';
styleUrls: ['./agenda-list.component.scss'] styleUrls: ['./agenda-list.component.scss']
}) })
export class AgendaListComponent extends ListViewBaseComponent<ViewItem> implements OnInit { 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 * The usual constructor for components
* @param titleService Setting the browser tab title * @param titleService Setting the browser tab title
@ -26,8 +42,11 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
* @param route Angulars ActivatedRoute * @param route Angulars ActivatedRoute
* @param router Angulars router * @param router Angulars router
* @param repo the agenda repository, * @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( public constructor(
titleService: Title, titleService: Title,
@ -36,7 +55,11 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private repo: AgendaRepositoryService, 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); super(titleService, translate, matSnackBar);
@ -55,12 +78,17 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
this.dataSource.data = newAgendaItem; this.dataSource.data = newAgendaItem;
this.checkSelection(); 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 * Gets content object from the repository rather than from the model
* to avoid race conditions * to avoid race conditions
*
* @param item the item that was selected from the list view * @param item the item that was selected from the list view
*/ */
public singleSelectAction(item: ViewItem): void { public singleSelectAction(item: ViewItem): void {
@ -68,8 +96,48 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
this.router.navigate([contentObject.getDetailStateURL()]); 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 * Handler for the speakers button
*
* @param item indicates the row that was clicked on * @param item indicates the row that was clicked on
*/ */
public onSpeakerIcon(item: ViewItem): void { public onSpeakerIcon(item: ViewItem): void {
@ -84,6 +152,18 @@ export class AgendaListComponent extends ListViewBaseComponent<ViewItem> impleme
this.router.navigate(['topics/new'], { relativeTo: this.route }); 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 * Handler for deleting multiple entries. Needs items in selectedRows, which
* is only filled with any data in multiSelect mode * 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. * is only filled with any data in multiSelect mode.
* *
* @param visible true if the item is to be shown * @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) { 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[] { public getColumnDefinition(): string[] {
const list = ['title', 'duration', 'speakers']; const list = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
if (this.isMultiSelect) { if (this.isMultiSelect) {
return ['selector'].concat(list); return ['selector'].concat(list);
} }

View File

@ -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>

View File

@ -0,0 +1,8 @@
.itemDialogForm {
display: inline-block;
::ng-deep {
.mat-form-field {
width: 100%;
}
}
}

View File

@ -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();
});*/
});

View File

@ -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();
}
}
}

View File

@ -1,4 +1,6 @@
<os-head-bar <os-head-bar
[mainButton]="isAllowed('edit')"
mainButtonIcon="edit"
[nav]="false" [nav]="false"
[goBack]="true" [goBack]="true"
[editMode]="editTopic" [editMode]="editTopic"
@ -29,16 +31,22 @@
<h2 *ngIf="editTopic">{{ topicForm.get('title').value }}</h2> <h2 *ngIf="editTopic">{{ topicForm.get('title').value }}</h2>
</div> </div>
<mat-card *ngIf="topic || editTopic" class="topic-text"> <mat-card *ngIf="topic.text || topic.hasAttachments() || editTopic" class="topic-text">
<div> <div>
<span *ngIf="!editTopic"> <span *ngIf="!editTopic">
<!-- Render topic text as HTML -->
<div [innerHTML]="topic.text"></div> <div [innerHTML]="topic.text"></div>
</span> </span>
</div> </div>
<div *ngIf="topic.attachments && topic.attachments.length > 0"> <div *ngIf="topic.hasAttachments() && !editTopic">
<h3> <h3>
<span translate>Attachments</span>: <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 --> <!-- TODO: Mediafiles and attachments are not fully implemented -->
</h3> </h3>
</div> </div>
@ -52,14 +60,49 @@
osAutofocus osAutofocus
required required
formControlName="title" formControlName="title"
placeholder="{{ 'Title' | translate}}" placeholder="{{ 'Title' | translate }}"
/> />
<mat-error *ngIf="topicForm.invalid" translate>A name is required</mat-error> <mat-error *ngIf="topicForm.invalid" translate>A name is required</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<!-- The editor --> <!-- The editor -->
<div><editor formControlName="text" [init]="tinyMceSettings"></editor></div> <editor formControlName="text" [init]="tinyMceSettings"></editor>
<!-- TODO: Select Mediafiles as attachments here -->
<!-- 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> </form>
</mat-card> </mat-card>
</div> </div>

View File

@ -11,6 +11,11 @@ import { BaseViewComponent } from 'app/site/base/base-view';
import { PromptService } from 'app/core/services/prompt.service'; import { PromptService } from 'app/core/services/prompt.service';
import { TopicRepositoryService } from '../../services/topic-repository.service'; import { TopicRepositoryService } from '../../services/topic-repository.service';
import { ViewTopic } from '../../models/view-topic'; 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. * Detail page for topics.
@ -41,8 +46,24 @@ export class TopicDetailComponent extends BaseViewComponent {
*/ */
public topicForm: FormGroup; 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. * Constructor for the topic detail page.
*
* @param title Setting the browsers title * @param title Setting the browsers title
* @param matSnackBar display errors and other messages * @param matSnackBar display errors and other messages
* @param translate Handles translations * @param translate Handles translations
@ -52,6 +73,8 @@ export class TopicDetailComponent extends BaseViewComponent {
* @param formBuilder Angulars FormBuilder * @param formBuilder Angulars FormBuilder
* @param repo The topic repository * @param repo The topic repository
* @param promptService Allows warning before deletion attempts * @param promptService Allows warning before deletion attempts
* @param operator The current user
* @param DS Data Store
*/ */
public constructor( public constructor(
title: Title, title: Title,
@ -62,15 +85,29 @@ export class TopicDetailComponent extends BaseViewComponent {
private location: Location, private location: Location,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private repo: TopicRepositoryService, private repo: TopicRepositoryService,
private promptService: PromptService private promptService: PromptService,
private operator: OperatorService,
private DS: DataStoreService
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.getTopicByUrl(); this.getTopicByUrl();
this.createForm(); 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 * Set the edit mode to the given Status
*
* @param mode * @param mode
*/ */
public setEditMode(mode: boolean): void { public setEditMode(mode: boolean): void {
@ -88,13 +125,17 @@ export class TopicDetailComponent extends BaseViewComponent {
*/ */
public async saveTopic(): Promise<void> { public async saveTopic(): Promise<void> {
if (this.newTopic && this.topicForm.valid) { 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); const response = await this.repo.create(this.topicForm.value);
this.router.navigate([`/agenda/topics/${response.id}`]); this.router.navigate([`/agenda/topics/${response.id}`]);
// after creating a new topic, go "back" to agenda list view // after creating a new topic, go "back" to agenda list view
this.location.replaceState('/agenda/'); this.location.replaceState('/agenda/');
} else { } else {
await this.repo.update(this.topicForm.value, this.topic);
this.setEditMode(false); this.setEditMode(false);
await this.repo.update(this.topicForm.value, this.topic);
} }
} }
@ -103,9 +144,14 @@ export class TopicDetailComponent extends BaseViewComponent {
*/ */
public createForm(): void { public createForm(): void {
this.topicForm = this.formBuilder.group({ this.topicForm = this.formBuilder.group({
title: ['', Validators.required], agenda_type: [],
text: [''] 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 => { Object.keys(this.topicForm.controls).forEach(ctrl => {
topicPatch[ctrl] = this.topic[ctrl]; topicPatch[ctrl] = this.topic[ctrl];
}); });
this.topicForm.patchValue(topicPatch); this.topicForm.patchValue(topicPatch);
} }
@ -139,6 +186,7 @@ export class TopicDetailComponent extends BaseViewComponent {
/** /**
* Loads a top from the repository * Loads a top from the repository
*
* @param id the id of the required topic * @param id the id of the required topic
*/ */
public loadTopic(id: number): void { public loadTopic(id: number): void {
@ -172,14 +220,31 @@ export class TopicDetailComponent extends BaseViewComponent {
/** /**
* Handler for the delete button. Uses the PromptService * 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}?`; const content = this.translate.instant('Delete') + ` ${this.topic.title}?`;
if (await this.promptService.open('Are you sure?', content)) { 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']); 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 * clicking Shift and Enter will save automatically
* Hitting escape while in topicForm should cancel editing * Hitting escape while in topicForm should cancel editing

View File

@ -18,6 +18,10 @@ export class ViewItem extends BaseViewModel {
return this.item ? this.item.id : null; return this.item ? this.item.id : null;
} }
public get itemNumber(): string {
return this.item ? this.item.item_number : null;
}
public get duration(): number { public get duration(): number {
return this.item ? this.item.duration : null; return this.item ? this.item.duration : null;
} }
@ -26,6 +30,22 @@ export class ViewItem extends BaseViewModel {
return this.item ? this.item.speakerAmount : null; 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) { public constructor(item: Item, contentObject: AgendaBaseModel) {
super(); super();
this._item = item; 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 { public getListTitle(): string {
const contentObject: AgendaBaseModel = this.contentObject; const contentObject: AgendaBaseModel = this.contentObject;
const numberPrefix = this.itemNumber ? `${this.itemNumber} · ` : '';
if (contentObject) { if (contentObject) {
return contentObject.getAgendaTitleWithType(); return numberPrefix + contentObject.getAgendaTitleWithType();
} else { } else {
return this.item ? this.item.title_with_type : null; return this.item ? numberPrefix + this.item.title_with_type : null;
} }
} }

View File

@ -2,9 +2,11 @@ import { BaseViewModel } from '../../base/base-view-model';
import { Topic } from 'app/shared/models/topics/topic'; import { Topic } from 'app/shared/models/topics/topic';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Item } from 'app/shared/models/agenda/item'; 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 * Provides "safe" access to topic with all it's components
* @ignore
*/ */
export class ViewTopic extends BaseViewModel { export class ViewTopic extends BaseViewModel {
private _topic: Topic; private _topic: Topic;
@ -31,6 +33,10 @@ export class ViewTopic extends BaseViewModel {
return this.topic ? this.topic.agenda_item_id : null; 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 { public get title(): string {
return this.topic ? this.topic.title : null; return this.topic ? this.topic.title : null;
} }
@ -50,9 +56,16 @@ export class ViewTopic extends BaseViewModel {
return this.title; return this.title;
} }
public updateValues(update: Topic): void { public hasAttachments(): boolean {
if (this.id === update.id) { return this.attachments && this.attachments.length > 0;
this._topic = update; }
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;
}
} }
} }
} }

View File

@ -15,6 +15,7 @@ import { Speaker } from 'app/shared/models/agenda/speaker';
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
import { HttpService } from 'app/core/services/http.service'; import { HttpService } from 'app/core/services/http.service';
import { ConfigService } from 'app/core/services/config.service'; import { ConfigService } from 'app/core/services/config.service';
import { DataSendService } from 'app/core/services/data-send.service';
/** /**
* Repository service for users * Repository service for users
@ -27,16 +28,19 @@ import { ConfigService } from 'app/core/services/config.service';
export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> { export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
/** /**
* Contructor for agenda repository. * Contructor for agenda repository.
*
* @param DS The DataStore * @param DS The DataStore
* @param httpService OpenSlides own HttpService * @param httpService OpenSlides own HttpService
* @param mapperService OpenSlides mapping service for collection strings * @param mapperService OpenSlides mapping service for collection strings
* @param config Read config variables * @param config Read config variables
* @param dataSend send models to the server
*/ */
public constructor( public constructor(
protected DS: DataStoreService, protected DS: DataStoreService,
private httpService: HttpService, private httpService: HttpService,
mapperService: CollectionStringModelMapperService, mapperService: CollectionStringModelMapperService,
private config: ConfigService private config: ConfigService,
private dataSend: DataSendService
) { ) {
super(DS, mapperService, Item); 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} * Returns the corresponding content object to a given {@link Item} as an {@link AgendaBaseModel}
* Used dynamically because of heavy race conditions * Used dynamically because of heavy race conditions
*
* @param agendaItem the target agenda Item * @param agendaItem the target agenda Item
* @returns the content object of the given item. Might be null if it was not found. * @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 * Generate viewSpeaker objects from a given agenda Item
*
* @param item agenda Item holding speakers * @param item agenda Item holding speakers
* @returns the list of view speakers corresponding to the given item * @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. * Add a new speaker to an agenda item.
* Sends the users ID to the server * Sends the users ID to the server
*
* Might need another repo * Might need another repo
*
* @param id {@link User} id of the new speaker * @param id {@link User} id of the new speaker
* @param agenda the target agenda item * @param agenda the target agenda item
*/ */
@ -100,6 +106,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
/** /**
* Sets the given speaker ID to Speak * Sets the given speaker ID to Speak
*
* @param id the speakers id * @param id the speakers id
* @param agenda the target agenda item * @param agenda the target agenda item
*/ */
@ -110,6 +117,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
/** /**
* Stops the current speaker * Stops the current speaker
*
* @param agenda the target agenda item * @param agenda the target agenda item
*/ */
public async stopSpeaker(agenda: Item): Promise<void> { public async stopSpeaker(agenda: Item): Promise<void> {
@ -119,6 +127,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
/** /**
* Marks the current speaker * Marks the current speaker
*
* @param id {@link User} id of the new speaker * @param id {@link User} id of the new speaker
* @param mark determine if the user was marked or not * @param mark determine if the user was marked or not
* @param agenda the target agenda item * @param agenda the target agenda item
@ -130,6 +139,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
/** /**
* Deletes the given speaker for the agenda * Deletes the given speaker for the agenda
*
* @param id the speakers id * @param id the speakers id
* @param agenda the target agenda item * @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 * Posts an (manually) sorted speaker list to the server
*
* @param ids array of speaker id numbers * @param ids array of speaker id numbers
* @param Item the target agenda item * @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> { public async update(update: Partial<Item>, viewItem: ViewItem): Promise<void> {
return null; 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 * @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> { public delete(item: ViewItem): Promise<void> {
return null; throw new Error("Method not implemented.");
} }
/** /**
* @ignore * @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> { public async create(item: Item): Promise<Identifiable> {
return null; throw new Error("Method not implemented.");
} }
/** /**
* Creates the viewItem out of a given item * Creates the viewItem out of a given item
*
* @param item the item that should be converted to view item * @param item the item that should be converted to view item
* @returns a new view item * @returns a new view item
*/ */

View File

@ -34,6 +34,7 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
/** /**
* Creates a new viewModel out of the given model * Creates a new viewModel out of the given model
*
* @param topic The topic that shall be converted into a view topic * @param topic The topic that shall be converted into a view topic
* @returns a new view topic * @returns a new view topic
*/ */
@ -56,6 +57,7 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
/** /**
* Save a new topic * Save a new topic
*
* @param topicData Partial topic data to be created * @param topicData Partial topic data to be created
* @returns an Identifiable (usually id) as promise * @returns an Identifiable (usually id) as promise
*/ */
@ -81,6 +83,7 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
/** /**
* Delete a topic * Delete a topic
*
* @param viewTopic the topic that should be removed * @param viewTopic the topic that should be removed
*/ */
public async delete(viewTopic: ViewTopic): Promise<void> { public async delete(viewTopic: ViewTopic): Promise<void> {

View File

@ -7,7 +7,7 @@ import { BaseViewComponent } from './base-view';
export abstract class ListViewBaseComponent<V extends BaseViewModel> extends BaseViewComponent { 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>; public dataSource: MatTableDataSource<V>;
@ -17,12 +17,12 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
protected canMultiSelect = false; 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}. * see {@link selectItem}.
*/ */
public selectedRows: V[]; public selectedRows: V[];
@ -75,7 +75,7 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
*/ */
public selectItem(row: V, event: MouseEvent): void { public selectItem(row: V, event: MouseEvent): void {
event.stopPropagation(); event.stopPropagation();
if (!this._multiSelectModus) { if (!this._multiSelectMode) {
this.singleSelectAction(row); this.singleSelectAction(row);
} else { } else {
const idx = this.selectedRows.indexOf(row); const idx = this.selectedRows.indexOf(row);
@ -99,10 +99,10 @@ export abstract class ListViewBaseComponent<V extends BaseViewModel> extends Bas
*/ */
public toggleMultiSelect(): void { public toggleMultiSelect(): void {
if (!this.canMultiSelect || this.isMultiSelect) { if (!this.canMultiSelect || this.isMultiSelect) {
this._multiSelectModus = false; this._multiSelectMode = false;
this.clearSelection(); this.clearSelection();
} else { } 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 * Returns the current state of the multiSelect modus
*/ */
public get isMultiSelect(): boolean { 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 * @param item The row's entry
*/ */
public isSelected(item: V): boolean { public isSelected(item: V): boolean {
if (!this._multiSelectModus) { if (!this._multiSelectMode) {
return false; return false;
} }
return this.selectedRows.indexOf(item) >= 0; return this.selectedRows.indexOf(item) >= 0;

View 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();
});
});

View 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 '';
}
}
}

View File

@ -309,7 +309,6 @@
<form <form
class="motion-content" class="motion-content"
[formGroup]="contentForm" [formGroup]="contentForm"
(clickdown)="onKeyDown($event)"
(keydown)="onKeyDown($event)" (keydown)="onKeyDown($event)"
(ngSubmit)="saveMotion()" (ngSubmit)="saveMotion()"
*ngIf="motion" *ngIf="motion"
@ -492,7 +491,7 @@
<os-search-value-selector <os-search-value-selector
ngDefaultControl ngDefaultControl
[form]="contentForm" [form]="contentForm"
[formControl]="contentForm.get('parent_id')" [formControl]="contentForm.get('agenda_parent_id')"
[multiple]="false" [multiple]="false"
[includeNone]="true" [includeNone]="true"
listname="{{ 'Parent Item' | translate }}" listname="{{ 'Parent Item' | translate }}"

View File

@ -449,7 +449,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
reason: [''], reason: [''],
category_id: [''], category_id: [''],
attachments_id: [[]], attachments_id: [[]],
parent_id: [], agenda_parent_id: [],
agenda_type: [''], agenda_type: [''],
submitters_id: [], submitters_id: [],
supporters_id: [[]], supporters_id: [[]],
@ -495,6 +495,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
); );
motionValues.text = ''; motionValues.text = '';
} }
motion.deserialize(motionValues); motion.deserialize(motionValues);
return motion; return motion;
} }
@ -504,6 +505,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
*/ */
public async createMotion(): Promise<void> { public async createMotion(): Promise<void> {
const newMotionValues = { ...this.contentForm.value }; const newMotionValues = { ...this.contentForm.value };
if (!newMotionValues.agenda_parent_id) {
delete newMotionValues.agenda_parent_id;
}
const motion = this.prepareMotionForSave(newMotionValues, CreateMotion); const motion = this.prepareMotionForSave(newMotionValues, CreateMotion);
try { try {

View File

@ -1,5 +1,5 @@
<os-head-bar <os-head-bar
[mainButton]="isAllowed('manage')" [mainButton]="isAllowed('changePersonal')"
mainButtonIcon="edit" mainButtonIcon="edit"
[nav]="false" [nav]="false"
[editMode]="editUser" [editMode]="editUser"