Add Smarter motion tiles and filters

Removes motion tiles for subcategories
Sort motion-special tiles always behind category tiles
Add new logic to build tiles
Filter subcateories with their parent
This commit is contained in:
Sean Engelhardt 2019-08-13 13:54:29 +02:00
parent 3ccf5e9bea
commit a39d9b5843
4 changed files with 142 additions and 71 deletions

View File

@ -38,6 +38,7 @@ export interface OsFilterOption {
condition: OsFilterOptionCondition; condition: OsFilterOptionCondition;
isActive?: boolean; isActive?: boolean;
isChild?: boolean; isChild?: boolean;
children?: OsFilterOption[];
} }
/** /**
@ -54,12 +55,13 @@ export interface OsFilterIndicator {
*/ */
interface HierarchyModel extends BaseViewModel { interface HierarchyModel extends BaseViewModel {
parent: BaseViewModel; parent: BaseViewModel;
children: BaseViewModel<any>[];
} }
/** /**
* Define the type of a filter condition * Define the type of a filter condition
*/ */
type OsFilterOptionCondition = string | boolean | number | number[]; export type OsFilterOptionCondition = string | boolean | number | number[];
/** /**
* Filter for the list view. List views can subscribe to its' dataService (providing filter definitions) * Filter for the list view. List views can subscribe to its' dataService (providing filter definitions)
@ -292,7 +294,16 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
return { return {
condition: model.id, condition: model.id,
label: model.getTitle(), label: model.getTitle(),
isChild: !!model.parent isChild: !!model.parent,
children:
model.children && model.children.length
? model.children.map(child => {
return {
label: child.getTitle(),
condition: child.id
};
})
: undefined
}; };
}); });
@ -373,6 +384,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
*/ */
protected addFilterOption(filterProperty: string, option: OsFilterOption): void { protected addFilterOption(filterProperty: string, option: OsFilterOption): void {
const filter = this.filterDefinitions.find(f => f.property === filterProperty); const filter = this.filterDefinitions.find(f => f.property === filterProperty);
if (filter) { if (filter) {
const filterOption = filter.options.find( const filterOption = filter.options.find(
o => typeof o !== 'string' && o.condition === option.condition o => typeof o !== 'string' && o.condition === option.condition
@ -387,6 +399,12 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
} else { } else {
filter.count += 1; filter.count += 1;
} }
if (filterOption.children && filterOption.children.length) {
for (const child of filterOption.children) {
this.addFilterOption(filterProperty, child);
}
}
} }
} }
} }
@ -420,6 +438,12 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
if (filter.count) { if (filter.count) {
filter.count -= 1; filter.count -= 1;
} }
if (filterOption.children && filterOption.children.length) {
for (const child of filterOption.children) {
this.removeFilterOption(filterProperty, child);
}
}
} }
} }
} }

View File

@ -32,6 +32,14 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
return this._parent; return this._parent;
} }
public get oldestParent(): ViewCategory {
if (!this.parent_id) {
return this;
} else {
return this.parent.oldestParent;
}
}
public get children(): ViewCategory[] { public get children(): ViewCategory[] {
return this._children || []; return this._children || [];
} }
@ -64,6 +72,17 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
return this.prefix ? this.prefix + ' - ' + this.name : this.name; return this.prefix ? this.prefix + ' - ' + this.name : this.name;
} }
/**
* The amount of all motions on this category and all children
*/
public get totalAmountOfMotions(): number {
let totalAmount = this.motions.length;
for (const child of this.children) {
totalAmount += child.totalAmountOfMotions;
}
return totalAmount;
}
/** /**
* @returns the name with all parents in brackets: "<Cat> (<CatParent>, <CatParentParent>)" * @returns the name with all parents in brackets: "<Cat> (<CatParent>, <CatParentParent>)"
*/ */

View File

