Selaa lähdekoodia

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 7 vuotta sitten
vanhempi
sitoutus
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-action-bar>
 
 
 <vdr-product-category-tree
 <vdr-product-category-tree
-    [productCategories]="categories$ | async"
+    [productCategories]="items$ | async"
     (rearrange)="onRearrange($event)"
     (rearrange)="onRearrange($event)"
 ></vdr-product-category-tree>
 ></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 { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 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 { 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 { DataService } from '../../../data/providers/data.service';
 import { RearrangeEvent } from '../product-category-tree/product-category-tree.component';
 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'],
     styleUrls: ['./product-category-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
     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(router, route);
         super.setQueryFn(
         super.setQueryFn(
             (...args: any[]) => this.dataService.product.getProductCategories(99999, 0),
             (...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) {
     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"
     [cdkDropListDisabled]="true"
     (cdkDropListDropped)="drop($event)"
     (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="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>
             <div class="flex-spacer"></div>
             <a class="btn btn-link btn-sm" [routerLink]="['./', category.id]">
             <a class="btn btn-link btn-sm" [routerLink]="['./', category.id]">
                 <clr-icon shape="edit"></clr-icon>
                 <clr-icon shape="edit"></clr-icon>
@@ -25,13 +33,32 @@
                     <clr-icon shape="ellipsis-vertical"></clr-icon>
                     <clr-icon shape="ellipsis-vertical"></clr-icon>
                 </span>
                 </span>
                 <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
                 <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
-                    <h4 class="dropdown-header">Move to:</h4>
                     <button
                     <button
                         type="button"
                         type="button"
                         class="dropdown-item"
                         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)"
                         *ngFor="let item of getMoveListItems(category)"
+                        (click)="move(category, item.id)"
                     >
                     >
+                        <clr-icon shape="child-arrow"></clr-icon>
                         {{ item.path }}
                         {{ item.path }}
                     </button>
                     </button>
                 </clr-dropdown-menu>
                 </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) {
             if (node.id !== category.id) {
                 const path = parentPath.concat(node.name);
                 const path = parentPath.concat(node.name);
                 if (node.id !== category.parent.id) {
                 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));
                 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>>) {
     drop(event: CdkDragDrop<ProductCategory.Fragment | RootNode<ProductCategory.Fragment>>) {
         this.root.onDrop(event);
         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 {
 import {
     AddOptionGroupToProduct,
     AddOptionGroupToProduct,
     ApplyFacetValuesToProductVariants,
     ApplyFacetValuesToProductVariants,
@@ -15,6 +17,8 @@ import {
     GetProductList,
     GetProductList,
     GetProductOptionGroups,
     GetProductOptionGroups,
     GetProductWithVariants,
     GetProductWithVariants,
+    MoveProductCategory,
+    MoveProductCategoryInput,
     RemoveOptionGroupFromProduct,
     RemoveOptionGroupFromProduct,
     UpdateProduct,
     UpdateProduct,
     UpdateProductCategory,
     UpdateProductCategory,
@@ -40,6 +44,7 @@ import {
     GET_PRODUCT_LIST,
     GET_PRODUCT_LIST,
     GET_PRODUCT_OPTION_GROUPS,
     GET_PRODUCT_OPTION_GROUPS,
     GET_PRODUCT_WITH_VARIANTS,
     GET_PRODUCT_WITH_VARIANTS,
+    MOVE_PRODUCT_CATEGORY,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_CATEGORY,
     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-product-variants": "Generate product variants",
     "generate-variants-default-only": "This product does not have options",
     "generate-variants-default-only": "This product does not have options",
     "generate-variants-with-options": "This product has 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-facets": "No facets",
     "no-featured-asset": "No featured asset",
     "no-featured-asset": "No featured asset",
     "no-selection": "No selection",
     "no-selection": "No selection",
@@ -95,11 +98,14 @@
     "next": "Next",
     "next": "Next",
     "notify-create-error": "An error occurred, could not create { entity }",
     "notify-create-error": "An error occurred, could not create { entity }",
     "notify-create-success": "Created new { 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-error": "An error occurred, could not update { entity }",
     "notify-update-success": "Updated { entity }",
     "notify-update-success": "Updated { entity }",
     "password": "Password",
     "password": "Password",
     "remember-me": "Remember me",
     "remember-me": "Remember me",
     "remove": "Remove",
     "remove": "Remove",
+    "save-changes": "Save changes",
     "select": "Select...",
     "select": "Select...",
     "update": "Update",
     "update": "Update",
     "updated": "Updated",
     "updated": "Updated",