Merge pull request #4059 from tsiegleauq/agenda-list-controls
Add controls to agenda list
This commit is contained in:
commit
3a2df3b731
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
<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>
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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> {
|
||||||
|
@ -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;
|
||||||
|
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
|
<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 }}"
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user