@ -133,7 +133,7 @@
<div class="column-state innerTable"> <div class="column-state innerTable">
<!-- Category --> <!-- Category -->
<div class="ellipsis-overflow" *ngIf="motion.category"> <div class="ellipsis-overflow" *ngIf="motion.category">
<os-icon-container icon="device_hub"> <os-icon-container icon="category">
{{ motion.category.nameWithParentAbove }} {{ motion.category.nameWithParentAbove }}
</os-icon-container> </os-icon-container>
</div> </div>
@ -169,32 +169,38 @@
<ng-template #tiles> <ng-template #tiles>
<os-grid-layout> <os-grid-layout>
<os-block-tile <os-block-tile
*ngFor="let tileCategory of tileCategories" *ngFor="let tile of listTiles"
(clicked)="changeToViewWithTileCategory(tileCategory)" (clicked)="changeToViewWithTileCategory(tile)"
[orientation]="'horizontal'" [orientation]="'horizontal'"
[only]="'title'" [only]="'title'"
[blockType]="'node'" [blockType]="'node'"
[data]="tileCategory" [data]="tile"
title="{{ tileCategory.name | translate }}" title="{{ tile.name | translate }}"
> >
<ng-container class="block-node"> <ng-container class="block-node">
<table <table
matTooltip="{{ tileCategory.amountOfMotions }} {{ 'Motions' | translate }} {{ matTooltip="{{ tile.amountOfMotions }} {{ 'Motions' | translate }} {{ tile.name | translate }}"
tileCategory.name | translate
}}"
> >
<tbody> <tbody>
<tr> <tr>
<td> <td>
<span <span
class="tile-block-title" class="tile-block-title"
[matBadge]="tileCategory.amountOfMotions" [matBadge]="tile.amountOfMotions"
[matBadgeColor]="'accent'" [matBadgeColor]="'accent'"
[ngSwitch]="tileCategory.name" [ngSwitch]="tile.name"
> >
<span *ngSwitchCase="'Favorites'"><mat-icon>star</mat-icon></span> <span *ngSwitchCase="'Favorites'"><mat-icon>star</mat-icon></span>
<span *ngSwitchCase="'No category'"><mat-icon>block</mat-icon></span> <span *ngSwitchCase="'No category'"><mat-icon>block</mat-icon></span>
<span *ngSwitchDefault>{{ tileCategory.prefix }}</span> <span *ngSwitchCase="'Personal Note'"><mat-icon>speaker_notes</mat-icon></span>
<span *ngSwitchDefault>
<span *ngIf="tile.prefix">
{{ tile.prefix }}
</span>
<span *ngIf="!tile.prefix">
<mat-icon>category</mat-icon>
</span>
</span>
</span> </span>
</td> </td>
</tr> </tr>
@ -221,7 +227,7 @@
</div> </div>
<div *ngIf="perms.isAllowed('manage') || categories.length"> <div *ngIf="perms.isAllowed('manage') || categories.length">
<button mat-menu-item routerLink="category"> <button mat-menu-item routerLink="category">
<mat-icon>device_hub</mat-icon> <mat-icon>category</mat-icon>
<span translate>Categories</span> <span translate>Categories</span>
</button> </button>
</div> </div>
@ -305,7 +311,7 @@
*ngIf="categories.length" *ngIf="categories.length"
(click)="multiselectWrapper(multiselectService.setCategory(selectedRows))" (click)="multiselectWrapper(multiselectService.setCategory(selectedRows))"
> >
<mat-icon>device_hub</mat-icon> <mat-icon>category</mat-icon>
<!-- TODO: icon --> <!-- TODO: icon -->
<span translate>Set category</span> <span translate>Set category</span>
</button> </button>

View File

