Parcourir la source

feat(admin-ui): Persist changes to category tree, simplify list comp

Ran into a whole host of problems to do with caching and UI updating when trying to implement the "batch all changes" method of updating the tree. For now, each change will immediately be persisted to the server. Can be revisited in future.
Michael Bromley il y a 7 ans
Parent
commit
64ce452626

+ 1 - 1
admin-ui/src/app/catalog/components/product-category-list/product-category-list.component.html

@@ -8,6 +8,6 @@
 </vdr-action-bar>
 
 <vdr-product-category-tree
-    [productCategories]="categories$ | async"
+    [productCategories]="items$ | async"
     (rearrange)="onRearrange($event)"
 ></vdr-product-category-tree>

+ 24 - 38
admin-ui/src/app/catalog/components/product-category-list/product-category-list.component.ts

@@ -1,10 +1,12 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
-import { Observable, Subject } from 'rxjs';
-import { scan, startWith, switchMap } from 'rxjs/operators';
-import { GetProductCategoryList } from 'shared/generated-types';
+import { merge, Observable, Subject } from 'rxjs';
+import { debounceTime, scan, startWith, switchMap, tap } from 'rxjs/operators';
+import { GetProductCategoryList, MoveProductCategoryInput } from 'shared/generated-types';
 
 import { BaseListComponent } from '../../../common/base-list.component';
+import { _ } from '../../../core/providers/i18n/mark-for-extraction';
+import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { DataService } from '../../../data/providers/data.service';
 import { RearrangeEvent } from '../product-category-tree/product-category-tree.component';
 
