Merge pull request #4263 from MaximilianKrambach/categoryView

rework category list and sorting
This commit is contained in:
Emanuel Schütze 2019-02-12 21:57:09 +01:00 committed by GitHub
commit 4f75639780
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 367 additions and 187 deletions

View File

@ -11,7 +11,6 @@ import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { HttpService } from '../../core-services/http.service';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { Motion } from 'app/shared/models/motions/motion';
import { ViewCategory } from 'app/site/motions/models/view-category';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
@ -81,22 +80,6 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
await this.dataSend.deleteModel(category);
}
/**
* Returns all motions belonging to a category
* @param category category
*/
public getMotionsOfCategory(category: Category): Motion[] {
const motList = this.DS.getAll(Motion);
const retList: Array<Motion> = [];
motList.forEach(motion => {
if (motion.category_id && motion.category_id === category.id) {
retList.push(motion);
}
});
// TODO: Sorting the return List?!
return retList;
}
/**
* Returns the category for the ID
* @param category_id category ID

View File

@ -1,12 +1,15 @@
<div cdkDropList class="list" (cdkDropListDropped)="drop($event)">
<div cdkDropList class="os-card" (cdkDropListDropped)="drop($event)">
<div class= "box line" *ngIf="!array.length">
<span translate>No data</span>
</div>
<div class="box line" *ngFor="let item of array; let i = index" cdkDrag>
<div class="section-one" cdkDragHandle>
<mat-icon>drag_indicator</mat-icon>
</div>
<div class="section-two">
<!-- {number}. {item.toString()} -->
<span *ngIf="count">{{ i+1 }}.&nbsp;</span>
<span>{{ item }}</span>
<!-- {number}. {item.getTitle()} -->
<span *ngIf="count">{{ i + 1 }}.&nbsp;</span>
<span>{{ item.getTitle() }}</span>
</div>
<div class="section-three">
<!-- Extra controls slot using implicit template references -->

View File

@ -1,9 +1,3 @@
.list {
width: 100%;
display: block;
overflow: hidden;
}
.box {
width: 100%;
border-bottom: solid 1px #ccc;

View File

@ -5,7 +5,6 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable, Subscription } from 'rxjs';
import { Selectable } from '../selectable';
import { EmptySelectable } from '../empty-selectable';
/**
* Reusable Sorting List
@ -133,8 +132,8 @@ export class SortingListComponent implements OnInit, OnDestroy {
if (this.array.length !== newValues.length || this.live) {
this.array = [];
this.array = newValues.map(val => val);
} else if (this.array.length === 0) {
this.array.push(new EmptySelectable(this.translate));
} else {
this.array = this.array.map(arrayValue => newValues.find(val => val.id === arrayValue.id));
}
}

View File

@ -4,27 +4,28 @@
<h2 translate>Categories</h2>
</div>
</os-head-bar>
<div class="custom-table-header"></div>
<!-- Creating a new category -->
<mat-card *ngIf="categoryToCreate">
<div class="spacer-top-20"></div>
<mat-card class="os-card" *ngIf="categoryToCreate">
<mat-card-title translate>Create new category</mat-card-title>
<mat-card-content>
<form [formGroup]="createForm" (keydown)="keyDownFunction($event)">
<p>
<!-- Prefix field -->
<mat-form-field>
<input formControlName="prefix" matInput placeholder="{{'Prefix' | translate}}">
<form
class="full-width-form flex-spaced"
id="createForm"
[formGroup]="createForm"
(keydown)="keyDownFunction($event)"
>
<!-- prefix input -->
<mat-form-field class="short-input">
<input formControlName="prefix" matInput placeholder="{{ 'Prefix' | translate }}" />
</mat-form-field>
<!-- Name field -->
<mat-form-field>
<input formControlName="name" matInput placeholder="{{'Name' | translate}}" required>
<mat-hint *ngIf="!createForm.controls.name.valid">
<!-- name input -->
<mat-form-field class="long-input">
<input formControlName="name" matInput placeholder="{{ 'Name' | translate }}" required />
<mat-hint *ngIf="!updateForm.controls.name.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
</form>
</mat-card-content>
@ -39,28 +40,22 @@
</mat-card-actions>
</mat-card>
<mat-accordion class="os-form-card">
<mat-expansion-panel *ngFor="let category of categories" (opened)="openId = category.id" (closed)="panelClosed(category)"
[expanded]="openId === category.id" multiple="false">
<mat-card class="os-card">
<mat-accordion displayMode="flat">
<ng-container *ngFor="let category of categories">
<mat-expansion-panel
class="os-card-expandion-panel"
(opened)="setValues(category)"
[expanded]="editId === category.id"
(closed)="onCancelButton()"
>
<!-- Header shows Prefix and name -->
<mat-expansion-panel-header>
<mat-panel-title>
<div class="header-container">
<div class="header-prefix">
<div *ngIf="editId !== category.id">
{{ category.prefix }}
</div>
<div *ngIf="editId === category.id">
{{ updateForm.get('prefix').value }}
</div>
</div>
<div class="header-name">
<div *ngIf="editId !== category.id">
{{ category.name }}
</div>
<div *ngIf="editId === category.id">
{{ updateForm.get('name').value }}
<div>
{{ category.prefixedName }}
</div>
</div>
<div class="header-size os-amount-chip">
@ -71,48 +66,68 @@
</mat-expansion-panel-header>
<!-- Edit form shows during the edit event -->
<form id="updateForm" [formGroup]='updateForm' *ngIf="editId === category.id" (keydown)="keyDownFunction($event, category)">
<span translate>Edit category</span>:<br>
<mat-form-field>
<input formControlName="prefix" matInput placeholder="{{'Prefix' | translate}}">
<div class="full-width-form">
<form
class="full-width-form"
id="updateForm"
[formGroup]="updateForm"
*ngIf="editId === category.id"
(keydown)="keyDownFunction($event, category)"
>
<div class="flex-spaced">
<mat-form-field class="short-input">
<input formControlName="prefix" matInput placeholder="{{ 'Prefix' | translate }}" />
</mat-form-field>
<mat-form-field>
<input formControlName="name" matInput placeholder="{{'Name' | translate}}" required>
<mat-form-field class="long-input">
<input
formControlName="name"
matInput
placeholder="{{ 'Name' | translate }}"
required
/>
<mat-hint *ngIf="!updateForm.controls.name.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</div>
<div class="inline-form-submit" *osPerms="'motions.can_manage'">
<button
[disabled]="!updateForm.dirty"
mat-button
class="on-transition-fade"
(click)="onSaveButton(category)"
>
<span translate>Save</span>
</button>
<button
type="button"
mat-button
class="on-transition-fade"
[routerLink]="getSortUrl(category)"
>
<span translate>Sort motions</span>
</button>
<button
type="button"
mat-button
class="on-transition-fade"
(click)="onDeleteButton(category)"
>
<span translate>Delete</span>
</button>
</div>
</form>
</div>
<!-- Show and sort corresponding motions-->
<div *ngIf="motionsInCategory(category).length > 0">
<span translate>Motions</span>:
<div *ngIf="editId !== category.id">
<div>
<ul *ngFor="let motion of motionsInCategory(category)">
<li>{{ motion }}</li>
<li class="ellipsis-overflow">{{ motion.getListTitle() }}</li>
</ul>
</div>
<div *ngIf="editId === category.id" class="half-width">
<os-sorting-list [input]="motionsInCategory(category)" #sorter></os-sorting-list>
</div>
</div>
<!-- Buttons to edit, delete, save ... -->
<mat-action-row>
<button mat-icon-button *ngIf="editId !== category.id" class='on-transition-fade' (click)=onEditButton(category)>
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button *ngIf="editId === category.id" class='on-transition-fade' (click)=onCancelButton()>
<mat-icon>close</mat-icon>
</button>
<button mat-icon-button *ngIf="editId === category.id" class='on-transition-fade' (click)=onSaveButton(category)>
<mat-icon>save</mat-icon>
</button>
<button mat-icon-button mat-button class='on-transition-fade' (click)=onDeleteButton(category)>
<mat-icon>delete</mat-icon>
</button>
</mat-action-row>
</mat-expansion-panel>
</mat-accordion>
</ng-container>
</mat-accordion>
</mat-card>

View File

@ -1,33 +1,50 @@
.header-container {
display: grid;
grid-template-rows: auto;
grid-template-columns: 33.333% 33.333% 33.333%;
grid-template-columns: 75% 25%;
width: 100%;
> div {
grid-row-start: 1;
grid-row-end: span 1;
grid-column-end: span 3;
}
.header-prefix {
grid-column-start: 1;
grid-column-end: span 2;
}
.header-name {
grid-column-start: 2;
grid-column-start: 1;
color: lightslategray;
}
.header-size {
grid-column-start: 3;
grid-column-start: 2;
}
}
#updateForm {
margin-bottom: 20px;
mat-expansion-panel {
max-width: 770px;
margin: auto;
}
.half-width {
width: 50%;
.flex-spaced {
display: flex;
justify-content: space-between;
}
.full-width-form {
display: flex;
width: 100%;
align-content: space-between;
flex: 2;
}
.short-input {
width: 20%;
}
.long-input {
width: 75%;
}
.inline-form-submit {
justify-content: end;
display: block;
flex: 1;
}

View File

@ -1,17 +1,17 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from '../../../base/base-view';
import { Category } from 'app/shared/models/motions/category';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { ViewCategory } from '../../models/view-category';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Motion } from 'app/shared/models/motions/motion';
import { SortingListComponent } from 'app/shared/components/sorting-list/sorting-list.component';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { BaseViewComponent } from '../../../base/base-view';
import { MatSnackBar } from '@angular/material';
import { ViewCategory } from '../../models/view-category';
import { ViewMotion } from '../../models/view-motion';
/**
* List view for the categories.
@ -28,15 +28,10 @@ export class CategoryListComponent extends BaseViewComponent implements OnInit {
public categoryToCreate: Category | null;
/**
* Determine which category to edit
* Determine which category is opened
*/
public editId: number | null;
/**
* Determine which category is opened.
*/
public openId: number | null;
/**
* Source of the data
*/
@ -52,12 +47,6 @@ export class CategoryListComponent extends BaseViewComponent implements OnInit {
*/
public updateForm: FormGroup;
/**
* The MultiSelect Component
*/
@ViewChild('sorter')
public sortSelector: SortingListComponent;
/**
* The usual component constructor
* @param titleService
@ -72,6 +61,7 @@ export class CategoryListComponent extends BaseViewComponent implements OnInit {
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: CategoryRepositoryService,
private motionRepo: MotionRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService
) {
@ -89,7 +79,8 @@ export class CategoryListComponent extends BaseViewComponent implements OnInit {
}
/**
* Event on key-down in form
* Event on key-down in form. Submits the current form if the 'enter' button is pressed
*
* @param event
* @param viewCategory
*/
@ -154,47 +145,33 @@ export class CategoryListComponent extends BaseViewComponent implements OnInit {
}
/**
* Saves the category
*
* TODO: Do not number the motions. This needs to be a separate button (maybe with propting for confirmation), because
* not every body uses this and this would destroy their own order in motion identifiers.
* See issue #3969
* Saves a category
* TODO: Some feedback
*
* @param viewCategory
*/
public async onSaveButton(viewCategory: ViewCategory): Promise<void> {
// get the sorted motions. Save them before updating the category.
let sortedMotionIds;
if (this.sortSelector) {
sortedMotionIds = this.sortSelector.array.map(selectable => selectable.id);
this.repo.numberMotionsInCategory(viewCategory.category, sortedMotionIds);
}
if (this.updateForm.valid) {
if (this.updateForm.dirty && this.updateForm.valid) {
const cat: Partial<Category> = { name: this.updateForm.get('name').value };
if (this.updateForm.get('prefix').value) {
cat.prefix = this.updateForm.get('prefix').value;
}
// wait for the category to update; then the (maybe) changed prefix can be applied to the motions
await this.repo.update(cat, viewCategory);
this.onCancelButton();
if (this.sortSelector) {
this.repo.numberMotionsInCategory(viewCategory.category, sortedMotionIds);
}
this.updateForm.markAsPristine();
}
}
/**
* executed on cancel button
* @param viewCategory
* Trigger after cancelling an edit. The updateForm is reset to an original
* value, which might belong to a different category
*/
public onCancelButton(): void {
this.editId = null;
this.updateForm.markAsPristine();
}
/**
* is executed, when the delete button is pressed
*
* @param viewCategory The category to delete
*/
public async onDeleteButton(viewCategory: ViewCategory): Promise<void> {
@ -206,23 +183,36 @@ export class CategoryListComponent extends BaseViewComponent implements OnInit {
/**
* Returns the motions corresponding to a category
*
* @param category target
* @returns all motions in the category
*/
public motionsInCategory(category: Category): Motion[] {
const motions = this.repo.getMotionsOfCategory(category);
motions.sort((motion1, motion2) => (motion1 > motion2 ? 1 : -1));
return motions;
public motionsInCategory(category: Category): ViewMotion[] {
return this.motionRepo
.getViewModelList()
.filter(m => m.category_id === category.id)
.sort((motion1, motion2) => motion1.identifier.localeCompare(motion2.identifier));
}
/**
* Is executed when a mat-extension-panel is closed
* @param viewCategory the category in the panel
* Fetch the correct URL for a detail sort view
*
* @param viewCategory
*/
public panelClosed(viewCategory: ViewCategory): void {
this.openId = null;
if (this.editId) {
this.onSaveButton(viewCategory);
public getSortUrl(viewCategory: ViewCategory): string {
return `/motions/category/${viewCategory.id}`;
}
/**
* Set/reset the initial values and the referenced category of the update form
*
* @param category
*/
public setValues(category: ViewCategory): void {
this.editId = category.id;
this.updateForm.setValue({
prefix: category.prefix,
name: category.name
});
}
}

View File

@ -0,0 +1,24 @@
<!-- TODO permission -->
<os-head-bar [nav]="false">
<!-- Title -->
<div class="title-slot"><h2 translate>Sort motions</h2></div>
</os-head-bar>
<mat-card class="os-form-card">
<h3>{{ categoryName }}</h3>
<br />
<span translate>
Drag and drop motions to reorder the category. Then click the button to renumber.
</span>
<br />
<button
mat-raised-button
color="primary"
(click)="onNumberMotions()"
class="spacer-top-10"
[disabled]="!motionsCount"
>
<span translate>Number motions</span>
</button>
<os-sorting-list [input]="motionObservable" #sorter></os-sorting-list>
</mat-card>

View File

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

View File

@ -0,0 +1,119 @@
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { Component, OnInit, ViewChild } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { MatSnackBar } from '@angular/material';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { SortingListComponent } from 'app/shared/components/sorting-list/sorting-list.component';
import { ViewCategory } from '../../models/view-category';
import { ViewMotion } from '../../models/view-motion';
/**
* View for rearranging and renumbering the motions of a category. The {@link onNumberMotions}
* method sends a request to the server to re-number the given motions in the order
* as displayed in this view
*/
@Component({
selector: 'os-category-sort',
templateUrl: './category-sort.component.html',
styleUrls: ['./category-sort.component.scss']
})
export class CategorySortComponent extends BaseViewComponent implements OnInit {
/**
* The current category. Determined by the route
*/
public category: ViewCategory;
/**
* A behaviorSubject emitting the currently asigned motions on change
*/
public motionsSubject = new BehaviorSubject<ViewMotion[]>([]);
/**
* Counter indicating the amount of motions currently in the category
*/
public motionsCount = 0;
/**
* @returns an observable for the {@link motionsSubject}
*/
public get motionObservable(): Observable<ViewMotion[]> {
return this.motionsSubject.asObservable();
}
/**
* @returns the name and (if present) prefix of the category
*/
public get categoryName(): string {
if (!this.category) {
return '';
}
return this.category.prefix ? `${this.category.name} (${this.category.prefix})` : this.category.name;
}
/**
* The Sort Component
*/
@ViewChild('sorter')
public sortSelector: SortingListComponent;
/**
* Constructor. Calls parents
*
* @param title
* @param translate
* @param matSnackBar
* @param promptService
* @param repo
* @param route
* @param motionRepo
*/
public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private promptService: PromptService,
private repo: CategoryRepositoryService,
private route: ActivatedRoute,
private motionRepo: MotionRepositoryService
) {
super(title, translate, matSnackBar);
}
/**
* Subscribes to the category and motions of this category.
*/
public ngOnInit(): void {
const category_id: number = +this.route.snapshot.params.id;
this.repo.getViewModelObservable(category_id).subscribe(cat => {
this.category = cat;
});
this.motionRepo.getViewModelListObservable().subscribe(motions => {
const filtered = motions.filter(m => m.category_id === category_id);
this.motionsCount = filtered.length;
this.motionsSubject.next(filtered);
});
}
/**
* Triggers a (re-)numbering of the motions after a configmarion dialog
*
* @param category
*/
public async onNumberMotions(): Promise<void> {
if (this.sortSelector) {
const content = this.translate.instant('This will change the identifier for the motions of this category.');
if (await this.promptService.open('Are you sure?', content)) {
const sortedMotionIds = this.sortSelector.array.map(selectable => selectable.id);
await this.repo
.numberMotionsInCategory(this.category.category, sortedMotionIds)
.then(null, this.raiseError);
}
}
}
}

View File

@ -74,7 +74,7 @@
</div>
<!-- submitters line -->
<div class="motion-list">
<span class="motion-list-from" *ngIf="motion.submitters.length">
<span class="motion-list-from ellipsis-overflow" *ngIf="motion.submitters.length">
<span translate>by</span> {{ motion.submitters }}
</span>
</div>

View File

@ -4,6 +4,7 @@ import { Routes, RouterModule } from '@angular/router';
import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.component';
import { CallListComponent } from './components/call-list/call-list.component';
import { CategoryListComponent } from './components/category-list/category-list.component';
import { CategorySortComponent } from './components/category-sort/category-sort.component';
import { MotionBlockListComponent } from './components/motion-block-list/motion-block-list.component';
import { MotionBlockDetailComponent } from './components/motion-block-detail/motion-block-detail.component';
import { MotionCommentSectionListComponent } from './components/motion-comment-section-list/motion-comment-section-list.component';
@ -19,6 +20,7 @@ import { WorkflowDetailComponent } from './components/workflow-detail/workflow-d
const routes: Routes = [
{ path: '', component: MotionListComponent },
{ path: 'category', component: CategoryListComponent },
{ path: 'category/:id', component: CategorySortComponent },
{ path: 'comment-section', component: MotionCommentSectionListComponent },
{ path: 'statute-paragraphs', component: StatuteParagraphListComponent },
{ path: 'statute-paragraphs/import', component: StatuteImportListComponent },

View File

@ -25,6 +25,7 @@ import { MotionExportDialogComponent } from './components/motion-export-dialog/m
import { StatuteImportListComponent } from './components/statute-paragraph-list/statute-import-list/statute-import-list.component';
import { WorkflowListComponent } from './components/workflow-list/workflow-list.component';
import { WorkflowDetailComponent } from './components/workflow-detail/workflow-detail.component';
import { CategorySortComponent } from './components/category-sort/category-sort.component';
@NgModule({
imports: [CommonModule, MotionsRoutingModule, SharedModule],
@ -50,7 +51,8 @@ import { WorkflowDetailComponent } from './components/workflow-detail/workflow-d
MotionExportDialogComponent,
StatuteImportListComponent,
WorkflowListComponent,
WorkflowDetailComponent
WorkflowDetailComponent,
CategorySortComponent
],
entryComponents: [
MotionChangeRecommendationComponent,

View File

@ -640,3 +640,9 @@ button.mat-menu-item.selected {
margin: 10px 0;
}
}
.ellipsis-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}