Workflow initial

This commit is contained in:
Adina Țeudan 2021-09-21 15:43:24 +03:00
parent 6c0f123bd9
commit cb8393c492
18 changed files with 308 additions and 28 deletions

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg id="Layer_1" style="enable-background:new 0 0 14 14;" version="1.1" viewBox="0 0 14 14" x="0px"
xml:space="preserve" xmlns="http://www.w3.org/2000/svg" y="0px">
<style type="text/css">
.st0 {
fill: currentColor;
}
</style>
<g id="Styleguide">
<g id="Export-just-icons" transform="translate(-979.000000, -294.000000)">
<g id="Group" transform="translate(979.000000, 294.000000)">
<g id="lanes-view">
<path class="st0" d="M0,0v14h14V0H0z M8.4,1.4v11.2H5.6V1.4H8.4z M1.4,1.4h2.8v11.2H1.4V1.4z M12.6,12.6H9.8V1.4h2.8V12.6z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 726 B

20
src/assets/icons/list.svg Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg id="Layer_1" style="enable-background:new 0 0 14 14;" version="1.1" viewBox="0 0 14 14" x="0px"
xml:space="preserve" xmlns="http://www.w3.org/2000/svg" y="0px">
<style type="text/css">
.st0 {
fill: currentColor;
}
</style>
<g id="Styleguide">
<g id="Export-just-icons" transform="translate(-979.000000, -252.000000)">
<g id="Group" transform="translate(979.000000, 252.000000)">
<g id="list-view">
<path class="st0" d="M0,0h14v14H0V0z M2.8,12.6V9.8H1.4v2.8H2.8z M2.8,5.6H1.4v2.8h1.4V5.6z M4.2,8.4h8.4V5.6H4.2V8.4z M1.4,4.2
h1.4V1.4H1.4V4.2z M4.2,1.4v2.8h8.4V1.4H4.2z M12.6,9.8H4.2v2.8h8.4V9.8z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 806 B

View File

@ -15,9 +15,11 @@
@mixin no-scroll-bar {
scrollbar-width: none; /* Firefox */
scrollbar-height: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
&::-webkit-scrollbar {
width: 0;
height: 0;
background: transparent; /* Chrome/Safari/Webkit */
}
}

View File

