Workflow multi select
This commit is contained in:
parent
4a27531b8e
commit
11b82af88e
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
.heading {
|
||||
margin: 0 18px 16px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@ -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>
|
||||
@ -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([]);
|
||||
}
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user