Răsfoiți Sursa

refactor(admin-ui): Simplify DataTable/BulkActionMenu relationship

Michael Bromley 3 ani în urmă
părinte
comite
8701af75b0

+ 7 - 12
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html

@@ -83,19 +83,14 @@
     [currentPage]="currentPage$ | async"
     (pageChange)="setPageNumber($event)"
     (itemsPerPageChange)="setItemsPerPage($event)"
-    [allSelected]="areAllSelected()"
-    [isRowSelectedFn]="isMemberSelected"
-    (rowSelectChange)="toggleSelectMember($event)"
-    (allSelectChange)="toggleSelectAll()"
+    [selectionManager]="selectionManager"
 >
-    <vdr-dt-column>
-        <vdr-bulk-action-menu
-            locationId="product-list"
-            [hostComponent]="this"
-            [selection]="selectionManager.selection"
-            (clearSelection)="selectionManager.clearSelection()"
-        ></vdr-bulk-action-menu>
-    </vdr-dt-column>
+    <vdr-bulk-action-menu
+        locationId="product-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    ></vdr-bulk-action-menu>
+    <vdr-dt-column> </vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>

+ 0 - 20
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.ts

@@ -114,10 +114,6 @@ export class ProductListComponent
             .getPendingSearchIndexUpdates()
             .mapSingle(({ pendingSearchIndexUpdates }) => pendingSearchIndexUpdates)
             .subscribe(value => (this.pendingSearchIndexUpdates = value));
-
-        this.items$
-            .pipe(takeUntil(this.destroy$))
-            .subscribe(items => this.selectionManager.setCurrentItems(items));
     }
 
     ngAfterViewInit() {
@@ -198,20 +194,4 @@ export class ProductListComponent
     setLanguage(code: LanguageCode) {
         this.dataService.client.setContentLanguage(code).subscribe();
     }
-
-    areAllSelected(): boolean {
-        return this.selectionManager.areAllCurrentItemsSelected();
-    }
-
-    toggleSelectAll() {
-        this.selectionManager.toggleSelectAll();
-    }
-
-    toggleSelectMember({ event, item }: { event: MouseEvent; item: SearchProducts.Items }) {
-        this.selectionManager.toggleSelection(item, event);
-    }
-
-    isMemberSelected = (product: SearchProducts.Items): boolean => {
-        return this.selectionManager.isSelected(product);
-    };
 }

+ 16 - 1
packages/admin-ui/src/lib/core/src/common/utilities/selection-manager.ts

@@ -1,3 +1,5 @@
+import { Observable, Subject } from 'rxjs';
+
 export interface SelectionManagerOptions<T> {
     multiSelect: boolean;
     itemsAreEqual: (a: T, b: T) => boolean;
@@ -10,14 +12,19 @@ export interface SelectionManagerOptions<T> {
  * cmd/ctrl/shift key.
  */
 export class SelectionManager<T> {
-    constructor(private options: SelectionManagerOptions<T>) {}
+    constructor(private options: SelectionManagerOptions<T>) {
+        this.selectionChanges$ = this.selectionChangesSubject.asObservable();
+    }
 
     get selection(): T[] {
         return this._selection;
     }
 
+    selectionChanges$: Observable<T[]>;
+
     private _selection: T[] = [];
     private items: T[] = [];
+    private selectionChangesSubject = new Subject<T[]>();
 
     setMultiSelect(isMultiSelect: boolean) {
         this.options.multiSelect = isMultiSelect;
@@ -56,14 +63,17 @@ export class SelectionManager<T> {
         }
         // Make the selection mutable
         this._selection = this._selection.map(x => ({ ...x }));
+        this.invokeOnSelectionChangeHandler();
     }
 
     selectMultiple(items: T[]) {
         this._selection = items;
+        this.invokeOnSelectionChangeHandler();
     }
 
     clearSelection() {
         this._selection = [];
+        this.invokeOnSelectionChangeHandler();
     }
 
     isSelected(item: T): boolean {
@@ -90,9 +100,14 @@ export class SelectionManager<T> {
                 }
             }
         }
+        this.invokeOnSelectionChangeHandler();
     }
 
     lastSelected(): T {
         return this._selection[this._selection.length - 1];
     }
+
+    private invokeOnSelectionChangeHandler() {
+        this.selectionChangesSubject.next(this._selection);
+    }
 }