@@ -14,13 +16,16 @@ import { RearrangeEvent } from '../product-category-tree/product-category-tree.c
     styleUrls: ['./product-category-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ProductCategoryListComponent
-    extends BaseListComponent<GetProductCategoryList.Query, GetProductCategoryList.Items>
-    implements OnInit {
-    categories$: Observable<GetProductCategoryList.Items[]>;
-    private rearrange = new Subject<RearrangeEvent>();
-
-    constructor(private dataService: DataService, router: Router, route: ActivatedRoute) {
+export class ProductCategoryListComponent extends BaseListComponent<
+    GetProductCategoryList.Query,
+    GetProductCategoryList.Items
+> {
+    constructor(
+        private dataService: DataService,
+        private notificationService: NotificationService,
+        router: Router,
+        route: ActivatedRoute,
+    ) {
         super(router, route);
         super.setQueryFn(
             (...args: any[]) => this.dataService.product.getProductCategories(99999, 0),
@@ -28,34 +33,15 @@ export class ProductCategoryListComponent
         );
     }
 
-    ngOnInit() {
-        super.ngOnInit();
-        this.categories$ = this.items$.pipe(
-            switchMap(items => {
-                return this.rearrange.pipe(
-                    startWith({} as any),
-                    scan<RearrangeEvent, GetProductCategoryList.Items[]>((acc, event) => {
-                        const itemIndex = acc.findIndex(item => item.id === event.categoryId);
-                        if (-1 < itemIndex) {
-                            let newIndex = 0;
-                            if (0 < event.index) {
-                                const priorItem = acc.filter(item => item.parent.id === event.parentId)[
-                                    event.index - 1
-                                ];
-                                newIndex = acc.indexOf(priorItem) + 1;
-                            }
-                            acc[itemIndex].parent = { ...acc[itemIndex].parent, id: event.parentId };
-                            acc.splice(newIndex, 0, acc.splice(itemIndex, 1)[0]);
-                            return [...acc];
-                        }
-                        return acc;
-                    }, items),
-                );
-            }),
-        );
-    }
-
     onRearrange(event: RearrangeEvent) {
-        this.rearrange.next(event);
+        this.dataService.product.moveProductCategory([event]).subscribe({
+            next: () => {
+                this.notificationService.success(_('common.notify-saved-changes'));
+                this.refresh();
+            },
+            error: err => {
+                this.notificationService.error(_('common.notify-save-changes-error'));
+            },
+        });
     }
 }

+ 31 - 4
admin-ui/src/app/catalog/components/product-category-tree/product-category-tree-node.component.html

@@ -6,9 +6,17 @@
     [cdkDropListDisabled]="true"
     (cdkDropListDropped)="drop($event)"
 >
-    <div class="category" *ngFor="let category of categoryTree.children" cdkDrag [cdkDragData]="category">
+    <div
+        class="category"
+        *ngFor="let category of categoryTree.children; index as i"
+        cdkDrag
+        [cdkDragData]="category"
+    >
         <div class="category-detail" [ngClass]="'depth-' + depth">
-            <div class="name">{{ category.name }}</div>
+            <div class="name">
+                <clr-icon shape="folder"></clr-icon>
+                {{ category.name }}
+            </div>
             <div class="flex-spacer"></div>
             <a class="btn btn-link btn-sm" [routerLink]="['./', category.id]">
                 <clr-icon shape="edit"></clr-icon>
@@ -25,13 +33,32 @@
                     <clr-icon shape="ellipsis-vertical"></clr-icon>
                 </span>
                 <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
-                    <h4 class="dropdown-header">Move to:</h4>
                     <button
                         type="button"
                         class="dropdown-item"
-                        (click)="move(category, item.id)"
+                        [disabled]="i === 0"
+                        (click)="moveUp(category, i)"
+                    >
+                        <clr-icon shape="caret up"></clr-icon>
+                        {{ 'catalog.move-up' | translate }}
+                    </button>
+                    <button
+                        type="button"
+                        class="dropdown-item"
+                        [disabled]="i === categoryTree.children.length - 1"
+                        (click)="moveDown(category, i)"
+                    >
+                        <clr-icon shape="caret down"></clr-icon>
+                        {{ 'catalog.move-down' | translate }}
+                    </button>
+                    <h4 class="dropdown-header">{{ 'catalog.move-to' | translate }}</h4>
+                    <button
+                        type="button"
+                        class="dropdown-item"
                         *ngFor="let item of getMoveListItems(category)"
+                        (click)="move(category, item.id)"
                     >
+                        <clr-icon shape="child-arrow"></clr-icon>
                         {{ item.path }}
                     </button>
                 </clr-dropdown-menu>

+ 17 - 1
admin-ui/src/app/catalog/components/product-category-tree/product-category-tree-node.component.ts

@@ -38,7 +38,7 @@ export class ProductCategoryTreeNodeComponent implements OnInit {
             if (node.id !== category.id) {
                 const path = parentPath.concat(node.name);
                 if (node.id !== category.parent.id) {
-                    output.push({ path: path.join(' / ') || 'root', id: node.id });
+                    output.push({ path: path.slice(1).join(' / ') || 'root', id: node.id });
                 }
                 node.children.forEach(child => visit(child, path, output));
             }
@@ -55,6 +55,22 @@ export class ProductCategoryTreeNodeComponent implements OnInit {
         });
     }
 
+    moveUp(category: ProductCategory.Fragment, currentIndex: number) {
+        this.root.onMove({
+            index: currentIndex - 1,
+            parentId: category.parent.id,
+            categoryId: category.id,
+        });
+    }
+
+    moveDown(category: ProductCategory.Fragment, currentIndex: number) {
+        this.root.onMove({
+            index: currentIndex + 1,
+            parentId: category.parent.id,
+            categoryId: category.id,
+        });
+    }
+
     drop(event: CdkDragDrop<ProductCategory.Fragment | RootNode<ProductCategory.Fragment>>) {
         this.root.onDrop(event);
     }

+ 17 - 0
admin-ui/src/app/data/providers/product-data.service.ts

@@ -1,3 +1,5 @@
+import { forkJoin, from } from 'rxjs';
+import { bufferCount, concatMap, mergeMap } from 'rxjs/operators';
 import {
     AddOptionGroupToProduct,
     ApplyFacetValuesToProductVariants,
@@ -15,6 +17,8 @@ import {
     GetProductList,
     GetProductOptionGroups,
     GetProductWithVariants,
+    MoveProductCategory,
+    MoveProductCategoryInput,
     RemoveOptionGroupFromProduct,
     UpdateProduct,
     UpdateProductCategory,
@@ -40,6 +44,7 @@ import {
     GET_PRODUCT_LIST,
     GET_PRODUCT_OPTION_GROUPS,
     GET_PRODUCT_WITH_VARIANTS,
+    MOVE_PRODUCT_CATEGORY,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_CATEGORY,
@@ -207,4 +212,16 @@ export class ProductDataService {
             },
         );
     }
+
+    moveProductCategory(inputs: MoveProductCategoryInput[]) {
+        return from(inputs).pipe(
+            concatMap(input =>
+                this.baseDataService.mutate<MoveProductCategory.Mutation, MoveProductCategory.Variables>(
+                    MOVE_PRODUCT_CATEGORY,
+                    { input },
+                ),
+            ),
+            bufferCount(inputs.length),
+        );
+    }
 }

+ 6 - 0
admin-ui/src/i18n-messages/en.json

@@ -40,6 +40,9 @@
     "generate-product-variants": "Generate product variants",
     "generate-variants-default-only": "This product does not have options",
     "generate-variants-with-options": "This product has options",
+    "move-down": "Move down",
+    "move-to": "Move to",
+    "move-up": "Move up",
     "no-facets": "No facets",
     "no-featured-asset": "No featured asset",
     "no-selection": "No selection",
@@ -95,11 +98,14 @@
     "next": "Next",
     "notify-create-error": "An error occurred, could not create { entity }",
     "notify-create-success": "Created new { entity }",
+    "notify-save-changes-error": "An error occurred, could not save changes",
+    "notify-saved-changes": "Saved changes",
     "notify-update-error": "An error occurred, could not update { entity }",
     "notify-update-success": "Updated { entity }",
     "password": "Password",
     "remember-me": "Remember me",
     "remove": "Remove",
+    "save-changes": "Save changes",
     "select": "Select...",
     "update": "Update",
     "updated": "Updated",