Merge pull request #4913 from tsiegleauq/smarter-motion-tiles
Add Smarter motion tiles and filters
This commit is contained in:
commit
8555516a53
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>)"
|
||||||
*/
|
*/
|
||||||
|
@ -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>
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
Loading…
Reference in New Issue
Block a user