Browse Source

feat(admin-ui): Multiple asset uploads, improve asset gallery

Michael Bromley 7 years ago
parent
commit
80582c4309

+ 11 - 0
admin-ui/src/app/catalog/components/asset-gallery/asset-gallery.component.html

@@ -35,4 +35,15 @@
             </div>
         </div>
     </div>
+    <div class="card stack" [class.visible]="selection.length > 1"></div>
+    <div class="selection-count" [class.visible]="selection.length > 1">
+        <clr-tooltip>
+            <div clrTooltipTrigger class="trigger">{{ 'catalog.assets-selected-count' | translate: { count: selection.length } }}</div>
+            <clr-tooltip-content clrPosition="top-left" clrSize="lg" *clrIfOpen>
+                <ul>
+                    <li *ngFor="let asset of selection">{{ asset.name }}</li>
+                </ul>
+            </clr-tooltip-content>
+        </clr-tooltip>
+    </div>
 </div>

+ 50 - 3
admin-ui/src/app/catalog/components/asset-gallery/asset-gallery.component.scss

@@ -2,6 +2,7 @@
 
 :host {
     display: flex;
+    overflow: hidden;
 }
 
 .gallery {
@@ -10,11 +11,18 @@
     grid-template-columns: repeat(auto-fill, 150px);
     grid-gap: 10px 20px;
     height: 100%;
+    overflow-y: auto;
+    padding-left: 12px;
+    padding-top: 12px;
+
+    .card:hover {
+        box-shadow: 0 0.125rem 0 0 $color-brand;
+        border: 1px solid $color-brand;
+    }
 }
 
