From cb8393c492ec1e2795d644009266cc71eeca11a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Tue, 21 Sep 2021 15:43:24 +0300 Subject: [PATCH] Workflow initial --- src/assets/icons/lanes.svg | 19 ++++ src/assets/icons/list.svg | 20 ++++ src/assets/styles/_mixins.scss | 2 + src/assets/styles/_texts.scss | 7 -- src/lib/icons/icons.module.ts | 2 + src/lib/listing/index.ts | 2 + .../listing/listing-component.directive.ts | 11 +- src/lib/listing/listing.module.ts | 5 +- src/lib/listing/models/index.ts | 1 + src/lib/listing/models/listing-modes.ts | 6 ++ .../table-header/table-header.component.html | 1 + .../table-header/table-header.component.ts | 13 +-- src/lib/listing/table/table.component.html | 1 + src/lib/listing/table/table.component.scss | 1 - src/lib/listing/table/table.component.ts | 23 ++-- .../listing/workflow/workflow.component.html | 26 +++++ .../listing/workflow/workflow.component.scss | 96 +++++++++++++++++ .../listing/workflow/workflow.component.ts | 100 ++++++++++++++++++ 18 files changed, 308 insertions(+), 28 deletions(-) create mode 100644 src/assets/icons/lanes.svg create mode 100644 src/assets/icons/list.svg create mode 100644 src/lib/listing/models/listing-modes.ts create mode 100644 src/lib/listing/workflow/workflow.component.html create mode 100644 src/lib/listing/workflow/workflow.component.scss create mode 100644 src/lib/listing/workflow/workflow.component.ts diff --git a/src/assets/icons/lanes.svg b/src/assets/icons/lanes.svg new file mode 100644 index 0000000..d9aa808 --- /dev/null +++ b/src/assets/icons/lanes.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/src/assets/icons/list.svg b/src/assets/icons/list.svg new file mode 100644 index 0000000..a211317 --- /dev/null +++ b/src/assets/icons/list.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/src/assets/styles/_mixins.scss b/src/assets/styles/_mixins.scss index 7236bd7..a49c0b9 100644 --- a/src/assets/styles/_mixins.scss +++ b/src/assets/styles/_mixins.scss @@ -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 */ } } diff --git a/src/assets/styles/_texts.scss b/src/assets/styles/_texts.scss index 82cf526..61e0cbf 100644 --- a/src/assets/styles/_texts.scss +++ b/src/assets/styles/_texts.scss @@ -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; } diff --git a/src/lib/icons/icons.module.ts b/src/lib/icons/icons.module.ts index 8c39496..b5e40e8 100644 --- a/src/lib/icons/icons.module.ts +++ b/src/lib/icons/icons.module.ts @@ -18,6 +18,8 @@ export class IqserIconsModule { 'edit', 'failure', 'help-outline', + 'lanes', + 'list', 'refresh', 'search', 'sort-asc', diff --git a/src/lib/listing/index.ts b/src/lib/listing/index.ts index 29623f2..5fa0f36 100644 --- a/src/lib/listing/index.ts +++ b/src/lib/listing/index.ts @@ -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'; diff --git a/src/lib/listing/listing-component.directive.ts b/src/lib/listing/listing-component.directive.ts index bc20fae..b3c67e5 100644 --- a/src/lib/listing/listing-component.directive.ts +++ b/src/lib/listing/listing-component.directive.ts @@ -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 extends AutoUnsubscri readonly noMatch$ = this._noMatch$; readonly noContent$ = this._noContent$; readonly sortedDisplayedEntities$ = this._sortedDisplayedEntities$; + readonly listingMode$: Observable; 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 extends AutoUnsubscri * @protected */ protected abstract readonly _primaryKey: KeysOf; + private readonly _listingMode$ = new BehaviorSubject(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 extends AutoUnsubscri return this.entitiesService.all; } + set listingMode(listingMode: ListingMode) { + this._listingMode$.next(listingMode); + } + private get _sortedDisplayedEntities$(): Observable { const sort = (entities: T[]) => this.sortingService.defaultSort(entities); const sortedEntities = () => this.entitiesService.displayed$.pipe(map(sort)); diff --git a/src/lib/listing/listing.module.ts b/src/lib/listing/listing.module.ts index 6f4172b..3b347db 100644 --- a/src/lib/listing/listing.module.ts +++ b/src/lib/listing/listing.module.ts @@ -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, diff --git a/src/lib/listing/models/index.ts b/src/lib/listing/models/index.ts index 24d6869..1096e80 100644 --- a/src/lib/listing/models/index.ts +++ b/src/lib/listing/models/index.ts @@ -1,2 +1,3 @@ export * from './listable'; export * from './table-column-config.model'; +export * from './listing-modes'; diff --git a/src/lib/listing/models/listing-modes.ts b/src/lib/listing/models/listing-modes.ts new file mode 100644 index 0000000..ed2cf73 --- /dev/null +++ b/src/lib/listing/models/listing-modes.ts @@ -0,0 +1,6 @@ +export const ListingModes = { + table: 'table', + workflow: 'workflow' +} as const; + +export type ListingMode = keyof typeof ListingModes; diff --git a/src/lib/listing/table-header/table-header.component.html b/src/lib/listing/table-header/table-header.component.html index 5660a7c..8d699f2 100644 --- a/src/lib/listing/table-header/table-header.component.html +++ b/src/lib/listing/table-header/table-header.component.html @@ -19,6 +19,7 @@
{ + readonly listingModes = ListingModes; + @Input() @Required() tableHeaderLabel!: string; @Input() @Required() tableColumnConfigs!: readonly TableColumnConfig[]; @Input() hasEmptyColumn = false; @Input() selectionEnabled = false; - @Input() mode: ListingMode = ListingModes.list; + @Input() listingMode: ListingMode = ListingModes.table; @Input() totalSize?: number; @Input() bulkActions?: TemplateRef; diff --git a/src/lib/listing/table/table.component.html b/src/lib/listing/table/table.component.html index 90e9d9d..a80f4d4 100644 --- a/src/lib/listing/table/table.component.html +++ b/src/lib/listing/table/table.component.html @@ -1,6 +1,7 @@ implements OnInit { + readonly listingModes = ListingModes; + @Input() bulkActions?: TemplateRef; @Input() actionsTemplate?: TemplateRef; @Input() headerTemplate?: TemplateRef; @@ -44,9 +46,6 @@ export class TableComponent 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[]; - tableHeaderLabel!: string; @ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport; constructor( @@ -58,16 +57,24 @@ export class TableComponent implements OnInit { return this._parent; } + get routerLinkFn(): ((entity: T) => string | string[]) | undefined { + return this.listingComponent.routerLinkFn; + } + + get tableColumnConfigs(): readonly TableColumnConfig[] { + return this.listingComponent.tableColumnConfigs; + } + + get tableHeaderLabel(): string | undefined { + return this.listingComponent.tableHeaderLabel; + } + ngOnInit(): void { - this.tableColumnConfigs = []>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(); } diff --git a/src/lib/listing/workflow/workflow.component.html b/src/lib/listing/workflow/workflow.component.html new file mode 100644 index 0000000..99ad25c --- /dev/null +++ b/src/lib/listing/workflow/workflow.component.html @@ -0,0 +1,26 @@ + + + + +
+
+
{{ column.label | translate }} ({{column.entities.length}})
+
+
+
+ +
+
+
+
+ diff --git a/src/lib/listing/workflow/workflow.component.scss b/src/lib/listing/workflow/workflow.component.scss new file mode 100644 index 0000000..e1b37c1 --- /dev/null +++ b/src/lib/listing/workflow/workflow.component.scss @@ -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); +} diff --git a/src/lib/listing/workflow/workflow.component.ts b/src/lib/listing/workflow/workflow.component.ts new file mode 100644 index 0000000..faad148 --- /dev/null +++ b/src/lib/listing/workflow/workflow.component.ts @@ -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 { + key: K; + label: string; + color: string; + enterFn: (entity: T) => Promise | void; + enterPredicate: (entity: T) => boolean; + entities?: T[]; +} + +export interface WorkflowConfig { + key: string; + columns: WorkflowColumn[]; +} + +@Component({ + selector: 'iqser-workflow', + templateUrl: './workflow.component.html', + styleUrls: ['./workflow.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WorkflowComponent implements OnInit { + readonly listingModes = ListingModes; + @Input() headerTemplate?: TemplateRef; + @Input() @Required() itemTemplate!: TemplateRef; + @Input() @Required() config!: WorkflowConfig; + + dragging = false; + + constructor( + @Inject(forwardRef(() => ListingComponent)) private _parent: ListingComponent, + private readonly _loadingService: LoadingService + ) {} + + _itemHeight = 0; + + get itemHeight(): string { + return `${this._itemHeight}px`; + } + + get listingComponent(): ListingComponent { + return this._parent; + } + + get tableHeaderLabel(): string | undefined { + return this.listingComponent.tableHeaderLabel; + } + + async move(event: CdkDragDrop<(T | number)[]>): Promise { + if (event.previousContainer !== event.container) { + const column = this._getColumnByKey((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): (item: CdkDrag) => boolean { + return (item: CdkDrag) => 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 { + return this.config.columns.find(col => col.key === key) as WorkflowColumn; + } +}