+ 6 - 1
packages/admin-ui/src/lib/core/src/shared/components/bulk-action-menu/bulk-action-menu.component.html

@@ -1,5 +1,5 @@
 <vdr-dropdown *ngIf="actions$ | async as actions">
-    <button class="btn btn-sm btn-outline" vdrDropdownTrigger [disabled]="!selection?.length">
+    <button class="btn btn-sm btn-outline" vdrDropdownTrigger [disabled]="!selectionManager.selection?.length" [class.hidden]="!selectionManager.selection?.length">
         <clr-icon shape="file-group"></clr-icon>
         {{ 'common.with-selected' | translate }}
     </button>
@@ -22,3 +22,8 @@
         </ng-container>
     </vdr-dropdown-menu>
 </vdr-dropdown>
+<button class="btn btn-sm btn-link" (click)="clearSelection()"
+        [class.hidden]="!selectionManager.selection?.length">
+    <clr-icon shape="times"></clr-icon>
+    {{ 'common.clear-selection' | translate }}
+</button>

+ 7 - 0
packages/admin-ui/src/lib/core/src/shared/components/bulk-action-menu/bulk-action-menu.component.scss

@@ -0,0 +1,7 @@
+:host {
+    display: inline-flex;
+    align-items: center;
+}
+button.hidden {
+    display: none;
+}

+ 14 - 18
packages/admin-ui/src/lib/core/src/shared/components/bulk-action-menu/bulk-action-menu.component.ts

@@ -1,19 +1,17 @@
 import {
     ChangeDetectionStrategy,
+    ChangeDetectorRef,
     Component,
-    EventEmitter,
     Injector,
     Input,
-    OnChanges,
     OnDestroy,
     OnInit,
-    Output,
-    SimpleChanges,
 } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
-import { BehaviorSubject, Observable, Subscription } from 'rxjs';
+import { Observable, Subscription } from 'rxjs';
 import { switchMap } from 'rxjs/operators';
 
+import { SelectionManager } from '../../../common/utilities/selection-manager';
 import { DataService } from '../../../data/providers/data.service';
 import { BulkActionRegistryService } from '../../../providers/bulk-action-registry/bulk-action-registry.service';
 import { BulkAction, BulkActionLocationId } from '../../../providers/bulk-action-registry/bulk-action-types';