@ -14,6 +14,7 @@ import { MotionBlockRepositoryService } from 'app/core/repositories/motions/moti
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service'; import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service'; import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
import { OsFilterOptionCondition } from 'app/core/ui-services/base-filter-list.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { ColumnRestriction } from 'app/shared/components/list-view-table/list-view-table.component'; import { ColumnRestriction } from 'app/shared/components/list-view-table/list-view-table.component';
import { infoDialogSettings, largeDialogSettings } from 'app/shared/utils/dialog-settings'; import { infoDialogSettings, largeDialogSettings } from 'app/shared/utils/dialog-settings';
@ -40,7 +41,7 @@ interface TileCategoryInformation {
filter: string; filter: string;
name: string; name: string;
prefix?: string; prefix?: string;
condition: number | boolean | null; condition: OsFilterOptionCondition;
amountOfMotions: number; amountOfMotions: number;
} }
@ -157,12 +158,11 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
* List of `TileCategoryInformation`. * List of `TileCategoryInformation`.
* Necessary to not iterate over the values of the map below. * Necessary to not iterate over the values of the map below.
*/ */
public tileCategories: TileCategoryInformation[] = []; public listTiles: TileCategoryInformation[];
/** private motionTiles: TileCategoryInformation[] = [];
* Map of information about the categories relating to their id.
*/ private categoryTiles: TileCategoryInformation[] = [];
public informationOfMotionsInTileCategories: { [id: number]: TileCategoryInformation } = {};
/** /**
* The verbose name for the motions. * The verbose name for the motions.
@ -257,34 +257,87 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
this.motionRepo.getViewModelListObservable().subscribe(motions => { this.motionRepo.getViewModelListObservable().subscribe(motions => {
if (motions && motions.length) { if (motions && motions.length) {
this.createTiles(motions); this.createMotionTiles(motions);
} }
}); });
} }
private createTiles(motions: ViewMotion[]): void { /**
this.informationOfMotionsInTileCategories = {}; * Publishes the tileList
*/
private createTileList(): void {
this.listTiles = this.categoryTiles.concat(this.motionTiles);
}
/**
* Creates the tiles for categories.
* Filters thous without parent, sorts them by theit weight, maps them to TileInfo and publishes
* the result
*/
private createCategoryTiles(categories: ViewCategory[]): void {
this.categoryTiles = categories
.filter(category => !category.parent_id && !!category.totalAmountOfMotions)
.sort((a, b) => a.weight - b.weight)
.map(category => {
return {
filter: 'category',
name: category.name,
condition: category.id,
amountOfMotions: category.totalAmountOfMotions,
prefix: category.prefix
};
});
}
/**
* Creates the tiles for motions
* @param motions
*/
private createMotionTiles(motions: ViewMotion[]): void {
this.motionTiles = [];
let favoriteMotions = 0;
let motionsWithNotes = 0;
let motionsWithoutCategory = 0;
const localCategories = new Set<ViewCategory>();
for (const motion of motions) { for (const motion of motions) {
if (motion.star) { if (!motion.category_id) {
this.countMotions(-1, true, 'star', 'Favorites'); motionsWithoutCategory++;
}
if (motion.category_id) {
this.countMotions(
motion.category_id,
motion.category_id,
'category',
motion.category.name,
motion.category.prefix
);
} else { } else {
this.countMotions(-2, null, 'category', 'No category'); localCategories.add(motion.category.oldestParent);
} }
favoriteMotions += +this.motionHasProp(motion, 'star');
motionsWithNotes += +this.motionHasProp(motion, 'hasNotes');
} }
this.tileCategories = Object.values(this.informationOfMotionsInTileCategories).sort((a, b) => this.addToTileInfo('Favorites', 'star', true, favoriteMotions);
('' + a.prefix).localeCompare(b.prefix) this.addToTileInfo('Personal Note', 'hasNote', true, motionsWithNotes);
); this.addToTileInfo('No category', 'category', null, motionsWithoutCategory);
this.createCategoryTiles(Array.from(localCategories));
this.createTileList();
}
/**
* @returns true if the motion has the given prop
*/
private motionHasProp(motion: ViewMotion, property: string, positive: boolean = true): boolean {
return !!motion[property] === positive ? true : false;
}
/**
* Helper function to add new tile information to the tileCategories-List
*/
private addToTileInfo(name: string, filter: string, condition: OsFilterOptionCondition, amount: number): void {
if (amount) {
this.motionTiles.push({
filter: filter,
name: name,
condition: condition,
amountOfMotions: amount
});
}
} }
/** /**
@ -338,37 +391,6 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
}); });
} }
/**
* Function to count the motions in their related categories.
*
* @param id The key of TileCategory in `informationOfMotionsInTileCategories` object
* @param condition The condition, if the tile is selected
* @param filter The filter, if the tile is selected
* @param name The title of the tile
* @param prefix The prefix of the category
*/
private countMotions(
id: number,
condition: number | boolean | null,
filter: string,
name: string,
prefix?: string
): void {
let info = this.informationOfMotionsInTileCategories[id];
if (info) {
++info.amountOfMotions;
} else {
info = {
filter,
name,
condition,
prefix,
amountOfMotions: 1
};
}
this.informationOfMotionsInTileCategories[id] = info;
}
/** /**
* Wraps multiselect actions to close the multiselect mode or throw an error if one happens. * Wraps multiselect actions to close the multiselect mode or throw an error if one happens.
* *