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;
isActive?: boolean;
isChild?: boolean;
children?: OsFilterOption[];
}
/**
@ -54,12 +55,13 @@ export interface OsFilterIndicator {
*/
interface HierarchyModel extends BaseViewModel {
parent: BaseViewModel;
children: BaseViewModel<any>[];
}
/**
* 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)
@ -292,7 +294,16 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
return {
condition: model.id,
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 {
const filter = this.filterDefinitions.find(f => f.property === filterProperty);
if (filter) {
const filterOption = filter.options.find(
o => typeof o !== 'string' && o.condition === option.condition
@ -387,6 +399,12 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
} else {
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) {
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;
}
public get oldestParent(): ViewCategory {
if (!this.parent_id) {
return this;
} else {
return this.parent.oldestParent;
}
}
public get children(): ViewCategory[] {
return this._children || [];
}
@ -64,6 +72,17 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
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>)"
*/

View File

@ -133,7 +133,7 @@
<div class="column-state innerTable">
<!-- Category -->
<div class="ellipsis-overflow" *ngIf="motion.category">
<os-icon-container icon="device_hub">
<os-icon-container icon="category">
{{ motion.category.nameWithParentAbove }}
</os-icon-container>
</div>
@ -169,32 +169,38 @@
<ng-template #tiles>
<os-grid-layout>
<os-block-tile
*ngFor="let tileCategory of tileCategories"
(clicked)="changeToViewWithTileCategory(tileCategory)"
*ngFor="let tile of listTiles"
(clicked)="changeToViewWithTileCategory(tile)"
[orientation]="'horizontal'"
[only]="'title'"
[blockType]="'node'"
[data]="tileCategory"
title="{{ tileCategory.name | translate }}"
[data]="tile"
title="{{ tile.name | translate }}"
>
<ng-container class="block-node">
<table
matTooltip="{{ tileCategory.amountOfMotions }} {{ 'Motions' | translate }} {{
tileCategory.name | translate
}}"
matTooltip="{{ tile.amountOfMotions }} {{ 'Motions' | translate }} {{ tile.name | translate }}"
>
<tbody>
<tr>
<td>
<span
class="tile-block-title"
[matBadge]="tileCategory.amountOfMotions"
[matBadge]="tile.amountOfMotions"
[matBadgeColor]="'accent'"
[ngSwitch]="tileCategory.name"
[ngSwitch]="tile.name"
>
<span *ngSwitchCase="'Favorites'"><mat-icon>star</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>
</td>
</tr>
@ -221,7 +227,7 @@
</div>
<div *ngIf="perms.isAllowed('manage') || categories.length">
<button mat-menu-item routerLink="category">
<mat-icon>device_hub</mat-icon>
<mat-icon>category</mat-icon>
<span translate>Categories</span>
</button>
</div>
@ -305,7 +311,7 @@
*ngIf="categories.length"
(click)="multiselectWrapper(multiselectService.setCategory(selectedRows))"
>
<mat-icon>device_hub</mat-icon>
<mat-icon>category</mat-icon>
<!-- TODO: icon -->
<span translate>Set category</span>
</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 { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-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 { ColumnRestriction } from 'app/shared/components/list-view-table/list-view-table.component';
import { infoDialogSettings, largeDialogSettings } from 'app/shared/utils/dialog-settings';
@ -40,7 +41,7 @@ interface TileCategoryInformation {
filter: string;
name: string;
prefix?: string;
condition: number | boolean | null;
condition: OsFilterOptionCondition;
amountOfMotions: number;
}
@ -157,12 +158,11 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
* List of `TileCategoryInformation`.
* Necessary to not iterate over the values of the map below.
*/
public tileCategories: TileCategoryInformation[] = [];
public listTiles: TileCategoryInformation[];
/**
* Map of information about the categories relating to their id.
*/
public informationOfMotionsInTileCategories: { [id: number]: TileCategoryInformation } = {};
private motionTiles: TileCategoryInformation[] = [];
private categoryTiles: TileCategoryInformation[] = [];
/**
* The verbose name for the motions.
@ -257,34 +257,87 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
this.motionRepo.getViewModelListObservable().subscribe(motions => {
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) {
if (motion.star) {
this.countMotions(-1, true, 'star', 'Favorites');
}
if (motion.category_id) {
this.countMotions(
motion.category_id,
motion.category_id,
'category',
motion.category.name,
motion.category.prefix
);
if (!motion.category_id) {
motionsWithoutCategory++;
} 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) =>
('' + a.prefix).localeCompare(b.prefix)
);
this.addToTileInfo('Favorites', 'star', true, favoriteMotions);
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.
*