@@ -24,14 +22,12 @@ import { BulkAction, BulkActionLocationId } from '../../../providers/bulk-action
     styleUrls: ['./bulk-action-menu.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class BulkActionMenuComponent<T = any> implements OnInit, OnChanges, OnDestroy {
+export class BulkActionMenuComponent<T = any> implements OnInit, OnDestroy {
     @Input() locationId: BulkActionLocationId;
-    @Input() selection: T[];
+    @Input() selectionManager: SelectionManager<T>;
     @Input() hostComponent: any;
-    @Output() clearSelection = new EventEmitter<void>();
     actions$: Observable<Array<BulkAction<T> & { display: boolean }>>;
     userPermissions: string[] = [];
-    selection$ = new BehaviorSubject<T[]>([]);
 
     private subscription: Subscription;
 
@@ -40,17 +36,12 @@ export class BulkActionMenuComponent<T = any> implements OnInit, OnChanges, OnDe
         private injector: Injector,
         private route: ActivatedRoute,
         private dataService: DataService,
+        private changeDetectorRef: ChangeDetectorRef,
     ) {}
 
-    ngOnChanges(changes: SimpleChanges) {
-        if ('selection' in changes) {
-            this.selection$.next(this.selection);
-        }
-    }
-
     ngOnInit(): void {
         const actionsForLocation = this.bulkActionRegistryService.getBulkActionsForLocation(this.locationId);
-        this.actions$ = this.selection$.pipe(
+        this.actions$ = this.selectionManager.selectionChanges$.pipe(
             switchMap(selection => {
                 return Promise.all(
                     actionsForLocation.map(async action => {
@@ -101,9 +92,14 @@ export class BulkActionMenuComponent<T = any> implements OnInit, OnChanges, OnDe
             injector: this.injector,
             event,
             route: this.route,
-            selection: this.selection,
+            selection: this.selectionManager.selection,
             hostComponent: this.hostComponent,
-            clearSelection: () => this.clearSelection.next(),
+            clearSelection: () => this.selectionManager.clearSelection(),
         });
     }
+
+    clearSelection() {
+        this.selectionManager.clearSelection();
+        this.changeDetectorRef.markForCheck();
+    }
 }

+ 11 - 8
packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.html

@@ -1,16 +1,19 @@
 <ng-container *ngIf="!items || (items && items.length); else emptyPlaceholder">
+    <div class="bulk-actions">
+    <ng-content select="vdr-bulk-action-menu"></ng-content>
+    </div>
     <table class="table" [class.no-select]="disableSelect">
-        <thead>
+        <thead [class.items-selected]="selectionManager?.selection.length">
             <tr>
-                <th *ngIf="isRowSelectedFn" class="align-middle">
+                <th *ngIf="isRowSelectedFn || selectionManager" class="align-middle">
                     <input
                         type="checkbox"
                         clrCheckbox
-                        [checked]="allSelected"
-                        (change)="allSelectChange.emit()"
+                        [checked]="allSelected ? allSelected : selectionManager?.areAllCurrentItemsSelected()"
+                        (change)="onToggleAllClick()"
                     />
                 </th>
-                <th *ngFor="let header of columns?.toArray()" class="left" [class.expand]="header.expand">
+                <th *ngFor="let header of columns?.toArray()" class="left align-middle" [class.expand]="header.expand">
                     <ng-container *ngTemplateOutlet="header.template"></ng-container>
                 </th>
             </tr>
@@ -29,12 +32,12 @@
                     trackBy: trackByFn
                 "
             >
-                <td *ngIf="isRowSelectedFn" class="align-middle selection-col">
+                <td *ngIf="isRowSelectedFn || selectionManager" class="align-middle selection-col">
                     <input
                         type="checkbox"
                         clrCheckbox
-                        [checked]="isRowSelectedFn(item)"
-                        (click)="rowSelectChange.emit({ event: $event, item })"
+                        [checked]="isRowSelectedFn ? isRowSelectedFn(item) : selectionManager?.isSelected(item)"
+                        (click)="onRowClick(item, $event)"
                     />
                 </td>
                 <ng-container

+ 12 - 0
packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.scss

@@ -3,6 +3,14 @@
     display: block;
     max-width: 100%;
     overflow: auto;
+    position: relative;
+}
+
+.bulk-actions {
+    position: absolute;
+    left: 50px;
+    top: 30px;
+    z-index: 2;
 }
 
 table.no-select {
@@ -19,6 +27,10 @@ thead th {
     }
 }
 
+thead.items-selected tr th {
+    color: transparent;
+}
+
 .selection-col {
     width: 24px;
 }

+ 37 - 10
packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.ts

@@ -15,6 +15,9 @@ import {
     TemplateRef,
 } from '@angular/core';
 import { PaginationService } from 'ngx-pagination';
+import { Subscription } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { SelectionManager } from '../../../common/utilities/selection-manager';
 
 import { DataTableColumnComponent } from './data-table-column.component';
 
@@ -89,13 +92,20 @@ export class DataTableComponent<T> implements AfterContentInit, OnChanges, OnIni
     @Input() itemsPerPage: number;
     @Input() currentPage: number;
     @Input() totalItems: number;
+    @Input() emptyStateLabel: string;
+    @Input() selectionManager?: SelectionManager<T>;
+    @Output() pageChange = new EventEmitter<number>();
+    @Output() itemsPerPageChange = new EventEmitter<number>();
+
+    /** @deprecated pass a SelectionManager instance instead */
     @Input() allSelected: boolean;
+    /** @deprecated pass a SelectionManager instance instead */
     @Input() isRowSelectedFn: (item: T) => boolean;
-    @Input() emptyStateLabel: string;
+    /** @deprecated pass a SelectionManager instance instead */
     @Output() allSelectChange = new EventEmitter<void>();
+    /** @deprecated pass a SelectionManager instance instead */
     @Output() rowSelectChange = new EventEmitter<{ event: MouseEvent; item: T }>();
-    @Output() pageChange = new EventEmitter<number>();
-    @Output() itemsPerPageChange = new EventEmitter<number>();
+
     @ContentChildren(DataTableColumnComponent) columns: QueryList<DataTableColumnComponent>;
     @ContentChildren(TemplateRef) templateRefs: QueryList<TemplateRef<any>>;
     rowTemplate: TemplateRef<any>;
@@ -104,6 +114,7 @@ export class DataTableComponent<T> implements AfterContentInit, OnChanges, OnIni
     // This is used to apply a `user-select: none` CSS rule to the table,
     // which allows shift-click multi-row selection
     disableSelect = false;
+    private subscription: Subscription | undefined;
 
     constructor(private changeDetectorRef: ChangeDetectorRef) {}
 
@@ -122,17 +133,30 @@ export class DataTableComponent<T> implements AfterContentInit, OnChanges, OnIni
     };
 
     ngOnInit() {
-        if (typeof this.isRowSelectedFn === 'function') {
+        if (typeof this.isRowSelectedFn === 'function' || this.selectionManager) {
             document.addEventListener('keydown', this.shiftDownHandler, { passive: true });
             document.addEventListener('keyup', this.shiftUpHandler, { passive: true });
         }
+
+        this.subscription = this.selectionManager?.selectionChanges$.subscribe(() =>
+            this.changeDetectorRef.markForCheck(),
+        );
+    }
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (changes.items) {
+            this.currentStart = this.itemsPerPage * (this.currentPage - 1);
+            this.currentEnd = this.currentStart + changes.items.currentValue?.length;
+            this.selectionManager?.setCurrentItems(this.items);
+        }
     }
 
     ngOnDestroy() {
-        if (typeof this.isRowSelectedFn === 'function') {
+        if (typeof this.isRowSelectedFn === 'function' || this.selectionManager) {
             document.removeEventListener('keydown', this.shiftDownHandler);
             document.removeEventListener('keyup', this.shiftUpHandler);
         }
+        this.subscription?.unsubscribe();
     }
 
     ngAfterContentInit(): void {
@@ -147,10 +171,13 @@ export class DataTableComponent<T> implements AfterContentInit, OnChanges, OnIni
         }
     }
 
-    ngOnChanges(changes: SimpleChanges) {
-        if (changes.items) {
-            this.currentStart = this.itemsPerPage * (this.currentPage - 1);
-            this.currentEnd = this.currentStart + changes.items.currentValue?.length;
-        }
+    onToggleAllClick() {
+        this.allSelectChange.emit();
+        this.selectionManager?.toggleSelectAll();
+    }
+
+    onRowClick(item: T, event: MouseEvent) {
+        this.rowSelectChange.emit({ event, item });
+        this.selectionManager?.toggleSelection(item, event);
     }
 }

+ 6 - 3
packages/admin-ui/src/lib/customer/src/components/customer-group-member-list/customer-group-member-list.component.ts

@@ -19,6 +19,11 @@ export interface CustomerGroupMemberFetchParams {
     filterTerm: string;
 }
 
+type CustomerGroupMember = Pick<
+    Customer,
+    'id' | 'createdAt' | 'updatedAt' | 'title' | 'firstName' | 'lastName' | 'emailAddress'
+>;
+
 @Component({
     selector: 'vdr-customer-group-member-list',
     templateUrl: './customer-group-member-list.component.html',
@@ -26,9 +31,7 @@ export interface CustomerGroupMemberFetchParams {
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class CustomerGroupMemberListComponent implements OnInit, OnDestroy {
-    @Input() members: Array<
-        Pick<Customer, 'id' | 'createdAt' | 'updatedAt' | 'title' | 'firstName' | 'lastName' | 'emailAddress'>
-    >;
+    @Input() members: CustomerGroupMember[];
     @Input() totalItems: number;
     @Input() route: ActivatedRoute;
     @Input() selectedMemberIds: string[] = [];