diff --git a/src/assets/icons/arrow-down-o.svg b/src/assets/icons/arrow-down-o.svg
new file mode 100644
index 0000000..384b3c9
--- /dev/null
+++ b/src/assets/icons/arrow-down-o.svg
@@ -0,0 +1,18 @@
+
diff --git a/src/assets/styles/_inputs.scss b/src/assets/styles/_inputs.scss
index b6fbdb3..02ed6c8 100644
--- a/src/assets/styles/_inputs.scss
+++ b/src/assets/styles/_inputs.scss
@@ -195,7 +195,7 @@ form .iqser-input-group:not(first-of-type) {
.mat-datepicker-toggle {
position: absolute;
right: 0;
- top: 4px;
+ top: 1px;
color: $accent;
&.mat-datepicker-toggle-active {
diff --git a/src/assets/styles/_tables.scss b/src/assets/styles/_tables.scss
index 1579698..db1da5e 100644
--- a/src/assets/styles/_tables.scss
+++ b/src/assets/styles/_tables.scss
@@ -1,53 +1,3 @@
-@import 'variables';
-
-.table-header {
- display: flex;
- border-bottom: 1px solid $separator;
-
- &.no-data:not([synced='true']) {
- padding-left: 30px;
- }
-
- iqser-table-column-name:last-of-type {
- > div {
- padding-right: 13px;
- }
- }
-
- &.selection-enabled iqser-table-column-name > div {
- padding-left: 10px;
- }
-}
-
-.header-item {
- background-color: $btn-bg;
- height: 50px;
- padding: 0 24px;
- display: flex;
- align-items: center;
- z-index: 1;
- border-bottom: 1px solid $separator;
- box-sizing: border-box;
-
- > *:not(:last-child) {
- margin-right: 10px;
- }
-
- .actions {
- display: flex;
- align-items: center;
- justify-content: flex-end;
-
- > *:not(:last-child) {
- margin-right: 16px;
- }
- }
-
- &.selection-enabled {
- padding-left: 10px;
- }
-}
-
.scrollbar-placeholder {
width: 11px;
padding: 0 !important;
diff --git a/src/index.ts b/src/index.ts
index 4583469..0a56bbd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,3 +12,4 @@ export * from './lib/misc';
export * from './lib/loading';
export * from './lib/error';
export * from './lib/search';
+export * from './lib/empty-states';
diff --git a/src/lib/common-ui.module.ts b/src/lib/common-ui.module.ts
index 3f025d7..d255dc3 100644
--- a/src/lib/common-ui.module.ts
+++ b/src/lib/common-ui.module.ts
@@ -14,6 +14,8 @@ import { IqserInputsModule } from './inputs';
import { IqserHelpModeModule } from './help-mode';
import { IqserIconsModule } from './icons';
import { IqserButtonsModule } from './buttons';
+import { IqserScrollbarModule } from './scrollbar';
+import { IqserEmptyStatesModule } from './empty-states';
const matModules = [MatIconModule, MatProgressSpinnerModule];
const modules = [
@@ -23,7 +25,9 @@ const modules = [
IqserListingModule,
IqserFiltersModule,
IqserInputsModule,
- IqserHelpModeModule
+ IqserHelpModeModule,
+ IqserScrollbarModule,
+ IqserEmptyStatesModule
];
const components = [StatusBarComponent, FullPageLoadingIndicatorComponent, FullPageErrorComponent];
const pipes = [SortByPipe, HumanizePipe];
diff --git a/src/lib/empty-states/empty-state.module.ts b/src/lib/empty-states/empty-state.module.ts
new file mode 100644
index 0000000..0e352a1
--- /dev/null
+++ b/src/lib/empty-states/empty-state.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IqserIconsModule } from '../icons';
+import { EmptyStateComponent } from './empty-state/empty-state.component';
+import { IqserButtonsModule } from '../buttons';
+
+const modules = [IqserIconsModule, IqserButtonsModule];
+const components = [EmptyStateComponent];
+
+@NgModule({
+ declarations: [...components],
+ imports: [CommonModule, ...modules],
+ exports: [...components]
+})
+export class IqserEmptyStatesModule {}
diff --git a/src/lib/empty-states/empty-state/empty-state.component.html b/src/lib/empty-states/empty-state/empty-state.component.html
new file mode 100644
index 0000000..cc9bca5
--- /dev/null
+++ b/src/lib/empty-states/empty-state/empty-state.component.html
@@ -0,0 +1,21 @@
+
diff --git a/src/lib/empty-states/empty-state/empty-state.component.scss b/src/lib/empty-states/empty-state/empty-state.component.scss
new file mode 100644
index 0000000..042e4c1
--- /dev/null
+++ b/src/lib/empty-states/empty-state/empty-state.component.scss
@@ -0,0 +1,27 @@
+@import '../../../assets/styles/common';
+
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+
+ > mat-icon {
+ height: 60px;
+ width: 60px;
+ opacity: 0.1;
+ }
+
+ .heading-l {
+ color: $grey-7;
+ }
+
+ > .heading-l,
+ iqser-icon-button {
+ margin-top: 24px;
+ }
+
+ .ng-content-wrapper:not(:empty) + .heading-l {
+ display: none;
+ }
+}
diff --git a/src/lib/empty-states/empty-state/empty-state.component.ts b/src/lib/empty-states/empty-state/empty-state.component.ts
new file mode 100644
index 0000000..56a99e1
--- /dev/null
+++ b/src/lib/empty-states/empty-state/empty-state.component.ts
@@ -0,0 +1,26 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { IconButtonTypes } from '../../buttons';
+import { Required } from '../../utils';
+
+@Component({
+ selector: 'iqser-empty-state',
+ templateUrl: './empty-state.component.html',
+ styleUrls: ['./empty-state.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class EmptyStateComponent implements OnInit {
+ readonly iconButtonTypes = IconButtonTypes;
+
+ @Input() @Required() text!: string;
+ @Input() icon?: string;
+ @Input() showButton = true;
+ @Input() buttonIcon = 'red:plus';
+ @Input() buttonLabel?: string;
+ @Input() horizontalPadding = 100;
+ @Input() verticalPadding = 120;
+ @Output() readonly action = new EventEmitter();
+
+ ngOnInit(): void {
+ this.showButton = this.showButton && this.action.observers.length > 0;
+ }
+}
diff --git a/src/lib/empty-states/index.ts b/src/lib/empty-states/index.ts
new file mode 100644
index 0000000..987730b
--- /dev/null
+++ b/src/lib/empty-states/index.ts
@@ -0,0 +1,2 @@
+export * from './empty-state.module';
+export * from './empty-state/empty-state.component';
diff --git a/src/lib/error/error.service.ts b/src/lib/error/error.service.ts
index f23ab9d..2f2e878 100644
--- a/src/lib/error/error.service.ts
+++ b/src/lib/error/error.service.ts
@@ -2,14 +2,19 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { LoadingService } from '../loading';
+import { filter } from 'rxjs/operators';
+import { NavigationStart, Router } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class ErrorService {
readonly error$: Observable;
private readonly _errorEvent$ = new BehaviorSubject(undefined);
- constructor(private readonly _loadingService: LoadingService) {
+ constructor(private readonly _loadingService: LoadingService, private readonly _router: Router) {
this.error$ = this._errorEvent$.asObservable();
+ _router.events.pipe(filter(event => event instanceof NavigationStart)).subscribe(() => {
+ this.clear();
+ });
}
set(error: HttpErrorResponse): void {
diff --git a/src/lib/icons/icons.module.ts b/src/lib/icons/icons.module.ts
index 9c3f444..8c39496 100644
--- a/src/lib/icons/icons.module.ts
+++ b/src/lib/icons/icons.module.ts
@@ -12,6 +12,7 @@ export class IqserIconsModule {
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) {
const icons: Set = new Set([
'arrow-down',
+ 'arrow-down-o',
'check',
'close',
'edit',
diff --git a/src/lib/listing/index.ts b/src/lib/listing/index.ts
index 004b4bc..29623f2 100644
--- a/src/lib/listing/index.ts
+++ b/src/lib/listing/index.ts
@@ -1,8 +1,12 @@
-export * from './tables';
-export * from './workflow-listing';
+export * from './models';
+export * from './services';
+
+export * from './scroll-button/scroll-button.component';
+export * from './table/table.component';
+export * from './table-column-name/table-column-name.component';
+export * from './table-header/table-header.component';
+
+export * from './sync-width.directive';
export * from './listing.module';
-export * from './entities.service';
export * from './listing-component.directive';
-export * from './table-header/table-header.component';
-export * from './models/listable';
diff --git a/src/lib/listing/listing-component.directive.ts b/src/lib/listing/listing-component.directive.ts
index c28a074..bc20fae 100644
--- a/src/lib/listing/listing-component.directive.ts
+++ b/src/lib/listing/listing-component.directive.ts
@@ -5,9 +5,8 @@ import { FilterService } from '../filtering';
import { SortingOrders, SortingService } from '../sorting';
import { AutoUnsubscribe, Bind, KeysOf } from '../utils';
import { SearchService } from '../search';
-import { TableColumnConfig } from './tables';
-import { EntitiesService } from './entities.service';
-import { Listable } from './models/listable';
+import { EntitiesService } from './services';
+import { Listable, TableColumnConfig } from './models';
export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const;
@@ -19,9 +18,14 @@ export abstract class ListingComponent extends AutoUnsubscri
readonly entitiesService = this._injector.get>(EntitiesService);
readonly noMatch$ = this._noMatch$;
+ readonly noContent$ = this._noContent$;
readonly sortedDisplayedEntities$ = this._sortedDisplayedEntities$;
+ readonly routerLinkFn?: (entity: T) => string | string[];
+ // TODO: These should be somewhere in table listing, not generic listing
abstract readonly tableColumnConfigs: readonly TableColumnConfig[];
+ abstract readonly tableHeaderLabel: string;
+
/**
* Key used in the *trackBy* function with **ngFor* or **cdkVirtualFor*
* and in the default sorting and as the search field
@@ -51,6 +55,13 @@ export abstract class ListingComponent extends AutoUnsubscri
);
}
+ private get _noContent$(): Observable {
+ return combineLatest([this._noMatch$, this.entitiesService.noData$]).pipe(
+ map(([noMatch, noData]) => noMatch || noData),
+ distinctUntilChanged()
+ );
+ }
+
setInitialConfig(): void {
this.sortingService.setSortingOption({
column: this._primaryKey,
diff --git a/src/lib/listing/listing.module.ts b/src/lib/listing/listing.module.ts
index 04b5b05..6f4172b 100644
--- a/src/lib/listing/listing.module.ts
+++ b/src/lib/listing/listing.module.ts
@@ -1,18 +1,37 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
-import { TablesModule } from './tables';
import { TableHeaderComponent } from './table-header/table-header.component';
-import { WorkflowListingModule } from './workflow-listing';
import { IqserFiltersModule } from '../filtering';
import { IqserInputsModule } from '../inputs';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { TableColumnNameComponent } from './table-column-name/table-column-name.component';
+import { ScrollButtonComponent } from './scroll-button/scroll-button.component';
+import { TableComponent } from './table/table.component';
+import { SyncWidthDirective } from './sync-width.directive';
+import { ScrollingModule } from '@angular/cdk/scrolling';
+import { IqserIconsModule } from '../icons';
+import { IqserScrollbarModule } from '../scrollbar';
+import { RouterModule } from '@angular/router';
+import { IqserEmptyStatesModule } from '../empty-states';
-const components = [TableHeaderComponent];
-const modules = [TranslateModule, TablesModule, WorkflowListingModule, IqserFiltersModule, IqserInputsModule];
+const matModules = [MatTooltipModule];
+const components = [TableHeaderComponent, TableComponent, TableColumnNameComponent, ScrollButtonComponent];
+const modules = [
+ TranslateModule,
+ IqserFiltersModule,
+ IqserInputsModule,
+ IqserIconsModule,
+ IqserScrollbarModule,
+ IqserEmptyStatesModule,
+ ScrollingModule,
+ RouterModule
+];
+const utils = [SyncWidthDirective];
@NgModule({
- declarations: [...components],
- exports: [...components, TablesModule, WorkflowListingModule],
- imports: [CommonModule, ...modules]
+ declarations: [...components, ...utils],
+ exports: [...components, ...utils],
+ imports: [CommonModule, ...modules, ...matModules]
})
export class IqserListingModule {}
diff --git a/src/lib/listing/models/index.ts b/src/lib/listing/models/index.ts
new file mode 100644
index 0000000..24d6869
--- /dev/null
+++ b/src/lib/listing/models/index.ts
@@ -0,0 +1,2 @@
+export * from './listable';
+export * from './table-column-config.model';
diff --git a/src/lib/listing/tables/models/table-column-config.model.ts b/src/lib/listing/models/table-column-config.model.ts
similarity index 57%
rename from src/lib/listing/tables/models/table-column-config.model.ts
rename to src/lib/listing/models/table-column-config.model.ts
index 997a7cf..34679b8 100644
--- a/src/lib/listing/tables/models/table-column-config.model.ts
+++ b/src/lib/listing/models/table-column-config.model.ts
@@ -1,4 +1,5 @@
-import { KeysOf } from '../../../utils';
+import { KeysOf } from '../../utils';
+import { TemplateRef } from '@angular/core';
export interface TableColumnConfig {
readonly label: string;
@@ -8,4 +9,8 @@ export interface TableColumnConfig {
readonly rightIcon?: string;
readonly rightIconTooltip?: string;
readonly notTranslatable?: boolean;
+ readonly width?: string;
+ readonly template: TemplateRef;
+ readonly extra?: unknown;
+ last?: boolean;
}
diff --git a/src/lib/listing/scroll-button/scroll-button.component.html b/src/lib/listing/scroll-button/scroll-button.component.html
new file mode 100644
index 0000000..b04b249
--- /dev/null
+++ b/src/lib/listing/scroll-button/scroll-button.component.html
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/lib/listing/scroll-button/scroll-button.component.scss b/src/lib/listing/scroll-button/scroll-button.component.scss
new file mode 100644
index 0000000..093aaa9
--- /dev/null
+++ b/src/lib/listing/scroll-button/scroll-button.component.scss
@@ -0,0 +1,30 @@
+@import '../../../assets/styles/common';
+
+.scroll-button {
+ background-color: $white;
+ position: absolute;
+ right: 0;
+ height: 40px;
+ width: 44px;
+ border: none;
+ border-radius: 8px 0 0 8px;
+ box-shadow: -1px 1px 5px 0 rgba(40, 50, 65, 0.25);
+
+ &.bottom {
+ bottom: 30px;
+ }
+
+ &.top {
+ top: 100px;
+
+ mat-icon {
+ transform: rotate(180deg);
+ }
+ }
+}
+
+mat-icon {
+ width: 22px;
+ height: 22px;
+ color: $grey-7;
+}
diff --git a/src/lib/listing/scroll-button/scroll-button.component.ts b/src/lib/listing/scroll-button/scroll-button.component.ts
new file mode 100644
index 0000000..09ad966
--- /dev/null
+++ b/src/lib/listing/scroll-button/scroll-button.component.ts
@@ -0,0 +1,61 @@
+import { ChangeDetectionStrategy, Component, HostListener, Input, OnInit } from '@angular/core';
+import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
+import { concatMap, delay, distinctUntilChanged, map, startWith } from 'rxjs/operators';
+import { Observable, of } from 'rxjs';
+import { Required } from '../../utils';
+
+const ButtonTypes = {
+ top: 'top',
+ bottom: 'bottom'
+} as const;
+
+type ButtonType = keyof typeof ButtonTypes;
+
+@Component({
+ selector: 'iqser-scroll-button',
+ templateUrl: './scroll-button.component.html',
+ styleUrls: ['./scroll-button.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class ScrollButtonComponent implements OnInit {
+ readonly buttonType = ButtonTypes;
+
+ @Input() @Required() scrollViewport!: CdkVirtualScrollViewport;
+ @Input() @Required() itemSize!: number;
+
+ showScrollUp$?: Observable;
+ showScrollDown$?: Observable;
+
+ ngOnInit(): void {
+ const scrollSize = () => this.scrollViewport.getDataLength() * this.itemSize;
+ const scrollIsNeeded = () => this.scrollViewport.getViewportSize() < scrollSize();
+ const reachedEnd = (type: ButtonType) => this.scrollViewport.measureScrollOffset(type) === 0;
+
+ const showScrollUp = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.top);
+ const showScrollDown = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.bottom);
+
+ const scroll$ = this.scrollViewport.elementScrolled().pipe(
+ startWith(''),
+ /** Delay first value so that we can wait for items to be rendered in viewport and get correct values */
+ concatMap((value, index) => (index === 0 ? of(value).pipe(delay(0)) : of(value)))
+ );
+ this.showScrollUp$ = scroll$.pipe(map(showScrollUp), distinctUntilChanged());
+ this.showScrollDown$ = scroll$.pipe(map(showScrollDown), distinctUntilChanged());
+ }
+
+ scroll(type: ButtonType): void {
+ const viewportSize = (this.scrollViewport?.getViewportSize() - this.itemSize) * (type === ButtonTypes.top ? -1 : 1);
+ const scrollOffset = this.scrollViewport?.measureScrollOffset('top');
+ this.scrollViewport?.scrollToOffset(scrollOffset + viewportSize, 'smooth');
+ }
+
+ @HostListener('document:keyup', ['$event'])
+ spaceAndPageDownScroll(event: KeyboardEvent): void {
+ const target = event.target as EventTarget & { tagName: string };
+ if (['Space', 'PageDown'].includes(event.code) && target.tagName === 'BODY') {
+ this.scroll(ButtonTypes.bottom);
+ } else if (['PageUp'].includes(event.code) && target.tagName === 'BODY') {
+ this.scroll(ButtonTypes.top);
+ }
+ }
+}
diff --git a/src/lib/listing/entities.service.ts b/src/lib/listing/services/entities.service.ts
similarity index 96%
rename from src/lib/listing/entities.service.ts
rename to src/lib/listing/services/entities.service.ts
index c73f34b..b4edab1 100644
--- a/src/lib/listing/entities.service.ts
+++ b/src/lib/listing/services/entities.service.ts
@@ -1,9 +1,9 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, pipe } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
-import { FilterService, getFilteredEntities } from '../filtering';
-import { SearchService } from '../search';
-import { Listable } from './models/listable';
+import { FilterService, getFilteredEntities } from '../../filtering';
+import { SearchService } from '../../search';
+import { Listable } from '../models';
const toLengthValue = (entities: unknown[]) => entities?.length ?? 0;
const getLength = pipe(map(toLengthValue), distinctUntilChanged());
diff --git a/src/lib/listing/services/index.ts b/src/lib/listing/services/index.ts
new file mode 100644
index 0000000..8d6e94c
--- /dev/null
+++ b/src/lib/listing/services/index.ts
@@ -0,0 +1 @@
+export * from './entities.service';
diff --git a/src/lib/listing/tables/sync-width.directive.ts b/src/lib/listing/sync-width.directive.ts
similarity index 100%
rename from src/lib/listing/tables/sync-width.directive.ts
rename to src/lib/listing/sync-width.directive.ts
diff --git a/src/lib/listing/tables/table-column-name/table-column-name.component.html b/src/lib/listing/table-column-name/table-column-name.component.html
similarity index 91%
rename from src/lib/listing/tables/table-column-name/table-column-name.component.html
rename to src/lib/listing/table-column-name/table-column-name.component.html
index b9331ee..745d01d 100644
--- a/src/lib/listing/tables/table-column-name/table-column-name.component.html
+++ b/src/lib/listing/table-column-name/table-column-name.component.html
@@ -3,7 +3,7 @@
{{ label }}
-
+
diff --git a/src/lib/listing/tables/table-column-name/table-column-name.component.scss b/src/lib/listing/table-column-name/table-column-name.component.scss
similarity index 83%
rename from src/lib/listing/tables/table-column-name/table-column-name.component.scss
rename to src/lib/listing/table-column-name/table-column-name.component.scss
index ed0a5d7..731b910 100644
--- a/src/lib/listing/tables/table-column-name/table-column-name.component.scss
+++ b/src/lib/listing/table-column-name/table-column-name.component.scss
@@ -1,4 +1,4 @@
-@import '../../../../assets/styles/common';
+@import '../../../assets/styles/common';
:host {
display: flex;
@@ -10,7 +10,7 @@
display: flex;
width: 100%;
line-height: 11px;
- padding: 0 24px;
+ padding: 0 10px;
> mat-icon {
width: 10px;
@@ -24,6 +24,14 @@
}
}
+ &:first-child > div {
+ padding: 0 24px;
+ }
+
+ &:last-of-type > div {
+ padding: 0 13px 0 10px;
+ }
+
.flex-end {
min-width: 58px;
}
diff --git a/src/lib/listing/tables/table-column-name/table-column-name.component.ts b/src/lib/listing/table-column-name/table-column-name.component.ts
similarity index 87%
rename from src/lib/listing/tables/table-column-name/table-column-name.component.ts
rename to src/lib/listing/table-column-name/table-column-name.component.ts
index 887cb14..8977bf2 100644
--- a/src/lib/listing/tables/table-column-name/table-column-name.component.ts
+++ b/src/lib/listing/table-column-name/table-column-name.component.ts
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, Optional } from '@angular/core';
-import { SortingOrders, SortingService } from '../../../sorting';
-import { KeysOf, Required } from '../../../utils';
+import { SortingOrders, SortingService } from '../../sorting';
+import { KeysOf, Required } from '../../utils';
const ifHasRightIcon =
(thisArg: TableColumnNameComponent) => !!thisArg.rightIcon;
diff --git a/src/lib/listing/table-header/table-header.component.html b/src/lib/listing/table-header/table-header.component.html
index d695cf3..5660a7c 100644
--- a/src/lib/listing/table-header/table-header.component.html
+++ b/src/lib/listing/table-header/table-header.component.html
@@ -7,7 +7,7 @@
>
- {{ tableHeaderLabel | translate: { length: (entitiesService.displayedLength$ | async) } }}
+ {{ tableHeaderLabel | translate: {length: totalSize || (entitiesService.displayedLength$ | async)} }}
diff --git a/src/lib/listing/table-header/table-header.component.scss b/src/lib/listing/table-header/table-header.component.scss
index e69de29..106f35c 100644
--- a/src/lib/listing/table-header/table-header.component.scss
+++ b/src/lib/listing/table-header/table-header.component.scss
@@ -0,0 +1,39 @@
+@import '../../../assets/styles/common';
+
+.table-header {
+ display: flex;
+ border-bottom: 1px solid $separator;
+
+ &.no-data.selection-enabled:not([synced='true']) {
+ padding-left: 30px;
+ }
+}
+
+.header-item {
+ background-color: $btn-bg;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ z-index: 1;
+ border-bottom: 1px solid $separator;
+ box-sizing: border-box;
+ padding: 0 24px;
+
+ &.selection-enabled {
+ padding: 0 24px 0 10px;
+ }
+
+ > *:not(:last-child) {
+ margin-right: 10px;
+ }
+
+ .actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+
+ > *:not(:last-child) {
+ margin-right: 16px;
+ }
+ }
+}
diff --git a/src/lib/listing/table-header/table-header.component.ts b/src/lib/listing/table-header/table-header.component.ts
index 86bbb4e..69e28aa 100644
--- a/src/lib/listing/table-header/table-header.component.ts
+++ b/src/lib/listing/table-header/table-header.component.ts
@@ -1,9 +1,8 @@
import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core';
import { Required } from '../../utils';
import { FilterService } from '../../filtering';
-import { EntitiesService } from '../entities.service';
-import { Listable } from '../models/listable';
-import { TableColumnConfig } from '../tables';
+import { EntitiesService } from '../services';
+import { Listable, TableColumnConfig } from '../models';
export const ListingModes = {
list: 'list',
@@ -24,6 +23,7 @@ export class TableHeaderComponent {
@Input() hasEmptyColumn = false;
@Input() selectionEnabled = false;
@Input() mode: ListingMode = ListingModes.list;
+ @Input() totalSize?: number;
@Input() bulkActions?: TemplateRef;
constructor(readonly entitiesService: EntitiesService, readonly filterService: FilterService) {}
diff --git a/src/lib/listing/table/table.component.html b/src/lib/listing/table/table.component.html
new file mode 100644
index 0000000..90e9d9d
--- /dev/null
+++ b/src/lib/listing/table/table.component.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/listing/table/table.component.scss b/src/lib/listing/table/table.component.scss
new file mode 100644
index 0000000..b6c834f
--- /dev/null
+++ b/src/lib/listing/table/table.component.scss
@@ -0,0 +1,129 @@
+@import '../../../assets/styles/common';
+
+:host cdk-virtual-scroll-viewport {
+ height: calc(100vh - 50px - 31px - 111px);
+ overflow-y: hidden !important;
+
+ &.no-data {
+ display: none;
+ }
+
+ &.has-scrollbar:hover ::ng-deep.cdk-virtual-scroll-content-wrapper {
+ grid-template-columns: var(--gridTemplateColumnsHover);
+ }
+
+ ::ng-deep.cdk-virtual-scroll-content-wrapper {
+ grid-template-columns: var(--gridTemplateColumns);
+ display: grid;
+
+ .table-item {
+ display: contents;
+
+ > div {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ position: relative;
+ box-sizing: border-box;
+ border-bottom: 1px solid $separator;
+ height: var(--itemSize);
+ padding: 0 10px;
+
+ &.cell:first-of-type {
+ padding: 0 24px;
+ }
+
+ &.cell:last-of-type {
+ padding: 0 13px 0 10px;
+ }
+
+ &:not(.scrollbar-placeholder):not(.selection-column) {
+ min-width: 110px;
+ }
+
+ &.center {
+ align-items: center;
+ justify-content: center;
+ }
+
+ &.selection-column {
+ padding-right: 0 !important;
+
+ iqser-round-checkbox .wrapper {
+ opacity: 0;
+ transition: opacity 0.2s;
+
+ &.active {
+ opacity: 1;
+ }
+ }
+ }
+ }
+
+ .table-item-title {
+ font-weight: 600;
+ @include line-clamp(1);
+ }
+
+ .action-buttons {
+ position: absolute;
+ display: none;
+ right: -11px;
+ top: 0;
+ height: 100%;
+ width: fit-content;
+ flex-direction: row;
+ align-items: center;
+ padding-left: 100px;
+ padding-right: 21px;
+ z-index: 1;
+ background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, $grey-2 35%);
+
+ mat-icon {
+ width: 14px;
+ }
+
+ iqser-circle-button:not(:last-child) {
+ margin-right: 2px;
+ }
+ }
+
+ input,
+ mat-select {
+ margin-top: 0;
+ }
+
+ &:hover {
+ > div {
+ background-color: $grey-8;
+
+ &.selection-column iqser-round-checkbox .wrapper {
+ opacity: 1;
+ }
+ }
+
+ .action-buttons {
+ display: flex;
+ }
+ }
+ }
+ }
+
+ &:hover {
+ overflow-y: auto !important;
+ @include scroll-bar;
+
+ &.has-scrollbar {
+ .table-item {
+ .action-buttons {
+ right: 0;
+ padding-right: 13px;
+ }
+
+ .scrollbar-placeholder {
+ display: none;
+ }
+ }
+ }
+ }
+}
diff --git a/src/lib/listing/table/table.component.ts b/src/lib/listing/table/table.component.ts
new file mode 100644
index 0000000..41225a3
--- /dev/null
+++ b/src/lib/listing/table/table.component.ts
@@ -0,0 +1,116 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ EventEmitter,
+ forwardRef,
+ Inject,
+ Input,
+ OnInit,
+ Output,
+ TemplateRef,
+ ViewChild,
+ ViewContainerRef
+} from '@angular/core';
+import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
+import { Required } from '../../utils';
+import { Listable, TableColumnConfig } from '../models';
+import { ListingComponent } from '../listing-component.directive';
+
+const SCROLLBAR_WIDTH = 11;
+
+@Component({
+ selector: 'iqser-table',
+ templateUrl: './table.component.html',
+ styleUrls: ['./table.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class TableComponent implements OnInit {
+ @Input() bulkActions?: TemplateRef;
+ @Input() actionsTemplate?: TemplateRef;
+ @Input() headerTemplate?: TemplateRef;
+ @Input() @Required() itemSize!: number;
+ @Input() selectionEnabled = false;
+ @Input() hasScrollButton = false;
+ @Input() emptyColumnWidth?: string;
+ @Input() totalSize?: number;
+ @Input() classes?: string;
+ @Input() noDataText?: string;
+ @Input() noDataIcon?: string;
+ @Input() noDataButtonIcon?: string;
+ @Input() noDataButtonLabel?: string;
+ @Input() showNoDataButton = false;
+ @Output() readonly noDataAction = new EventEmitter();
+ @Input() noMatchText?: string;
+ @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(
+ @Inject(forwardRef(() => ListingComponent)) private _parent: ListingComponent,
+ private readonly _hostRef: ViewContainerRef
+ ) {}
+
+ get listingComponent(): ListingComponent {
+ return this._parent;
+ }
+
+ 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();
+ }
+
+ getTableItemClasses(entity: T): { [key: string]: boolean } {
+ const classes: { [key: string]: boolean } = {
+ 'table-item': true,
+ pointer: !!this.routerLinkFn && this.routerLinkFn(entity).length > 0
+ };
+ for (const key in this.tableItemClasses) {
+ if (Object.prototype.hasOwnProperty.call(this.tableItemClasses, key)) {
+ classes[key] = this.tableItemClasses[key](entity);
+ }
+ }
+ return classes;
+ }
+
+ private _patchConfig() {
+ this.tableColumnConfigs[this.tableColumnConfigs.length - 1].last = true;
+ }
+
+ private _setStyles(): void {
+ const element = this._hostRef.element.nativeElement as HTMLElement;
+ this._setColumnsWidth(element);
+ this._setItemSize(element);
+ }
+
+ private _setColumnsWidth(element: HTMLElement) {
+ let gridTemplateColumnsHover = '';
+ if (this.selectionEnabled) {
+ gridTemplateColumnsHover += '30px ';
+ }
+ for (const config of this.tableColumnConfigs) {
+ gridTemplateColumnsHover += `${config.width || '1fr'} `;
+ }
+ gridTemplateColumnsHover += this.emptyColumnWidth || '';
+ const gridTemplateColumns = `${gridTemplateColumnsHover} ${SCROLLBAR_WIDTH}px`;
+
+ element.style.setProperty('--gridTemplateColumns', gridTemplateColumns);
+ element.style.setProperty('--gridTemplateColumnsHover', gridTemplateColumnsHover);
+ }
+
+ private _setItemSize(element: HTMLElement) {
+ element.style.setProperty('--itemSize', `${this.itemSize}px`);
+ }
+}
diff --git a/src/lib/listing/tables/index.ts b/src/lib/listing/tables/index.ts
deleted file mode 100644
index 2cad679..0000000
--- a/src/lib/listing/tables/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './tables-module';
-export * from './table-column-name/table-column-name.component';
-export * from './models/table-column-config.model';
-export * from './sync-width.directive';
diff --git a/src/lib/listing/tables/tables-module.ts b/src/lib/listing/tables/tables-module.ts
deleted file mode 100644
index 96a9db2..0000000
--- a/src/lib/listing/tables/tables-module.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { TranslateModule } from '@ngx-translate/core';
-import { TableColumnNameComponent } from './table-column-name/table-column-name.component';
-import { SyncWidthDirective } from './sync-width.directive';
-import { IqserIconsModule } from '../../icons';
-
-const matModules = [MatTooltipModule];
-const components = [TableColumnNameComponent];
-const utils = [SyncWidthDirective];
-
-@NgModule({
- declarations: [...components, ...utils],
- exports: [...components, ...utils],
- imports: [CommonModule, TranslateModule, IqserIconsModule, ...matModules]
-})
-export class TablesModule {}
diff --git a/src/lib/listing/workflow-listing/index.ts b/src/lib/listing/workflow-listing/index.ts
deleted file mode 100644
index 87b1b5e..0000000
--- a/src/lib/listing/workflow-listing/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './workflow-listing.module';
diff --git a/src/lib/listing/workflow-listing/workflow-listing.module.ts b/src/lib/listing/workflow-listing/workflow-listing.module.ts
deleted file mode 100644
index 66d6d54..0000000
--- a/src/lib/listing/workflow-listing/workflow-listing.module.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { DragDropModule } from '@angular/cdk/drag-drop';
-import { TranslateModule } from '@ngx-translate/core';
-
-const matModules = [DragDropModule];
-
-@NgModule({
- declarations: [],
- imports: [CommonModule, TranslateModule, ...matModules]
-})
-export class WorkflowListingModule {}
diff --git a/src/lib/scrollbar/has-scrollbar.directive.ts b/src/lib/scrollbar/has-scrollbar.directive.ts
new file mode 100644
index 0000000..c2edf2f
--- /dev/null
+++ b/src/lib/scrollbar/has-scrollbar.directive.ts
@@ -0,0 +1,27 @@
+import { AfterContentChecked, Directive, ElementRef, HostBinding } from '@angular/core';
+
+@Directive({
+ selector: '[iqserHasScrollbar]',
+ exportAs: 'iqserHasScrollbar'
+})
+export class HasScrollbarDirective implements AfterContentChecked {
+ @HostBinding('class') class = '';
+
+ constructor(private readonly _elementRef: ElementRef) {}
+
+ get hasScrollbar(): boolean {
+ const element = this._elementRef?.nativeElement as HTMLElement;
+ return element.clientHeight < element.scrollHeight;
+ }
+
+ ngAfterContentChecked(): void {
+ this._process();
+ }
+
+ _process(): void {
+ const newClass = this.hasScrollbar ? 'has-scrollbar' : '';
+ if (this.class !== newClass) {
+ this.class = newClass;
+ }
+ }
+}
diff --git a/src/lib/scrollbar/index.ts b/src/lib/scrollbar/index.ts
new file mode 100644
index 0000000..19629b5
--- /dev/null
+++ b/src/lib/scrollbar/index.ts
@@ -0,0 +1,2 @@
+export * from '../scrollbar/has-scrollbar.directive';
+export * from './scrollbar.module';
diff --git a/src/lib/scrollbar/scrollbar.module.ts b/src/lib/scrollbar/scrollbar.module.ts
new file mode 100644
index 0000000..5efb8f1
--- /dev/null
+++ b/src/lib/scrollbar/scrollbar.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { HasScrollbarDirective } from './has-scrollbar.directive';
+
+const utils = [HasScrollbarDirective];
+
+@NgModule({
+ declarations: [...utils],
+ exports: [...utils],
+ imports: [CommonModule],
+ providers: [HasScrollbarDirective]
+})
+export class IqserScrollbarModule {}