Workflow multi select

This commit is contained in:
Adina Țeudan 2021-12-02 00:45:24 +02:00
parent 4a27531b8e
commit 11b82af88e
9 changed files with 173 additions and 28 deletions

View File

@ -110,3 +110,27 @@
border-radius: 50%;
position: absolute;
}
.multi-select {
min-height: 40px;
background: var(--iqser-primary);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px 0 16px;
color: var(--iqser-white);
.selected-wrapper {
display: flex;
align-items: center;
iqser-round-checkbox .wrapper.inactive {
cursor: default;
}
.all-caps-label {
margin: 0 16px 0 8px;
opacity: 1;
}
}
}

View File

@ -21,6 +21,7 @@ import { IqserButtonsModule } from '../buttons';
import { IqserHelpModeModule } from '../help-mode';
import { TableContentComponent } from './table-content/table-content.component';
import { TableItemComponent } from './table-content/table-item/table-item.component';
import { ColumnHeaderComponent } from './workflow/column-header/column-header.component';
const matModules = [MatTooltipModule];
const components = [
@ -30,6 +31,9 @@ const components = [
TableColumnNameComponent,
ScrollButtonComponent,
PageHeaderComponent,
TableContentComponent,
TableItemComponent,
ColumnHeaderComponent,
];
const modules = [
DragDropModule,
@ -47,7 +51,7 @@ const modules = [
const utils = [SyncWidthDirective];
@NgModule({
declarations: [...components, ...utils, TableContentComponent, TableItemComponent],
declarations: [...components, ...utils],
exports: [...components, ...utils],
imports: [CommonModule, ...modules, ...matModules],
})

View File

@ -92,7 +92,6 @@ export class ListingService<E extends IListable> {
}
isSelected(entity: E): boolean {
console.log('is selected');
return this.selectedIds.indexOf(entity.id) !== -1;
}

View File

@ -0,0 +1,10 @@
.heading {
margin: 0 18px 16px 18px;
display: flex;
align-items: center;
justify-content: space-between;
}
.mr-10 {
margin-right: 10px;
}

View File

@ -0,0 +1,34 @@
<ng-container *ngIf="column.entities | async as entities">
<div class="heading">
<span>{{ column.label | translate }} ({{entities.length || 0}})</span>
<span (click)="enableSelection()"
*ngIf="!activeSelection && !selectionColumn && entities.length > 1"
class="all-caps-label primary pointer"
translate="workflow.selection.select"></span>
<div *ngIf="activeSelection" class="d-flex">
<span (click)="selectAll()" class="all-caps-label primary pointer mr-10"
translate="workflow.selection.all"></span>
<span (click)="selectNone()" class="all-caps-label primary pointer"
translate="workflow.selection.none"></span>
</div>
</div>
<div *ngIf="activeSelection" class="multi-select mb-8">
<div class="selected-wrapper">
<iqser-round-checkbox
(click)="toggleSelectAll()"
[active]="allSelected$ | async"
[indeterminate]="indeterminate$ | async"
type="with-bg"
></iqser-round-checkbox>
<span [translateParams]="{ count: listingService.selectedLength$ | async }" [translate]="'workflow.selection.count'"
class="all-caps-label"></span>
</div>
<iqser-circle-button
(action)="disableSelection()"
[type]="circleButtonTypes.primary"
icon="iqser:close"
></iqser-circle-button>
</div>
</ng-container>

View File

@ -0,0 +1,77 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { filter, map, tap } from 'rxjs/operators';
import { CircleButtonTypes } from '../../../buttons';
import { IListable } from '../../models';
import { AutoUnsubscribe, Required } from '../../../utils';
import { WorkflowColumn } from '../workflow.component';
import { ListingService } from '../../services';
@Component({
selector: 'iqser-column-header',
templateUrl: './column-header.component.html',
styleUrls: ['./column-header.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ColumnHeaderComponent<T extends IListable, K extends string> extends AutoUnsubscribe implements OnInit {
readonly circleButtonTypes = CircleButtonTypes;
@Input() @Required() column!: WorkflowColumn<T, K>;
@Input() @Required() selectionColumn?: WorkflowColumn<T, K>;
@Output() @Required() readonly selectionColumnChange = new EventEmitter<WorkflowColumn<T, K> | undefined>();
allSelected$!: Observable<boolean>;
indeterminate$!: Observable<boolean>;
constructor(readonly listingService: ListingService<T>) {
super();
}
get activeSelection(): boolean {
return this.selectionColumn === this.column;
}
ngOnInit(): void {
this.allSelected$ = combineLatest([this.listingService.selectedLength$, this.column.entities]).pipe(
map(([length, columnEntities]) => length > 0 && length === columnEntities.length),
);
this.indeterminate$ = combineLatest([this.listingService.selectedLength$, this.column.entities]).pipe(
map(([length, columnEntities]) => length > 0 && length !== columnEntities.length),
);
this.addSubscription = this.column.entities
.pipe(
filter(entities => entities.length <= 1),
tap(() => this.disableSelection()),
)
.subscribe();
}
toggleSelectAll(): void {
if (this.listingService.selected.length === 0) {
this.selectAll();
} else {
this.selectNone();
}
}
enableSelection(): void {
this.selectionColumn = this.column;
this.selectionColumnChange.emit(this.selectionColumn);
}
disableSelection(): void {
if (this.activeSelection) {
this.selectionColumn = undefined;
this.selectionColumnChange.emit(this.selectionColumn);
this.selectNone();
}
}
selectAll(): void {
this.listingService.setSelected(this.column.entities.value || []);
}
selectNone(): void {
this.listingService.setSelected([]);
}
}

View File

@ -15,24 +15,26 @@
[text]="noDataText"
></iqser-empty-state>
<div *ngIf="(entitiesService.noData$ | async) === false" cdkDropListGroup class="columns-wrapper">
<div *ngIf="(entitiesService.noData$ | async) === false" [cdkDropListGroupDisabled]="!!selectionColumn" cdkDropListGroup
class="columns-wrapper">
<div *ngFor="let column of config.columns" [class.dragging]="dragging"
[class.list-can-receive]="isReceiving(column)"
[class.list-dragging]="isDragging(column)"
[class.list-source]="isSource(column)"
[style.--color]="column.color"
class="column">
<div class="heading">{{ column.label | translate }} ({{column.entities?.length || 0}})</div>
<iqser-column-header [(selectionColumn)]="this.selectionColumn" [column]="column"></iqser-column-header>
<div
(cdkDropListDropped)="move($event)"
[cdkDropListData]="column.entities"
[cdkDropListData]="column.entities | async"
[cdkDropListEnterPredicate]="canMoveTo(column)"
[id]="column.key"
cdkDropList cdkDropListSortingDisabled>
<div (cdkDragEnded)="stopDragging()" (cdkDragStarted)="startDragging(column)" *ngFor="let entity of column.entities"
[cdkDragData]="entity" [ngClass]="getItemClasses(entity)" cdkDrag
<div (cdkDragEnded)="stopDragging()" (cdkDragStarted)="startDragging(column)"
(click)="selectionColumn === column && listingService.select(entity)"
*ngFor="let entity of (column.entities | async)" [cdkDragData]="entity" [ngClass]="getItemClasses(entity)" cdkDrag
>
<div *cdkDragPlaceholder [style.--height]="itemHeight + 'px'"></div>
<div *cdkDragPlaceholder [style.min-height]="itemHeight + 'px'"></div>
<ng-container *ngTemplateOutlet="itemTemplate; context: { entity: entity, itemWidth: itemWidth }"></ng-container>
</div>
<div (click)="addElement.emit()" *ngIf="column.key === addElementColumn" class="add-btn">

View File

@ -37,10 +37,6 @@
margin-right: 8px;
}
> .heading {
margin: 0 18px 16px 18px;
}
&.dragging {
.cdk-drag {
pointer-events: none;
@ -76,6 +72,7 @@
.cdk-drag {
background-color: var(--iqser-white);
border: 2px solid var(--iqser-white);
&:last-child {
margin-bottom: 8px;
@ -85,6 +82,10 @@
background-color: var(--iqser-grey-6);
color: var(--iqser-disabled);
}
&.selected {
border-color: var(--iqser-primary);
}
}
.add-btn {
@ -116,11 +117,5 @@
.cdk-drag-placeholder {
border: 1px dashed var(--iqser-grey-5);
border-radius: 8px;
min-height: var(--height);
margin: 0 8px 4px 8px;
}
.cdk-drag-preview {
max-height: var(--height);
}

View File

@ -19,14 +19,15 @@ import { AutoUnsubscribe, Debounce, Required } from '../../utils';
import { LoadingService } from '../../loading';
import { IListable } from '../models';
import { EntitiesService, ListingService } from '../services';
import { BehaviorSubject } from 'rxjs';
interface WorkflowColumn<T, K> {
export interface WorkflowColumn<T, K> {
key: K;
label: string;
color: string;
enterFn: (entity: T) => Promise<void> | void;
enterPredicate: (entity: T) => boolean;
entities?: T[];
entities: BehaviorSubject<T[]>;
}
export interface WorkflowConfig<T, K> {
@ -60,6 +61,7 @@ export class WorkflowComponent<T extends IListable, K extends string> extends Au
itemHeight?: number;
dragging = false;
sourceColumn?: WorkflowColumn<T, K>;
selectionColumn?: WorkflowColumn<T, K>;
@ViewChildren(CdkDropList) private readonly _dropLists!: QueryList<CdkDropList>;
private _existingEntities: { [key: string]: T } = {};
@ -96,7 +98,6 @@ export class WorkflowComponent<T extends IListable, K extends string> extends Au
}
ngOnInit(): void {
this.config.columns.forEach(c => (c.entities = []));
this.addSubscription = this.listingService.displayed$.subscribe(entities => this._updateConfigItems(entities));
this._setupResizeObserver();
}
@ -182,19 +183,18 @@ export class WorkflowComponent<T extends IListable, K extends string> extends Au
if (!column) {
return;
}
if (!column.entities) {
column.entities = [];
}
this._existingEntities[entity.id] = entity;
column.entities.push(entity);
column.entities.next([...column.entities.value, entity]);
}
private _removeEntity(entity: T): void {
const existingEntity = this._existingEntities[entity.id];
const column = this._getColumnByKey(this.config.columnIdentifierFn(existingEntity));
if (column) {
const idx = (column.entities as T[]).findIndex(item => item.id === entity.id);
column.entities?.splice(idx, 1);
const entities = column.entities.value;
const idx = entities.findIndex(item => item.id === entity.id);
entities.splice(idx, 1);
column.entities.next(entities);
}
delete this._existingEntities[entity.id];
}
@ -208,7 +208,7 @@ export class WorkflowComponent<T extends IListable, K extends string> extends Au
return !this._existingEntities[entity.id];
}
private _getColumnByKey(key: K): WorkflowColumn<T, K> {
private _getColumnByKey(key: K | undefined): WorkflowColumn<T, K> {
return this.config.columns.find(col => col.key === key) as WorkflowColumn<T, K>;
}
}