@ -115,13 +115,6 @@ pre {
@include line-clamp(2);
}
.text-overflow {
display: block !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.no-wrap {
white-space: nowrap;
}

View File

@ -18,6 +18,8 @@ export class IqserIconsModule {
'edit',
'failure',
'help-outline',
'lanes',
'list',
'refresh',
'search',
'sort-asc',

View File

@ -6,6 +6,8 @@ export * from './table/table.component';
export * from './table-column-name/table-column-name.component';
export * from './table-header/table-header.component';
export * from './workflow/workflow.component';
export * from './sync-width.directive';
export * from './listing.module';

View File

@ -1,12 +1,12 @@
import { Directive, Injector, OnDestroy } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { FilterService } from '../filtering';
import { SortingOrders, SortingService } from '../sorting';
import { AutoUnsubscribe, Bind, KeysOf } from '../utils';
import { SearchService } from '../search';
import { EntitiesService } from './services';
import { Listable, TableColumnConfig } from './models';
import { Listable, ListingMode, ListingModes, TableColumnConfig } from './models';
export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const;
@ -20,6 +20,7 @@ export abstract class ListingComponent<T extends Listable> extends AutoUnsubscri
readonly noMatch$ = this._noMatch$;
readonly noContent$ = this._noContent$;
readonly sortedDisplayedEntities$ = this._sortedDisplayedEntities$;
readonly listingMode$: Observable<ListingMode>;
readonly routerLinkFn?: (entity: T) => string | string[];
// TODO: These should be somewhere in table listing, not generic listing
@ -32,9 +33,11 @@ export abstract class ListingComponent<T extends Listable> extends AutoUnsubscri
* @protected
*/
protected abstract readonly _primaryKey: KeysOf<T>;
private readonly _listingMode$ = new BehaviorSubject<ListingMode>(ListingModes.table);
protected constructor(protected readonly _injector: Injector) {
super();
this.listingMode$ = this._listingMode$.asObservable();
setTimeout(() => this.setInitialConfig());
}
@ -42,6 +45,10 @@ export abstract class ListingComponent<T extends Listable> extends AutoUnsubscri
return this.entitiesService.all;
}
set listingMode(listingMode: ListingMode) {
this._listingMode$.next(listingMode);
}
private get _sortedDisplayedEntities$(): Observable<readonly T[]> {
const sort = (entities: T[]) => this.sortingService.defaultSort(entities);
const sortedEntities = () => this.entitiesService.displayed$.pipe(map(sort));

View File

@ -14,10 +14,13 @@ import { IqserIconsModule } from '../icons';
import { IqserScrollbarModule } from '../scrollbar';
import { RouterModule } from '@angular/router';
import { IqserEmptyStatesModule } from '../empty-states';
import { WorkflowComponent } from './workflow/workflow.component';
import { DragDropModule } from '@angular/cdk/drag-drop';
const matModules = [MatTooltipModule];
const components = [TableHeaderComponent, TableComponent, TableColumnNameComponent, ScrollButtonComponent];
const components = [TableHeaderComponent, TableComponent, WorkflowComponent, TableColumnNameComponent, ScrollButtonComponent];
const modules = [
DragDropModule,
TranslateModule,
IqserFiltersModule,
IqserInputsModule,

View File

@ -1,2 +1,3 @@
export * from './listable';
export * from './table-column-config.model';
export * from './listing-modes';

View File

@ -0,0 +1,6 @@
export const ListingModes = {
table: 'table',
workflow: 'workflow'
} as const;
export type ListingMode = keyof typeof ListingModes;

View File

@ -19,6 +19,7 @@
</div>
<div
*ngIf="listingMode === listingModes.table"
[class.no-data]="entitiesService.noData$ | async"
[class.selection-enabled]="selectionEnabled"
class="table-header"

View File

@ -2,14 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular
import { Required } from '../../utils';
import { FilterService } from '../../filtering';
import { EntitiesService } from '../services';
import { Listable, TableColumnConfig } from '../models';
export const ListingModes = {
list: 'list',
workflow: 'workflow'
} as const;
export type ListingMode = keyof typeof ListingModes;
import { Listable, ListingMode, ListingModes, TableColumnConfig } from '../models';
@Component({
selector: 'iqser-table-header',
@ -18,11 +11,13 @@ export type ListingMode = keyof typeof ListingModes;
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableHeaderComponent<T extends Listable> {
readonly listingModes = ListingModes;
@Input() @Required() tableHeaderLabel!: string;
@Input() @Required() tableColumnConfigs!: readonly TableColumnConfig<T>[];
@Input() hasEmptyColumn = false;
@Input() selectionEnabled = false;
@Input() mode: ListingMode = ListingModes.list;
@Input() listingMode: ListingMode = ListingModes.table;
@Input() totalSize?: number;
@Input() bulkActions?: TemplateRef<unknown>;

View File

@ -1,6 +1,7 @@
<iqser-table-header
[bulkActions]="bulkActions"
[hasEmptyColumn]="!!emptyColumnWidth"
[listingMode]="listingModes.table"
[selectionEnabled]="selectionEnabled"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"

View File

@ -69,7 +69,6 @@
}
}
.table-item-title {
font-weight: 600;
@include line-clamp(1);

View File

@ -13,7 +13,7 @@ import {
} from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { Required } from '../../utils';
import { Listable, TableColumnConfig } from '../models';
import { Listable, ListingModes, TableColumnConfig } from '../models';
import { ListingComponent } from '../listing-component.directive';
const SCROLLBAR_WIDTH = 11;
@ -25,6 +25,8 @@ const SCROLLBAR_WIDTH = 11;
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableComponent<T extends Listable> implements OnInit {
readonly listingModes = ListingModes;
@Input() bulkActions?: TemplateRef<unknown>;
@Input() actionsTemplate?: TemplateRef<unknown>;
@Input() headerTemplate?: TemplateRef<unknown>;
@ -44,9 +46,6 @@ export class TableComponent<T extends Listable> implements OnInit {
@Input() tableItemClasses?: { [key: string]: (e: T) => boolean };
@Input() itemMouseEnterFn?: (entity: T) => void;
@Input() itemMouseLeaveFn?: (entity: T) => void;
routerLinkFn?: (entity: T) => string | string[];
tableColumnConfigs!: readonly TableColumnConfig<T>[];
tableHeaderLabel!: string;
@ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport;
constructor(
@ -58,16 +57,24 @@ export class TableComponent<T extends Listable> implements OnInit {
return this._parent;
}
get routerLinkFn(): ((entity: T) => string | string[]) | undefined {
return this.listingComponent.routerLinkFn;
}
get tableColumnConfigs(): readonly TableColumnConfig<T>[] {
return this.listingComponent.tableColumnConfigs;
}
get tableHeaderLabel(): string | undefined {
return this.listingComponent.tableHeaderLabel;
}
ngOnInit(): void {
this.tableColumnConfigs = <TableColumnConfig<T>[]>this.listingComponent.tableColumnConfigs;
this.tableHeaderLabel = this.listingComponent.tableHeaderLabel;
this.routerLinkFn = <((entity: T) => string | string[]) | undefined>this.listingComponent.routerLinkFn;
this.listingComponent.noContent$.subscribe(() => {
setTimeout(() => {
this.scrollViewport.checkViewportSize();
}, 0);
});
this._patchConfig();
this._setStyles();
}

View File

@ -0,0 +1,26 @@
<iqser-table-header
[listingMode]="listingModes.workflow"
[tableHeaderLabel]="tableHeaderLabel"
>
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</iqser-table-header>
<div cdkDropListGroup class="columns-wrapper">
<div (cdkDropListDropped)="move($event)" *ngFor="let column of config.columns"
[cdkDropListData]="column.entities"
[cdkDropListEnterPredicate]="canMoveTo(column)"
[class.dragging]="dragging"
[id]="column.key"
[style.--color]="column.color"
cdkDropList cdkDropListSortingDisabled>
<div class="heading">{{ column.label | translate }} ({{column.entities.length}})</div>
<div class="list">
<div (cdkDragEnded)="dragging = false" (cdkDragStarted)="dragging = true" *ngFor="let entity of column.entities"
[cdkDragData]="entity" [style.--height]="itemHeight" cdkDrag>
<div *cdkDragPlaceholder [style.--height]="itemHeight" class="placeholder"></div>
<ng-container *ngTemplateOutlet="itemTemplate; context: { entity: entity }"></ng-container>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,96 @@
@import '../../../assets/styles/common';
:host {
display: flex;
flex-direction: column;
height: 100%;
}
.columns-wrapper {
display: flex;
padding: 10px;
height: calc(100% - 70px);
> .cdk-drop-list {
display: flex;
flex-direction: column;
flex: 1;
background-color: $grey-2;
border-radius: 6px;
padding-top: 18px;
position: relative;
overflow: hidden;
&::before {
content: '';
width: 100%;
height: 4px;
background-color: var(--color);
border-radius: 6px 6px 0 0;
position: absolute;
top: 0;
left: 0;
}
&:not(:last-child) {
margin-right: 8px;
}
> .heading {
margin-left: 18px;
margin-bottom: 16px;
}
> .list {
overflow-y: auto;
@include no-scroll-bar;
min-height: calc(100% - 36px);
}
&.dragging {
.cdk-drag {
pointer-events: none;
}
&:not(.cdk-drop-list-receiving):not(.cdk-drop-list-dragging) {
background: repeating-linear-gradient(
-45deg,
$separator,
$separator 1px,
$white 1px,
$white 8px
);
> .heading, ::ng-deep.cdk-drag > * {
opacity: 0.3;
}
}
}
}
}
.cdk-drag {
background-color: $white;
transition: background-color 0.2s, box-shadow 0.2s;
border-radius: 8px;
margin: 0 8px 4px 8px;
&:hover {
background-color: $grey-6;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
}
}
.placeholder {
border: 1px dashed $grey-5;
border-radius: 8px;
min-height: var(--height);
margin: 0 8px 4px 8px;
}
.cdk-drag-preview {
max-height: var(--height);
}

View File

@ -0,0 +1,100 @@
import { ChangeDetectionStrategy, Component, forwardRef, Inject, Input, OnInit, TemplateRef } from '@angular/core';
import { ListingComponent } from '../listing-component.directive';
import { Listable, ListingModes } from '../models';
import { CdkDrag, CdkDragDrop } from '@angular/cdk/drag-drop';
import { Required } from '../../utils';
import { LoadingService } from '../../loading';
interface WorkflowColumn<T, K> {
key: K;
label: string;
color: string;
enterFn: (entity: T) => Promise<void> | void;
enterPredicate: (entity: T) => boolean;
entities?: T[];
}
export interface WorkflowConfig<T, K> {
key: string;
columns: WorkflowColumn<T, K>[];
}
@Component({
selector: 'iqser-workflow',
templateUrl: './workflow.component.html',
styleUrls: ['./workflow.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WorkflowComponent<T extends Listable, K extends string> implements OnInit {
readonly listingModes = ListingModes;
@Input() headerTemplate?: TemplateRef<unknown>;
@Input() @Required() itemTemplate!: TemplateRef<T>;
@Input() @Required() config!: WorkflowConfig<T, K>;
dragging = false;
constructor(
@Inject(forwardRef(() => ListingComponent)) private _parent: ListingComponent<T>,
private readonly _loadingService: LoadingService
) {}
_itemHeight = 0;
get itemHeight(): string {
return `${this._itemHeight}px`;
}
get listingComponent(): ListingComponent<T> {
return this._parent;
}
get tableHeaderLabel(): string | undefined {
return this.listingComponent.tableHeaderLabel;
}
async move(event: CdkDragDrop<(T | number)[]>): Promise<void> {
if (event.previousContainer !== event.container) {
const column = this._getColumnByKey((<unknown>event.container.id) as K);
await column.enterFn(event.item.data);
}
}
ngOnInit(): void {
this.listingComponent.entitiesService.displayed$.subscribe(entities => this._updateConfigItems(entities));
}
canMoveTo(column: WorkflowColumn<T, K>): (item: CdkDrag<T>) => boolean {
return (item: CdkDrag<T>) => column.enterPredicate(item.data);
}
private _computeItemHeight(): void {
const items = document.getElementsByClassName('cdk-drag');
if (items.length) {
this._itemHeight = items[0].getBoundingClientRect().height;
}
}
private _updateConfigItems(entities: T[]) {
// Disable updating while dragging
if (this.dragging) {
return;
}
// TODO ...
this.config.columns.forEach(column => {
column.entities = [];
});
entities.forEach(entity => {
const column = this._getColumnByKey(entity[this.config.key]);
if (column) {
column.entities?.push(entity);
}
});
this._computeItemHeight();
}
private _getColumnByKey(key: K): WorkflowColumn<T, K> {
return this.config.columns.find(col => col.key === key) as WorkflowColumn<T, K>;
}
}