Merge pull request #4924 from tsiegleauq/amendment-lists

Add amendment list
This commit is contained in:
Sean 2019-08-16 09:41:37 +02:00 committed by GitHub
commit eaf4d8180d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 712 additions and 165 deletions

View File

@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver';
import { ProgressSnackBarComponent } from 'app/shared/components/progress-snack-bar/progress-snack-bar.component';
import { ExportFormData } from 'app/site/motions/modules/motion-list/components/motion-export-dialog/motion-export-dialog.component';
import { MotionExportInfo } from 'app/site/motions/services/motion-export.service';
import { ConfigService } from '../ui-services/config.service';
import { HttpService } from '../core-services/http.service';
import { ProgressService } from '../ui-services/progress.service';
@ -163,7 +163,7 @@ export class PdfDocumentService {
private async getStandardPaper(
documentContent: object,
metadata?: object,
exportInfo?: ExportFormData,
exportInfo?: MotionExportInfo,
imageUrls?: string[],
customMargins?: [number, number, number, number],
landscape?: boolean
@ -308,7 +308,7 @@ export class PdfDocumentService {
* @param lrMargin optionally overriding the margins
* @returns the footer doc definition
*/
private getFooter(lrMargin?: [number, number], exportInfo?: ExportFormData): object {
private getFooter(lrMargin?: [number, number], exportInfo?: MotionExportInfo): object {
const columns = [];
const showPageNr = exportInfo && exportInfo.pdfOptions ? exportInfo.pdfOptions.includes('page') : true;
const showDate = exportInfo && exportInfo.pdfOptions ? exportInfo.pdfOptions.includes('date') : false;
@ -399,7 +399,7 @@ export class PdfDocumentService {
* @param filename the name of the file to use
* @param metadata
*/
public download(docDefinition: object, filename: string, metadata?: object, exportInfo?: ExportFormData): void {
public download(docDefinition: object, filename: string, metadata?: object, exportInfo?: MotionExportInfo): void {
this.getStandardPaper(docDefinition, metadata, exportInfo).then(doc => {
this.createPdf(doc, filename);
});

View File

@ -497,6 +497,13 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
);
}
/**
* @returns all amendments
*/
public getAllAmendmentsInstantly(): ViewMotion[] {
return this.getViewModelList().filter(motion => !!motion.parent_id);
}
/**
* Returns the amendments to a given motion
*

View File

@ -146,17 +146,21 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
return this._filterStack;
}
/**
* The key to access stored valued
*/
private storageKey: string;
/**
* Constructor.
*
* @param name the name of the filter service
* @param store storage service, to read saved filter variables
*/
public constructor(
protected name: string,
private store: StorageService,
private OSStatus: OpenSlidesStatusService
) {}
public constructor(private store: StorageService, private OSStatus: OpenSlidesStatusService) {
this.storageKey = this.constructor.name;
console.log('storage-key: ', this.storageKey);
}
/**
* Initializes the filterService.
@ -166,7 +170,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
public async initFilters(inputData: Observable<V[]>): Promise<void> {
let storedFilter: OsFilter[] = null;
if (!this.OSStatus.isInHistoryMode) {
storedFilter = await this.store.get<OsFilter[]>('filter_' + this.name);
storedFilter = await this.store.get<OsFilter[]>('filter_' + this.storageKey);
}
if (storedFilter && this.isOsFilter(storedFilter)) {
@ -237,7 +241,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
let storedFilter = null;
if (!this.OSStatus.isInHistoryMode) {
storedFilter = await this.store.get<OsFilter[]>('filter_' + this.name);
storedFilter = await this.store.get<OsFilter[]>('filter_' + this.storageKey);
}
if (!!storedFilter) {
@ -276,43 +280,43 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
* @param repo repository to create dynamic filters from
* @param filter the OSFilter for the filter property
* @param noneOptionLabel The label of the non option, if set
* @param exexcludeIds Set if certain ID's should be excluded from filtering
* @param filterFn custom filter function if required
*/
protected updateFilterForRepo(
repo: BaseRepository<BaseViewModel, BaseModel, TitleInformation>,
filter: OsFilter,
noneOptionLabel?: string,
excludeIds?: number[]
filterFn?: (filter: BaseViewModel<any>) => boolean
): void {
repo.getViewModelListObservable().subscribe(viewModel => {
if (viewModel && viewModel.length) {
let filterProperties: (OsFilterOption | string)[];
filterProperties = viewModel
.filter(model => (excludeIds && excludeIds.length ? !excludeIds.includes(model.id) : true))
.map((model: HierarchyModel) => {
return {
condition: model.id,
label: model.getTitle(),
isChild: !!model.parent,
children:
model.children && model.children.length
? model.children.map(child => {
return {
label: child.getTitle(),
condition: child.id
};
})
: undefined
};
});
filterProperties.push('-');
filterProperties.push({
condition: null,
label: noneOptionLabel
filterProperties = viewModel.filter(filterFn ? filterFn : () => true).map((model: HierarchyModel) => {
return {
condition: model.id,
label: model.getTitle(),
isChild: !!model.parent,
children:
model.children && model.children.length
? model.children.map(child => {
return {
label: child.getTitle(),
condition: child.id
};
})
: undefined
};
});
if (!!noneOptionLabel) {
filterProperties.push('-');
filterProperties.push({
condition: null,
label: noneOptionLabel
});
}
filter.options = filterProperties;
this.setFilterDefinitions();
}
@ -325,7 +329,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
public storeActiveFilters(): void {
this.updateFilteredData();
if (!this.OSStatus.isInHistoryMode) {
this.store.set('filter_' + this.name, this.filterDefinitions);
this.store.set('filter_' + this.storageKey, this.filterDefinitions);
}
}

View File

@ -102,7 +102,7 @@ export class SearchValueSelectorComponent implements OnDestroy {
* Placeholder of the List
*/
@Input()
public listname: String;
public listname: string;
/**
* Name of the Form

View File

@ -22,7 +22,7 @@ export class AgendaFilterListService extends BaseFilterListService<ViewItem> {
* @param translate Translation service
*/
public constructor(store: StorageService, OSStatus: OpenSlidesStatusService, private translate: TranslateService) {
super('Agenda', store, OSStatus);
super(store, OSStatus);
}
/**

View File

@ -19,7 +19,7 @@ export class AssignmentFilterListService extends BaseFilterListService<ViewAssig
* @param translate translate service
*/
public constructor(store: StorageService, OSStatus: OpenSlidesStatusService) {
super('Assignments', store, OSStatus);
super(store, OSStatus);
}
/**

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AmendmentListComponent } from './amendment-list.component';
const routes: Routes = [
{
path: '',
component: AmendmentListComponent,
pathMatch: 'full'
},
{ path: ':id', component: AmendmentListComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AmendmentListRoutingModule {}

View File

@ -0,0 +1,107 @@
<os-head-bar [nav]="false" [multiSelectMode]="isMultiSelect" goBack="true">
<!-- Title -->
<div class="title-slot"><h2 translate>Amendments</h2></div>
<!-- Menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="amendmentListMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<!-- Multiselect info -->
<div class="central-info-slot">
<button mat-icon-button (click)="toggleMultiSelect()"><mat-icon>arrow_back</mat-icon></button>
<span>{{ selectedRows.length }}&nbsp;</span><span translate>selected</span>
</div>
</os-head-bar>
<os-list-view-table
[repo]="motionRepo"
[sortService]="motionSortService"
[filterService]="amendmentFilterService"
[columns]="tableColumnDefinition"
[filterProps]="filterProps"
[multiSelect]="isMultiSelect"
listStorageKey="amendments"
[(selectedRows)]="selectedRows"
(dataSourceChange)="onDataSourceChange($event)"
>
<!-- Meta -->
<div *pblNgridCellDef="'meta'; row as motion" class="cell-slot fill">
<a class="detail-link" [routerLink]="motion.getDetailStateURL()"></a>
<div class="column-identifier innerTable">
<!-- Identifier and line -->
<div class="title-line">
{{ motion.identifier }}
(<span translate>Line</span>&nbsp;<span>{{ getChangeLines(motion) }}</span
>)
</div>
<!-- Submitter -->
<div class="submitters-line">
<span *ngIf="motion.submitters.length">
<span translate>by</span>
{{ motion.submitters }}
</span>
<span *ngIf="motion.submitters.length">
&middot;
</span>
<span translate>Sequential number</span>
{{ motion.id }}
</div>
<!-- State -->
<div>
<mat-basic-chip *ngIf="motion.state" [ngClass]="motion.stateCssColor" [disabled]="true">
{{ motionRepo.getExtendedStateLabel(motion) }}
</mat-basic-chip>
</div>
<!-- Reco -->
<div class="spacer-top-3" *ngIf="motion.recommendation && motion.state.next_states_id.length > 0">
<mat-basic-chip class="bluegrey" [disabled]="true">
{{ this.motionRepo.getExtendedRecommendationLabel(motion) }}
</mat-basic-chip>
</div>
</div>
</div>
<!-- Summary -->
<div *pblNgridCellDef="'summary'; row as motion" class="cell-slot fill">
<div class="innerTable">
<div class="motion-text" [innerHtml]="sanitizeText(getAmendmentSummary(motion))"></div>
</div>
</div>
<!-- List of Speakers -->
<div *pblNgridCellDef="'speakers'; row as motion" class="cell-slot fill">
<os-speaker-button [object]="motion"></os-speaker-button>
</div>
</os-list-view-table>
<mat-menu #amendmentListMenu="matMenu">
<div *ngIf="!isMultiSelect">
<div *osPerms="'motions.can_manage'">
<button mat-menu-item (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Multiselect</span>
</button>
</div>
<button mat-menu-item (click)="openExportDialog()">
<mat-icon>archive</mat-icon>
<span translate>Export</span>
</button>
</div>
<div *ngIf="isMultiSelect">
<button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon>
<span translate>Select all</span>
</button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="deselectAll()">
<mat-icon>clear</mat-icon>
<span translate>Deselect all</span>
</button>
</div>
</mat-menu>

View File

@ -0,0 +1,2 @@
@import '~assets/styles/motion-styles-common';
@import 'app/site/motions/styles/motion-list-styles.scss';

View File

@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { AmendmentListComponent } from './amendment-list.component';
describe('AmendmentListComponent', () => {
let component: AmendmentListComponent;
let fixture: ComponentFixture<AmendmentListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [AmendmentListComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AmendmentListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,189 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog, MatSnackBar } from '@angular/material';
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { AmendmentFilterListService } from '../../services/amendment-filter-list.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DiffLinesInParagraph } from 'app/core/ui-services/diff.service';
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
import { largeDialogSettings } from 'app/shared/utils/dialog-settings';
import { BaseListViewComponent } from 'app/site/base/base-list-view';
import { MotionExportDialogComponent } from '../shared-motion/motion-export-dialog/motion-export-dialog.component';
import { MotionExportInfo, MotionExportService } from '../../services/motion-export.service';
import { MotionSortListService } from '../../services/motion-sort-list.service';
import { ViewMotion } from '../../models/view-motion';
/**
* Shows all the amendments in the NGrid table
*/
@Component({
selector: 'os-amendment-list',
templateUrl: './amendment-list.component.html',
styleUrls: ['./amendment-list.component.scss']
})
export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> implements OnInit {
private parentMotionId: number;
/**
* Hold item visibility
*/
public itemVisibility = ItemVisibilityChoices;
/**
* To hold the motions line length
*/
private motionLineLength: number;
/**
* Column defintiion
*/
public tableColumnDefinition: PblColumnDefinition[] = [
{
prop: 'meta',
minWidth: 250,
width: '15%'
},
{
prop: 'summary',
width: 'auto'
},
{
prop: 'speakers',
width: this.singleButtonWidth
}
];
/**
* To filter stuff
*/
public filterProps = ['submitters', 'title', 'identifier'];
/**
*
* @param titleService set the title
* @param translate translate stuff
* @param matSnackBar show errors
* @param storage store and recall
* @param motionRepo get the motions
* @param motionSortService the default motion sorter
*
* @param configService get config vars
*/
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
storage: StorageService,
route: ActivatedRoute,
public motionRepo: MotionRepositoryService,
public motionSortService: MotionSortListService,
public amendmentFilterService: AmendmentFilterListService,
private sanitizer: DomSanitizer,
private configService: ConfigService,
private dialog: MatDialog,
private motionExport: MotionExportService,
private linenumberingService: LinenumberingService
) {
super(titleService, translate, matSnackBar, storage);
super.setTitle('Amendments');
this.canMultiSelect = true;
this.parentMotionId = parseInt(route.snapshot.params.id, 10);
}
/**
* Observe the line length
*/
public ngOnInit(): void {
this.configService.get<number>('motions_line_length').subscribe(lineLength => {
this.motionLineLength = lineLength;
});
if (!!this.parentMotionId) {
this.amendmentFilterService.clearAllFilters();
this.amendmentFilterService.parentMotionId = this.parentMotionId;
} else {
this.amendmentFilterService.parentMotionId = 0;
}
}
/**
* Helper function to get amendment paragraphs of a given motion
*
* @param amendment the get the paragraphs from
* @returns DiffLinesInParagraph-List
*/
private getDiffLines(amendment: ViewMotion): DiffLinesInParagraph[] {
if (amendment.isParagraphBasedAmendment()) {
return this.motionRepo.getAmendmentParagraphs(amendment, this.motionLineLength, false);
} else {
return null;
}
}
/**
* Extract the lines of the amendments
* If an amendments has multiple changes, they will be printed like an array of strings
*
* @param amendment the motion to create the amendment to
* @return The lines of the amendment
*/
public getChangeLines(amendment: ViewMotion): string {
const diffLines = this.getDiffLines(amendment);
if (!!diffLines) {
return diffLines
.map(diffLine => {
if (diffLine.diffLineTo === diffLine.diffLineFrom + 1) {
return '' + diffLine.diffLineFrom;
} else {
return `${diffLine.diffLineFrom} - ${diffLine.diffLineTo - 1}`;
}
})
.toString();
}
}
/**
* Formulate the amendment summary
*
* @param amendment the motion to create the amendment to
* @returns the amendments as string, if they are multiple they gonna be separated by `[...]`
*/
public getAmendmentSummary(amendment: ViewMotion): string {
const diffLines = this.getDiffLines(amendment);
if (!!diffLines) {
return diffLines
.map(diffLine => {
return this.linenumberingService.stripLineNumbers(diffLine.text);
})
.join('[...]');
}
}
// todo put in own file
public openExportDialog(): void {
const exportDialogRef = this.dialog.open(MotionExportDialogComponent, {
...largeDialogSettings,
data: this.dataSource
});
exportDialogRef
.afterClosed()
.subscribe((exportInfo: MotionExportInfo) =>
this.motionExport.evaluateExportRequest(
exportInfo,
this.isMultiSelect ? this.selectedRows : this.dataSource.filteredData
)
);
}
public sanitizeText(text: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(text);
}
}

View File

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AmendmentListRoutingModule } from './amendment-list-routing.module';
import { AmendmentListComponent } from './amendment-list.component';
import { SharedModule } from 'app/shared/shared.module';
import { SharedMotionModule } from '../shared-motion/shared-motion.module';
@NgModule({
declarations: [AmendmentListComponent],
imports: [CommonModule, AmendmentListRoutingModule, SharedModule, SharedMotionModule]
})
export class AmendmentListModule {}

View File

@ -452,9 +452,12 @@
<!-- Ammendments -->
<div *ngIf="!editMotion && amendments && amendments.length > 0">
<h4 translate>Amendments</h4>
<div *ngFor="let amendment of amendments">
<a [routerLink]="amendment.getDetailStateURL()">{{ amendment.identifierOrTitle }}</a>
</div>
<a *ngIf="amendments.length === 1" [routerLink]="['/motions/amendments', motion.id]">
{{ amendments.length }} <span translate>Amendment</span>
</a>
<a *ngIf="amendments.length > 1" [routerLink]="['/motions/amendments', motion.id]">
{{ amendments.length }} <span translate>Amendments</span></a
>
</div>
<!-- motion polls -->

View File

@ -600,6 +600,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
}
}
}),
this.repo.amendmentsTo(motionId).subscribe((amendments: ViewMotion[]): void => {
this.amendments = amendments;
this.recalcUnifiedChanges();

View File

@ -104,10 +104,11 @@
</span>
</div>
<!-- TODO: These two now appear twice. Might be an own component -->
<!-- Workflow state -->
<div class="ellipsis-overflow white">
<mat-basic-chip *ngIf="motion.state" [ngClass]="motion.stateCssColor" [disabled]="true">
{{ getStateLabel(motion) }}
{{ this.motionRepo.getExtendedStateLabel(motion) }}
</mat-basic-chip>
</div>
@ -117,7 +118,7 @@
class="ellipsis-overflow white spacer-top-3"
>
<mat-basic-chip class="bluegrey" [disabled]="true">
{{ getRecommendationLabel(motion) }}
{{ motionRepo.getExtendedRecommendationLabel(motion) }}
</mat-basic-chip>
</div>
</div>
@ -219,6 +220,14 @@
<span translate>Multiselect</span>
</button>
</div>
<div *ngIf="perms.isAllowed('manage') || hasAmendments()">
<button mat-menu-item routerLink="amendments">
<!-- color_lens -->
<!-- format_paint -->
<mat-icon>color_lens</mat-icon>
<span translate>Amendments</span>
</button>
</div>
<div *ngIf="perms.isAllowed('manage')">
<button mat-menu-item routerLink="call-list">
<mat-icon>sort</mat-icon>

View File

@ -1,4 +1,5 @@
@import '~assets/styles/tables.scss';
@import '~app/site/motions/styles/motion-list-styles.scss';
// Determine the distance between the top edge to the start of the table content
$text-margin-top: 10px;
@ -27,9 +28,6 @@ $text-margin-top: 10px;
margin-top: $text-margin-top;
.title-line {
font-weight: 500;
font-size: 16px;
.attached-files {
.mat-icon {
display: inline-flex;
@ -45,10 +43,6 @@ $text-margin-top: 10px;
padding-right: 3px;
}
}
.submitters-line {
font-size: 90%;
}
}
.column-state {

View File

@ -8,7 +8,6 @@ import { TranslateService } from '@ngx-translate/core';
import { PblColumnDefinition } from '@pebula/ngrid';
import { StorageService } from 'app/core/core-services/storage.service';
import { PdfError } from 'app/core/pdf-services/pdf-document.service';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
@ -24,18 +23,12 @@ import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { MotionCsvExportService } from 'app/site/motions/services/motion-csv-export.service';
import { MotionExportInfo, MotionExportService } from 'app/site/motions/services/motion-export.service';
import { MotionFilterListService } from 'app/site/motions/services/motion-filter-list.service';
import { MotionMultiselectService } from 'app/site/motions/services/motion-multiselect.service';
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
import { MotionSortListService } from 'app/site/motions/services/motion-sort-list.service';
import { MotionXlsxExportService } from 'app/site/motions/services/motion-xlsx-export.service';
import { ViewTag } from 'app/site/tags/models/view-tag';
import {
ExportFormData,
FileFormat,
MotionExportDialogComponent
} from '../motion-export-dialog/motion-export-dialog.component';
import { MotionExportDialogComponent } from '../../../shared-motion/motion-export-dialog/motion-export-dialog.component';
interface TileCategoryInformation {
filter: string;
@ -208,12 +201,10 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
private categoryRepo: CategoryRepositoryService,
private workflowRepo: WorkflowRepositoryService,
public motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService,
private pdfExport: MotionPdfExportService,
private dialog: MatDialog,
public multiselectService: MotionMultiselectService,
public perms: LocalPermissionsService,
private motionXlsxExport: MotionXlsxExportService
private motionExport: MotionExportService
) {
super(titleService, translate, matSnackBar, storage);
this.canMultiSelect = true;
@ -358,37 +349,14 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
data: this.dataSource
});
exportDialogRef.afterClosed().subscribe((exportInfo: ExportFormData) => {
if (exportInfo && exportInfo.format) {
const data = this.isMultiSelect ? this.selectedRows : this.dataSource.filteredData;
if (exportInfo.format === FileFormat.PDF) {
try {
this.pdfExport.exportMotionCatalog(data, exportInfo);
} catch (err) {
if (err instanceof PdfError) {
this.raiseError(err.message);
} else {
throw err;
}
}
} else if (exportInfo.format === FileFormat.CSV) {
const content = [];
const comments = [];
if (exportInfo.content) {
content.push(...exportInfo.content);
}
if (exportInfo.metaInfo) {
content.push(...exportInfo.metaInfo);
}
if (exportInfo.comments) {
comments.push(...exportInfo.comments);
}
this.motionCsvExport.exportMotionList(data, content, comments, exportInfo.crMode);
} else if (exportInfo.format === FileFormat.XLSX) {
this.motionXlsxExport.exportMotionList(data, exportInfo.metaInfo, exportInfo.comments);
}
}
});
exportDialogRef
.afterClosed()
.subscribe((exportInfo: MotionExportInfo) =>
this.motionExport.evaluateExportRequest(
exportInfo,
this.isMultiSelect ? this.selectedRows : this.dataSource.filteredData
)
);
}
/**
@ -404,26 +372,6 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
}
}
/**
* Fetch a motion's current recommendation label
*
* @param motion
* @returns the current recommendation label (with extension)
*/
public getRecommendationLabel(motion: ViewMotion): string {
return this.motionRepo.getExtendedRecommendationLabel(motion);
}
/**
* Fetch a motion's current state label
*
* @param motion
* @returns the current state label (with extension)
*/
public getStateLabel(motion: ViewMotion): string {
return this.motionRepo.getExtendedStateLabel(motion);
}
/**
* This function saves the selected view by changes.
*
@ -495,6 +443,13 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
}
}
/**
* @returns if there are amendments or not
*/
public hasAmendments(): boolean {
return !!this.motionRepo.getAllAmendmentsInstantly().length;
}
/**
* Checks if categories are available.
*

View File

@ -4,7 +4,6 @@ import { RouterModule, Routes } from '@angular/router';
import { MotionListComponent } from './components/motion-list/motion-list.component';
const routes: Routes = [{ path: '', component: MotionListComponent, pathMatch: 'full' }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]

View File

@ -2,13 +2,12 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from 'app/shared/shared.module';
import { MotionExportDialogComponent } from './components/motion-export-dialog/motion-export-dialog.component';
import { MotionListRoutingModule } from './motion-list-routing.module';
import { MotionListComponent } from './components/motion-list/motion-list.component';
import { SharedMotionModule } from '../shared-motion/shared-motion.module';
@NgModule({
imports: [CommonModule, MotionListRoutingModule, SharedModule],
declarations: [MotionListComponent, MotionExportDialogComponent],
entryComponents: [MotionExportDialogComponent]
imports: [CommonModule, MotionListRoutingModule, SharedModule, SharedMotionModule],
declarations: [MotionListComponent]
})
export class MotionListModule {}

View File

@ -11,29 +11,7 @@ import { ConfigService } from 'app/core/ui-services/config.service';
import { ChangeRecoMode, LineNumberingMode } from 'app/site/motions/models/view-motion';
import { ViewMotionCommentSection } from 'app/site/motions/models/view-motion-comment-section';
import { motionImportExportHeaderOrder, noMetaData } from 'app/site/motions/motion-import-export-order';
import { InfoToExport } from 'app/site/motions/services/motion-pdf.service';
/**
* Determine the possible file format
*/
export enum FileFormat {
PDF = 1,
CSV,
XLSX
}
/**
* Shape the structure of the dialog data
*/
export interface ExportFormData {
format?: FileFormat;
lnMode?: LineNumberingMode;
crMode?: ChangeRecoMode;
content?: string[];
metaInfo?: InfoToExport[];
pdfOptions?: string[];
comments?: number[];
}
import { ExportFileFormat, MotionExportInfo } from 'app/site/motions/services/motion-export.service';
/**
* Dialog component to determine exporting.
@ -57,7 +35,7 @@ export class MotionExportDialogComponent implements OnInit {
/**
* to use the format in the template
*/
public fileFormat = FileFormat;
public fileFormat = ExportFileFormat;
/**
* The form that contains the export information.
@ -67,8 +45,8 @@ export class MotionExportDialogComponent implements OnInit {
/**
* The default export values in contrast to the restored values
*/
private defaults: ExportFormData = {
format: FileFormat.PDF,
private defaults: MotionExportInfo = {
format: ExportFileFormat.PDF,
content: ['text', 'reason'],
pdfOptions: ['toc', 'page'],
metaInfo: ['submitters', 'state', 'recommendation', 'category', 'origin', 'tags', 'motion_block', 'polls', 'id']
@ -130,26 +108,26 @@ export class MotionExportDialogComponent implements OnInit {
* Observes the form for changes to react dynamically
*/
public ngOnInit(): void {
this.exportForm.valueChanges.pipe(auditTime(500)).subscribe((value: ExportFormData) => {
this.exportForm.valueChanges.pipe(auditTime(500)).subscribe((value: MotionExportInfo) => {
this.store.set('motion_export_selection', value);
});
this.exportForm.get('format').valueChanges.subscribe((value: FileFormat) => this.onFormatChange(value));
this.exportForm.get('format').valueChanges.subscribe((value: ExportFileFormat) => this.onFormatChange(value));
}
/**
* React to changes on the file format
* @param format
*/
private onFormatChange(format: FileFormat): void {
private onFormatChange(format: ExportFileFormat): void {
// XLSX cannot have "content"
if (format === FileFormat.XLSX) {
if (format === ExportFileFormat.XLSX) {
this.disableControl('content');
} else {
this.enableControl('content');
}
if (format === FileFormat.CSV || format === FileFormat.XLSX) {
if (format === ExportFileFormat.CSV || format === ExportFileFormat.XLSX) {
this.disableControl('lnMode');
this.disableControl('crMode');
this.disableControl('pdfOptions');
@ -165,7 +143,7 @@ export class MotionExportDialogComponent implements OnInit {
this.votingResultButton.disabled = true;
}
if (format === FileFormat.PDF) {
if (format === ExportFileFormat.PDF) {
this.enableControl('lnMode');
this.enableControl('crMode');
this.enableControl('pdfOptions');
@ -222,7 +200,7 @@ export class MotionExportDialogComponent implements OnInit {
});
// restore selection or set default
this.store.get<ExportFormData>('motion_export_selection').then(restored => {
this.store.get<MotionExportInfo>('motion_export_selection').then(restored => {
if (!!restored) {
this.exportForm.patchValue(restored);
} else {

View File

@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from 'app/shared/shared.module';
import { MotionExportDialogComponent } from './motion-export-dialog/motion-export-dialog.component';
@NgModule({
imports: [CommonModule, SharedModule],
declarations: [MotionExportDialogComponent],
entryComponents: [MotionExportDialogComponent]
})
export class SharedMotionModule {}

View File

@ -52,6 +52,11 @@ const routes: Routes = [
loadChildren: () => import('./modules/motion-detail/motion-detail.module').then(m => m.MotionDetailModule),
data: { basePerm: 'motions.can_create' }
},
{
path: 'amendments',
loadChildren: () => import('./modules/amendment-list/amendment-list.module').then(m => m.AmendmentListModule),
data: { basePerm: 'motions.can_see' }
},
{
path: ':id',
loadChildren: () => import('./modules/motion-detail/motion-detail.module').then(m => m.MotionDetailModule),

View File

@ -2,9 +2,8 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MotionsRoutingModule } from './motions-routing.module';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
imports: [CommonModule, MotionsRoutingModule, SharedModule]
imports: [CommonModule, MotionsRoutingModule]
})
export class MotionsModule {}

View File

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { AmendmentFilterListService } from './amendment-filter-list.service';
describe('AmendmentFilterService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => {
const service: AmendmentFilterListService = TestBed.get(AmendmentFilterListService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,96 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { OpenSlidesStatusService } from 'app/core/core-services/openslides-status.service';
import { OperatorService } from 'app/core/core-services/operator.service';
import { StorageService } from 'app/core/core-services/storage.service';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { MotionBlockRepositoryService } from 'app/core/repositories/motions/motion-block-repository.service';
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
import { OsFilter } from 'app/core/ui-services/base-filter-list.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { MotionFilterListService } from './motion-filter-list.service';
import { ViewMotion } from '../models/view-motion';
/**
* Filter the list of Amendments
*/
@Injectable({
providedIn: 'root'
})
export class AmendmentFilterListService extends MotionFilterListService {
/**
* Private acessor for an amendment id
*/
private _parentMotionId: number;
/**
* publicly get an amendment id
*/
public set parentMotionId(id: number) {
this._parentMotionId = id;
}
private motionFilterOptions: OsFilter = {
property: 'parent_id',
label: 'Motion',
options: []
};
public constructor(
store: StorageService,
OSStatus: OpenSlidesStatusService,
categoryRepo: CategoryRepositoryService,
motionBlockRepo: MotionBlockRepositoryService,
commentRepo: MotionCommentSectionRepositoryService,
tagRepo: TagRepositoryService,
workflowRepo: WorkflowRepositoryService,
translate: TranslateService,
operator: OperatorService,
configService: ConfigService,
motionRepo: MotionRepositoryService
) {
super(
store,
OSStatus,
categoryRepo,
motionBlockRepo,
commentRepo,
tagRepo,
workflowRepo,
translate,
operator,
configService
);
this.updateFilterForRepo(motionRepo, this.motionFilterOptions, null, (model: ViewMotion) =>
motionRepo.hasAmendments(model)
);
}
/**
* @override from base filter list service
*
* @returns the list of Motions which only contains view motions
*/
protected preFilter(motions: ViewMotion[]): ViewMotion[] {
return motions.filter(motion => {
if (!!this._parentMotionId) {
return motion.parent_id === this._parentMotionId;
} else {
return !!motion.parent_id;
}
});
}
/**
* Currently, no filters for the amendment list, except the pre-filter
*/
protected getFilterDefinitions(): OsFilter[] {
return [this.motionFilterOptions].concat(super.getFilterDefinitions());
}
}

View File

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { MotionExportService } from './motion-export.service';
describe('MotionExportService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
})
);
it('should be created', () => {
const service: MotionExportService = TestBed.get(MotionExportService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,80 @@
import { Injectable } from '@angular/core';
import { PdfError } from 'app/core/pdf-services/pdf-document.service';
import { MotionCsvExportService } from './motion-csv-export.service';
import { MotionPdfExportService } from './motion-pdf-export.service';
import { InfoToExport } from './motion-pdf.service';
import { MotionXlsxExportService } from './motion-xlsx-export.service';
import { ChangeRecoMode, LineNumberingMode, ViewMotion } from '../models/view-motion';
/**
* Determine the possible file format
*/
export enum ExportFileFormat {
PDF = 1,
CSV,
XLSX
}
/**
* Shape the structure of the dialog data
*/
export interface MotionExportInfo {
format?: ExportFileFormat;
lnMode?: LineNumberingMode;
crMode?: ChangeRecoMode;
content?: string[];
metaInfo?: InfoToExport[];
pdfOptions?: string[];
comments?: number[];
}
/**
* Generic layer to unify any motion export
*/
@Injectable({
providedIn: 'root'
})
export class MotionExportService {
public constructor(
private pdfExport: MotionPdfExportService,
private csvExport: MotionCsvExportService,
private xlsxExport: MotionXlsxExportService
) {}
public evaluateExportRequest(exportInfo: MotionExportInfo, data: ViewMotion[]): void {
if (!!exportInfo.format) {
if (exportInfo.format === ExportFileFormat.PDF) {
try {
this.pdfExport.exportMotionCatalog(data, exportInfo);
} catch (err) {
if (err instanceof PdfError) {
console.error('PDFError: ', err);
/**
* TODO: Has been this.raiseError(err.message) before. Central error treatment
*/
} else {
throw err;
}
}
} else if (exportInfo.format === ExportFileFormat.CSV) {
const content = [];
const comments = [];
if (exportInfo.content) {
content.push(...exportInfo.content);
}
if (exportInfo.metaInfo) {
content.push(...exportInfo.metaInfo);
}
if (exportInfo.comments) {
comments.push(...exportInfo.comments);
}
this.csvExport.exportMotionList(data, content, comments, exportInfo.crMode);
} else if (exportInfo.format === ExportFileFormat.XLSX) {
this.xlsxExport.exportMotionList(data, exportInfo.metaInfo, exportInfo.comments);
}
} else {
throw new Error('No export format was provided');
}
}
}

View File

@ -134,7 +134,7 @@ export class MotionFilterListService extends BaseFilterListService<ViewMotion> {
private operator: OperatorService,
private config: ConfigService
) {
super('Motion', store, OSStatus);
super(store, OSStatus);
this.getWorkflowConfig();
this.updateFilterForRepo(categoryRepo, this.categoryFilterOptions, this.translate.instant('No category set'));

View File

@ -7,7 +7,7 @@ import { BorderType, PdfDocumentService, PdfError, StyleType } from 'app/core/pd
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { ExportFormData } from '../modules/motion-list/components/motion-export-dialog/motion-export-dialog.component';
import { MotionExportInfo } from './motion-export.service';
import { MotionPdfService } from './motion-pdf.service';
import { ViewCategory } from '../models/view-category';
import { ViewMotion } from '../models/view-motion';
@ -56,7 +56,7 @@ export class MotionPdfCatalogService {
* @param commentsToExport
* @returns pdfmake doc definition as object
*/
public motionListToDocDef(motions: ViewMotion[], exportInfo: ExportFormData): object {
public motionListToDocDef(motions: ViewMotion[], exportInfo: MotionExportInfo): object {
let doc = [];
const motionDocList = [];

View File

@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { PdfDocumentService } from 'app/core/pdf-services/pdf-document.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
import { ExportFormData } from '../modules/motion-list/components/motion-export-dialog/motion-export-dialog.component';
import { MotionExportInfo } from './motion-export.service';
import { MotionPdfCatalogService } from './motion-pdf-catalog.service';
import { MotionPdfService } from './motion-pdf.service';
import { ViewMotion } from '../models/view-motion';
@ -41,7 +41,7 @@ export class MotionPdfExportService {
* @param lnMode the desired line numbering mode
* @param crMode the desired change recomendation mode
*/
public exportSingleMotion(motion: ViewMotion, exportInfo?: ExportFormData): void {
public exportSingleMotion(motion: ViewMotion, exportInfo?: MotionExportInfo): void {
const doc = this.motionPdfService.motionToDocDef(motion, exportInfo);
const filename = `${this.translate.instant('Motion')} ${motion.identifierOrTitle}`;
const metadata = {
@ -60,7 +60,7 @@ export class MotionPdfExportService {
* @param infoToExport Determine the meta info to export
* @param commentsToExport Comments (by id) to export
*/
public exportMotionCatalog(motions: ViewMotion[], exportInfo: ExportFormData): void {
public exportMotionCatalog(motions: ViewMotion[], exportInfo: MotionExportInfo): void {
const doc = this.pdfCatalogService.motionListToDocDef(motions, exportInfo);
const filename = this.translate.instant(this.configService.instant<string>('motions_export_title'));
const metadata = {

View File

@ -13,7 +13,7 @@ import { LinenumberingService } from 'app/core/ui-services/linenumbering.service
import { CalculablePollKey } from 'app/core/ui-services/poll.service';
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
import { getRecommendationTypeName } from 'app/shared/utils/recommendation-type-names';
import { ExportFormData } from '../modules/motion-list/components/motion-export-dialog/motion-export-dialog.component';
import { MotionExportInfo } from './motion-export.service';
import { MotionPollService } from './motion-poll.service';
import { ChangeRecoMode, LineNumberingMode, ViewMotion } from '../models/view-motion';
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
@ -92,7 +92,7 @@ export class MotionPdfService {
* @param commentsToExport comments to chose for export. If 'allcomments' is set in infoToExport, this selection will be ignored and all comments exported
* @returns doc def for the motion
*/
public motionToDocDef(motion: ViewMotion, exportInfo?: ExportFormData): object {
public motionToDocDef(motion: ViewMotion, exportInfo?: MotionExportInfo): object {
let lnMode = exportInfo && exportInfo.lnMode ? exportInfo.lnMode : null;
let crMode = exportInfo && exportInfo.crMode ? exportInfo.crMode : null;
const infoToExport = exportInfo ? exportInfo.metaInfo : null;

View File

@ -0,0 +1,8 @@
.title-line {
font-weight: 500;
font-size: 16px;
}
.submitters-line {
font-size: 90%;
}

View File

@ -35,8 +35,13 @@ export class UserFilterListService extends BaseFilterListService<ViewUser> {
groupRepo: GroupRepositoryService,
private translate: TranslateService
) {
super('User', store, OSStatus);
this.updateFilterForRepo(groupRepo, this.userGroupFilterOptions, this.translate.instant('Default'), [1]);
super(store, OSStatus);
this.updateFilterForRepo(
groupRepo,
this.userGroupFilterOptions,
this.translate.instant('Default'),
(model: ViewUser) => model.id !== 1
);
}
/**