Workflow initial
This commit is contained in:
parent
6c0f123bd9
commit
cb8393c492
19
src/assets/icons/lanes.svg
Normal file
19
src/assets/icons/lanes.svg
Normal 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
20
src/assets/icons/list.svg
Normal 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 |
@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@ export class IqserIconsModule {
|
||||
'edit',
|
||||
'failure',
|
||||
'help-outline',
|
||||
'lanes',
|
||||
'list',
|
||||
'refresh',
|
||||
'search',
|
||||
'sort-asc',
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './listable';
|
||||
export * from './table-column-config.model';
|
||||
export * from './listing-modes';
|
||||
|
||||
6
src/lib/listing/models/listing-modes.ts
Normal file
6
src/lib/listing/models/listing-modes.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const ListingModes = {
|
||||
table: 'table',
|
||||
workflow: 'workflow'
|
||||
} as const;
|
||||
|
||||
export type ListingMode = keyof typeof ListingModes;
|
||||
@ -19,6 +19,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="listingMode === listingModes.table"
|
||||
[class.no-data]="entitiesService.noData$ | async"
|
||||
[class.selection-enabled]="selectionEnabled"
|
||||
class="table-header"
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<iqser-table-header
|
||||
[bulkActions]="bulkActions"
|
||||
[hasEmptyColumn]="!!emptyColumnWidth"
|
||||
[listingMode]="listingModes.table"
|
||||
[selectionEnabled]="selectionEnabled"
|
||||
[tableColumnConfigs]="tableColumnConfigs"
|
||||
[tableHeaderLabel]="tableHeaderLabel"
|
||||
|
||||
@ -69,7 +69,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.table-item-title {
|
||||
font-weight: 600;
|
||||
@include line-clamp(1);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
26
src/lib/listing/workflow/workflow.component.html
Normal file
26
src/lib/listing/workflow/workflow.component.html
Normal 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>
|
||||
|
||||
96
src/lib/listing/workflow/workflow.component.scss
Normal file
96
src/lib/listing/workflow/workflow.component.scss
Normal 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);
|
||||
}
|
||||
100
src/lib/listing/workflow/workflow.component.ts
Normal file
100
src/lib/listing/workflow/workflow.component.ts
Normal 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>;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user