-.card:hover {
-    box-shadow: 0 0.125rem 0 0 $color-brand;
-    border: 1px solid $color-brand;
+.card {
+    margin-top: 0;
 }
 
 .selected-checkbox {
@@ -48,6 +56,45 @@
 
 .info-bar {
     width: 25%;
+    padding: 0 6px;
+    overflow-y: auto;
+
+    .card {
+        z-index: 1;
+    }
+
+    .stack {
+        z-index: 0;
+        opacity: 0;
+        transform: perspective(500px) translateZ(0px) translateY(-16px);
+        height: 16px;
+        transition: transform 0.3s, opacity 0s 0.3s;
+        background-color: white;
+
+        &.visible {
+            opacity: 1;
+            transform: perspective(500px) translateZ(-44px) translateY(0px);
+            background-color: $color-grey-1;
+            transition: transform 0.3s, color 0.3s;
+        }
+    }
+
+    .selection-count {
+        opacity: 0;
+        text-align: center;
+        visibility: hidden;
+        transition: opacity 0.3s, visibility 0s 0.3s;
+        .trigger {
+            cursor: pointer;
+            color: $color-grey-4;
+            text-decoration: underline;
+        }
+        &.visible {
+            opacity: 1;
+            visibility: visible;
+            transition: opacity 0.3s, visibility 0s;
+        }
+    }
 
     .placeholder {
         text-align: center;

+ 9 - 6
admin-ui/src/app/catalog/components/asset-list/asset-list.component.html

@@ -1,14 +1,17 @@
 <vdr-action-bar>
     <vdr-ab-right>
-        <!--<a class="btn btn-primary" [routerLink]="['./create']">
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'catalog.create-new-asset' | translate }}
-        </a>-->
-        <input type="file" (change)="fileSelected($event)">
+        <input type="file"
+               class="file-input"
+               #fileInput
+               (change)="filesSelected($event)" multiple>
+        <button class="btn btn-primary" (click)="fileInput.click()">
+            <clr-icon shape="upload-cloud"></clr-icon>
+            {{ 'catalog.upload-assets' | translate }}
+        </button>
     </vdr-ab-right>
 </vdr-action-bar>
 
-<vdr-asset-gallery [assets]="items$ | async"
+<vdr-asset-gallery [assets]="items$ | async | paginate : (paginationConfig$ | async) || {}"
                    [multiSelect]="true"></vdr-asset-gallery>
 
 <div class="paging-controls">

+ 15 - 2
admin-ui/src/app/catalog/components/asset-list/asset-list.component.scss

@@ -1,8 +1,21 @@
 @import "variables";
 
+:host {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+}
+
+.file-input {
+    display: none;
+}
+
+vdr-asset-gallery {
+    flex: 1;
+}
+
 .paging-controls {
-    margin-top: 24px;
-    padding-top: 24px;
+    padding-top: 6px;
     border-top: 1px solid $color-grey-2;
     display: flex;
     justify-content: space-between;

+ 29 - 7
admin-ui/src/app/catalog/components/asset-list/asset-list.component.ts

@@ -1,8 +1,12 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
+import { PaginationInstance } from 'ngx-pagination';
+import { combineLatest, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
 import { GetAssetList, GetAssetList_assets_items } from 'shared/generated-types';
 
 import { BaseListComponent } from '../../../common/base-list.component';
+import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { DataService } from '../../../data/providers/data.service';
 
 @Component({
@@ -10,8 +14,16 @@ import { DataService } from '../../../data/providers/data.service';
     templateUrl: './asset-list.component.html',
     styleUrls: ['./asset-list.component.scss'],
 })
-export class AssetListComponent extends BaseListComponent<GetAssetList, GetAssetList_assets_items> {
-    constructor(private dataService: DataService, router: Router, route: ActivatedRoute) {
+export class AssetListComponent extends BaseListComponent<GetAssetList, GetAssetList_assets_items>
+    implements OnInit {
+    paginationConfig$: Observable<PaginationInstance>;
+
+    constructor(
+        private notificationService: NotificationService,
+        private dataService: DataService,
+        router: Router,
+        route: ActivatedRoute,
+    ) {
         super(router, route);
         super.setQueryFn(
             (...args: any[]) => this.dataService.product.getAssetList(...args),
@@ -19,11 +31,21 @@ export class AssetListComponent extends BaseListComponent<GetAssetList, GetAsset
         );
     }
 
-    fileSelected(event: Event) {
+    ngOnInit() {
+        super.ngOnInit();
+        this.paginationConfig$ = combineLatest(this.itemsPerPage$, this.currentPage$, this.totalItems$).pipe(
+            map(([itemsPerPage, currentPage, totalItems]) => ({ itemsPerPage, currentPage, totalItems })),
+        );
+    }
+
+    filesSelected(event: Event) {
         const files = (event.target as HTMLInputElement).files;
-        if (files && files.length === 1) {
-            this.dataService.product.createAsset(files[0]).subscribe(res => {
-                // empty
+        if (files) {
+            this.dataService.product.createAssets(Array.from(files)).subscribe(res => {
+                super.refresh();
+                this.notificationService.success('catalog.notify-create-assets-success', {
+                    count: files.length,
+                });
             });
         }
     }

+ 11 - 3
admin-ui/src/app/common/base-list.component.ts

@@ -1,6 +1,6 @@
 import { OnDestroy, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
-import { combineLatest, Observable, Subject } from 'rxjs';
+import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
 import { map, takeUntil } from 'rxjs/operators';
 
 import { QueryResult } from '../data/query-result';
@@ -20,6 +20,7 @@ export class BaseListComponent<ResultType, ItemType> implements OnInit, OnDestro
     private destroy$ = new Subject<void>();
     private listQueryFn: ListQueryFn<ResultType>;
     private mappingFn: MappingFn<ItemType, ResultType>;
+    private refresh$ = new BehaviorSubject<undefined>(undefined);
 
     constructor(private router: Router, private route: ActivatedRoute) {}
 
@@ -39,7 +40,7 @@ export class BaseListComponent<ResultType, ItemType> implements OnInit, OnDestro
         }
         const listQuery = this.listQueryFn(10, 0);
 
-        const fetchPage = ([currentPage, itemsPerPage]: [number, number]) => {
+        const fetchPage = ([currentPage, itemsPerPage, _]: [number, number, undefined]) => {
             const take = itemsPerPage;
             const skip = (currentPage - 1) * itemsPerPage;
             listQuery.ref.refetch({ options: { skip, take } });
@@ -56,7 +57,7 @@ export class BaseListComponent<ResultType, ItemType> implements OnInit, OnDestro
             map(perPage => (!perPage ? 10 : +perPage)),
         );
 
-        combineLatest(this.currentPage$, this.itemsPerPage$)
+        combineLatest(this.currentPage$, this.itemsPerPage$, this.refresh$)
             .pipe(takeUntil(this.destroy$))
             .subscribe(fetchPage);
     }
@@ -74,6 +75,13 @@ export class BaseListComponent<ResultType, ItemType> implements OnInit, OnDestro
         this.setQueryParam('perPage', perPage);
     }
 
+    /**
+     * Re-fetch the current page
+     */
+    refresh() {
+        this.refresh$.next(undefined);
+    }
+
     private setQueryParam(key: string, value: any) {
         this.router.navigate(['./'], {
             queryParams: { [key]: value },

+ 1 - 0
admin-ui/src/app/core/components/main-nav/main-nav.component.html

@@ -19,6 +19,7 @@
                 </li>
                 <li><a class="nav-link"
                        [routerLink]="['/catalog', 'assets']"
+                       [queryParams]="{ perPage: 25 }"
                        routerLinkActive="active">
                     <clr-icon shape="image-gallery" size="20"></clr-icon>{{ 'nav.assets' | translate }}
                 </a>

+ 2 - 2
admin-ui/src/app/data/definitions/product-definitions.ts

@@ -241,8 +241,8 @@ export const GET_ASSET_LIST = gql`
 `;
 
 export const CREATE_ASSET = gql`
-    mutation CreateAsset($input: CreateAssetInput!) {
-        createAsset(input: $input) {
+    mutation CreateAsset($input: [CreateAssetInput!]!) {
+        createAssets(input: $input) {
             ...Asset
         }
     }

+ 1 - 1
admin-ui/src/app/data/providers/data.service.mock.ts

@@ -63,7 +63,7 @@ export class MockDataService implements DataServiceMock {
         generateProductVariants: spyObservable('generateProductVariants'),
         applyFacetValuesToProductVariants: spyObservable('applyFacetValuesToProductVariants'),
         getAssetList: spyQueryResult('getAssetList'),
-        createAsset: spyObservable('createAsset'),
+        createAssets: spyObservable('createAssets'),
     };
     auth = {
         checkLoggedIn: spyObservable('checkLoggedIn'),

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

@@ -183,9 +183,9 @@ export class ProductDataService {
         });
     }
 
-    createAsset(file: File): Observable<CreateAsset> {
+    createAssets(files: File[]): Observable<CreateAsset> {
         return this.baseDataService.mutate<CreateAsset, CreateAssetVariables>(CREATE_ASSET, {
-            input: { file },
+            input: files.map(file => ({ file })),
         });
     }
 }

+ 2 - 1
admin-ui/src/i18n-messages/en.json

@@ -31,9 +31,9 @@
   "catalog": {
     "add-facet-value": "Add facet value",
     "apply-facets": "Apply facets",
+    "assets-selected-count": "{ count } assets selected",
     "confirm-generate-product-variants": "Click 'Finish' to generate {count} product variants.",
     "create-group": "Create option group",
-    "create-new-asset": "Create new asset",
     "create-new-facet": "Create new facet",
     "create-new-option-group": "Create new option group",
     "create-new-product": "Create new product",
@@ -64,6 +64,7 @@
     "sku": "SKU",
     "slug": "Slug",
     "truncated-options-count": "{count} further {count, plural, one {option} other {options}}",
+    "upload-assets": "Upload assets",
     "values": "Values",
     "with-selected": "With selected"
   },

+ 3 - 3
shared/generated-types.ts

@@ -1493,7 +1493,7 @@ export interface GetAssetListVariables {
 // GraphQL mutation operation: CreateAsset
 // ====================================================
 
-export interface CreateAsset_createAsset {
+export interface CreateAsset_createAssets {
   __typename: "Asset";
   id: string;
   name: string;
@@ -1508,11 +1508,11 @@ export interface CreateAsset {
   /**
    * Create a new Asset
    */
-  createAsset: CreateAsset_createAsset;
+  createAssets: CreateAsset_createAssets[];
 }
 
 export interface CreateAssetVariables {
-  input: CreateAssetInput;
+  input: CreateAssetInput[];
 }
 
 /* tslint:disable */