Browse Source

feat(admin-ui): Implement Asset deletion UI

Closes #285
Michael Bromley 5 years ago
parent
commit
4912a2963f

+ 2 - 0
packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.html

@@ -20,6 +20,8 @@
 <vdr-asset-gallery
 <vdr-asset-gallery
     [assets]="(items$ | async)! | paginate: (paginationConfig$ | async) || {}"
     [assets]="(items$ | async)! | paginate: (paginationConfig$ | async) || {}"
     [multiSelect]="true"
     [multiSelect]="true"
+    [canDelete]="'DeleteCatalog' | hasPermission"
+    (deleteAsset)="deleteAsset($event)"
 ></vdr-asset-gallery>
 ></vdr-asset-gallery>
 
 
 <div class="paging-controls">
 <div class="paging-controls">

+ 59 - 9
packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.ts

@@ -2,11 +2,19 @@ import { Component, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { BaseListComponent, DataService, GetAssetList, NotificationService } from '@vendure/admin-ui/core';
+import {
+    Asset,
+    BaseListComponent,
+    DataService,
+    DeletionResult,
+    GetAssetList,
+    ModalService,
+    NotificationService,
+} from '@vendure/admin-ui/core';
 import { SortOrder } from '@vendure/common/lib/generated-shop-types';
 import { SortOrder } from '@vendure/common/lib/generated-shop-types';
 import { PaginationInstance } from 'ngx-pagination';
 import { PaginationInstance } from 'ngx-pagination';
-import { combineLatest, Observable } from 'rxjs';
-import { debounceTime, map, takeUntil } from 'rxjs/operators';
+import { combineLatest, EMPTY, Observable } from 'rxjs';
+import { debounceTime, map, switchMap, takeUntil } from 'rxjs/operators';
 
 
 @Component({
 @Component({
     selector: 'vdr-asset-list',
     selector: 'vdr-asset-list',
@@ -20,6 +28,7 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
 
 
     constructor(
     constructor(
         private notificationService: NotificationService,
         private notificationService: NotificationService,
+        private modalService: ModalService,
         private dataService: DataService,
         private dataService: DataService,
         router: Router,
         router: Router,
         route: ActivatedRoute,
         route: ActivatedRoute,
@@ -27,7 +36,7 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
         super(router, route);
         super(router, route);
         super.setQueryFn(
         super.setQueryFn(
             (...args: any[]) => this.dataService.product.getAssetList(...args),
             (...args: any[]) => this.dataService.product.getAssetList(...args),
-            data => data.assets,
+            (data) => data.assets,
             (skip, take) => ({
             (skip, take) => ({
                 options: {
                 options: {
                     skip,
                     skip,
@@ -51,16 +60,13 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
             map(([itemsPerPage, currentPage, totalItems]) => ({ itemsPerPage, currentPage, totalItems })),
             map(([itemsPerPage, currentPage, totalItems]) => ({ itemsPerPage, currentPage, totalItems })),
         );
         );
         this.searchTerm.valueChanges
         this.searchTerm.valueChanges
-            .pipe(
-                debounceTime(250),
-                takeUntil(this.destroy$),
-            )
+            .pipe(debounceTime(250), takeUntil(this.destroy$))
             .subscribe(() => this.refresh());
             .subscribe(() => this.refresh());
     }
     }
 
 
     filesSelected(files: File[]) {
     filesSelected(files: File[]) {
         if (files.length) {
         if (files.length) {
-            this.dataService.product.createAssets(files).subscribe(res => {
+            this.dataService.product.createAssets(files).subscribe((res) => {
                 super.refresh();
                 super.refresh();
                 this.notificationService.success(_('asset.notify-create-assets-success'), {
                 this.notificationService.success(_('asset.notify-create-assets-success'), {
                     count: files.length,
                     count: files.length,
@@ -68,4 +74,48 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
             });
             });
         }
         }
     }
     }
+
+    deleteAsset(asset: Asset) {
+        this.showModalAndDelete(asset.id)
+            .pipe(
+                switchMap((response) => {
+                    if (response.result === DeletionResult.DELETED) {
+                        return [true];
+                    } else {
+                        return this.showModalAndDelete(asset.id, response.message || '').pipe(
+                            map((r) => r.result === DeletionResult.DELETED),
+                        );
+                    }
+                }),
+            )
+            .subscribe(
+                () => {
+                    this.notificationService.success(_('common.notify-delete-success'), {
+                        entity: 'Asset',
+                    });
+                    this.refresh();
+                },
+                (err) => {
+                    this.notificationService.error(_('common.notify-delete-error'), {
+                        entity: 'Asset',
+                    });
+                },
+            );
+    }
+
+    private showModalAndDelete(assetId: string, message?: string) {
+        return this.modalService
+            .dialog({
+                title: _('catalog.confirm-delete-facet'),
+                body: message,
+                buttons: [
+                    { type: 'secondary', label: _('common.cancel') },
+                    { type: 'danger', label: _('common.delete'), returnValue: true },
+                ],
+            })
+            .pipe(
+                switchMap((res) => (res ? this.dataService.product.deleteAsset(assetId, !!message) : EMPTY)),
+                map((res) => res.deleteAsset),
+            );
+    }
 }
 }

+ 20 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -4982,6 +4982,20 @@ export type UpdateAssetMutation = (
   ) }
   ) }
 );
 );
 
 
+export type DeleteAssetMutationVariables = {
+  id: Scalars['ID'];
+  force?: Maybe<Scalars['Boolean']>;
+};
+
+
+export type DeleteAssetMutation = (
+  { __typename?: 'Mutation' }
+  & { deleteAsset: (
+    { __typename?: 'DeletionResponse' }
+    & Pick<DeletionResponse, 'result' | 'message'>
+  ) }
+);
+
 export type SearchProductsQueryVariables = {
 export type SearchProductsQueryVariables = {
   input: SearchInput;
   input: SearchInput;
 };
 };
@@ -6933,6 +6947,12 @@ export namespace UpdateAsset {
   export type UpdateAsset = AssetFragment;
   export type UpdateAsset = AssetFragment;
 }
 }
 
 
+export namespace DeleteAsset {
+  export type Variables = DeleteAssetMutationVariables;
+  export type Mutation = DeleteAssetMutation;
+  export type DeleteAsset = DeleteAssetMutation['deleteAsset'];
+}
+
 export namespace SearchProducts {
 export namespace SearchProducts {
   export type Variables = SearchProductsQueryVariables;
   export type Variables = SearchProductsQueryVariables;
   export type Query = SearchProductsQuery;
   export type Query = SearchProductsQuery;

+ 9 - 0
packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts

@@ -366,6 +366,15 @@ export const UPDATE_ASSET = gql`
     ${ASSET_FRAGMENT}
     ${ASSET_FRAGMENT}
 `;
 `;
 
 
+export const DELETE_ASSET = gql`
+    mutation DeleteAsset($id: ID!, $force: Boolean) {
+        deleteAsset(id: $id, force: $force) {
+            result
+            message
+        }
+    }
+`;
+
 export const SEARCH_PRODUCTS = gql`
 export const SEARCH_PRODUCTS = gql`
     query SearchProducts($input: SearchInput!) {
     query SearchProducts($input: SearchInput!) {
         search(input: $input) {
         search(input: $input) {

+ 10 - 1
packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts

@@ -13,6 +13,7 @@ import {
     CreateProductOptionInput,
     CreateProductOptionInput,
     CreateProductVariantInput,
     CreateProductVariantInput,
     CreateProductVariants,
     CreateProductVariants,
+    DeleteAsset,
     DeleteProduct,
     DeleteProduct,
     DeleteProductVariant,
     DeleteProductVariant,
     GetAsset,
     GetAsset,
@@ -45,6 +46,7 @@ import {
     CREATE_PRODUCT,
     CREATE_PRODUCT,
     CREATE_PRODUCT_OPTION_GROUP,
     CREATE_PRODUCT_OPTION_GROUP,
     CREATE_PRODUCT_VARIANTS,
     CREATE_PRODUCT_VARIANTS,
+    DELETE_ASSET,
     DELETE_PRODUCT,
     DELETE_PRODUCT,
     DELETE_PRODUCT_VARIANT,
     DELETE_PRODUCT_VARIANT,
     GET_ASSET,
     GET_ASSET,
@@ -272,7 +274,7 @@ export class ProductDataService {
 
 
     createAssets(files: File[]) {
     createAssets(files: File[]) {
         return this.baseDataService.mutate<CreateAssets.Mutation, CreateAssets.Variables>(CREATE_ASSETS, {
         return this.baseDataService.mutate<CreateAssets.Mutation, CreateAssets.Variables>(CREATE_ASSETS, {
-            input: files.map(file => ({ file })),
+            input: files.map((file) => ({ file })),
         });
         });
     }
     }
 
 
@@ -282,6 +284,13 @@ export class ProductDataService {
         });
         });
     }
     }
 
 
+    deleteAsset(id: string, force: boolean) {
+        return this.baseDataService.mutate<DeleteAsset.Mutation, DeleteAsset.Variables>(DELETE_ASSET, {
+            id,
+            force,
+        });
+    }
+
     assignProductsToChannel(input: AssignProductsToChannelInput) {
     assignProductsToChannel(input: AssignProductsToChannelInput) {
         return this.baseDataService.mutate<
         return this.baseDataService.mutate<
             AssignProductsToChannel.Mutation,
             AssignProductsToChannel.Mutation,

+ 7 - 2
packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.html

@@ -39,14 +39,19 @@
             </div>
             </div>
             <div>
             <div>
                 <button (click)="previewAsset(lastSelected())" class="btn btn-link">
                 <button (click)="previewAsset(lastSelected())" class="btn btn-link">
-                    {{ 'asset.preview' | translate }}
+                    <clr-icon shape="eye"></clr-icon> {{ 'asset.preview' | translate }}
                 </button>
                 </button>
             </div>
             </div>
             <div>
             <div>
                 <a [routerLink]="['./', lastSelected().id]" class="btn btn-link">
                 <a [routerLink]="['./', lastSelected().id]" class="btn btn-link">
-                    {{ 'common.edit' | translate }}
+                    <clr-icon shape="pencil"></clr-icon> {{ 'common.edit' | translate }}
                 </a>
                 </a>
             </div>
             </div>
+            <div *ngIf="selection.length === 1 && canDelete">
+                <button (click)="deleteAsset.emit(lastSelected())" class="btn btn-link">
+                    <clr-icon shape="trash" class="is-danger"></clr-icon> {{ 'common.delete' | translate }}
+                </button>
+            </div>
         </div>
         </div>
     </div>
     </div>
     <div class="card stack" [class.visible]="selection.length > 1"></div>
     <div class="card stack" [class.visible]="selection.length > 1"></div>

+ 5 - 3
packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.ts

@@ -16,7 +16,9 @@ export class AssetGalleryComponent implements OnChanges {
      * If true, allows multiple assets to be selected by ctrl+clicking.
      * If true, allows multiple assets to be selected by ctrl+clicking.
      */
      */
     @Input() multiSelect = false;
     @Input() multiSelect = false;
+    @Input() canDelete = false;
     @Output() selectionChange = new EventEmitter<Asset[]>();
     @Output() selectionChange = new EventEmitter<Asset[]>();
+    @Output() deleteAsset = new EventEmitter<Asset>();
 
 
     selection: Asset[] = [];
     selection: Asset[] = [];
 
 
@@ -26,7 +28,7 @@ export class AssetGalleryComponent implements OnChanges {
         if (this.assets) {
         if (this.assets) {
             for (const asset of this.selection) {
             for (const asset of this.selection) {
                 // Update and selected assets with any changes
                 // Update and selected assets with any changes
-                const match = this.assets.find(a => a.id === asset.id);
+                const match = this.assets.find((a) => a.id === asset.id);
                 if (match) {
                 if (match) {
                     Object.assign(asset, match);
                     Object.assign(asset, match);
                 }
                 }
@@ -35,7 +37,7 @@ export class AssetGalleryComponent implements OnChanges {
     }
     }
 
 
     toggleSelection(event: MouseEvent, asset: Asset) {
     toggleSelection(event: MouseEvent, asset: Asset) {
-        const index = this.selection.findIndex(a => a.id === asset.id);
+        const index = this.selection.findIndex((a) => a.id === asset.id);
         if (index === -1) {
         if (index === -1) {
             if (this.multiSelect && event.ctrlKey) {
             if (this.multiSelect && event.ctrlKey) {
                 this.selection.push(asset);
                 this.selection.push(asset);
@@ -55,7 +57,7 @@ export class AssetGalleryComponent implements OnChanges {
     }
     }
 
 
     isSelected(asset: Asset): boolean {
     isSelected(asset: Asset): boolean {
-        return !!this.selection.find(a => a.id === asset.id);
+        return !!this.selection.find((a) => a.id === asset.id);
     }
     }
 
 
     lastSelected(): Asset {
     lastSelected(): Asset {