Browse Source

feat(admin-ui): Add asset controls to ProductVariantsList

Relates to #45
Michael Bromley 7 years ago
parent
commit
8cb3d60e28

+ 58 - 37
admin-ui/src/app/catalog/components/product-assets/product-assets.component.html

@@ -1,4 +1,4 @@
-<div class="card">
+<div class="card" *ngIf="!compact; else: compactView">
     <div class="card-img">
         <div class="featured-asset">
             <img *ngIf="featuredAsset" [src]="featuredAsset!.preview + '?preset=small'" />
@@ -8,42 +8,7 @@
             </div>
         </div>
     </div>
-    <div class="card-block">
-        <div class="all-assets">
-            <ng-container *ngFor="let asset of assets">
-                <clr-dropdown>
-                    <div
-                        class="asset-thumb"
-                        clrDropdownTrigger
-                        [class.featured]="isFeatured(asset)"
-                        [title]=""
-                        tabindex="0"
-                    >
-                        <img [src]="asset.preview + '?preset=tiny'" />
-                    </div>
-                    <clr-dropdown-menu *clrIfOpen clrPosition="bottom-right">
-                        <button
-                            type="button"
-                            [disabled]="isFeatured(asset)"
-                            clrDropdownItem
-                            (click)="setAsFeatured(asset)"
-                        >
-                            {{ 'catalog.set-as-featured-asset' | translate }}
-                        </button>
-                        <div class="dropdown-divider"></div>
-                        <button
-                            type="button"
-                            class="remove-asset"
-                            clrDropdownItem
-                            (click)="removeAsset(asset)"
-                        >
-                            {{ 'catalog.remove-asset' | translate }}
-                        </button>
-                    </clr-dropdown-menu>
-                </clr-dropdown>
-            </ng-container>
-        </div>
-    </div>
+    <div class="card-block"><ng-container *ngTemplateOutlet="assetList"></ng-container></div>
     <div class="card-footer">
         <button class="btn" (click)="selectAssets()">
             <clr-icon shape="attachment"></clr-icon>
@@ -51,3 +16,59 @@
         </button>
     </div>
 </div>
+
+<ng-template #compactView>
+    <div class="featured-asset compact">
+        <clr-dropdown *ngIf="featuredAsset">
+            <img clrDropdownTrigger [src]="featuredAsset!.preview + '?preset=thumb'" />
+            <clr-dropdown-menu *clrIfOpen clrPosition="bottom-right">
+                <button
+                    type="button"
+                    class="remove-asset"
+                    clrDropdownItem
+                    (click)="removeAsset(featuredAsset)"
+                >
+                    {{ 'catalog.remove-asset' | translate }}
+                </button>
+            </clr-dropdown-menu>
+        </clr-dropdown>
+
+        <div class="placeholder" *ngIf="!featuredAsset"><clr-icon shape="image" size="64"></clr-icon></div>
+        <button class="compact-select btn btn-icon btn-sm" (click)="selectAssets()">
+            <clr-icon shape="attachment"></clr-icon>
+        </button>
+    </div>
+    <ng-container *ngTemplateOutlet="assetList"></ng-container>
+</ng-template>
+
+<ng-template #assetList>
+    <div class="all-assets">
+        <ng-container *ngFor="let asset of getAssetList()">
+            <clr-dropdown>
+                <div
+                    class="asset-thumb"
+                    clrDropdownTrigger
+                    [class.featured]="isFeatured(asset)"
+                    [title]=""
+                    tabindex="0"
+                >
+                    <img [src]="asset.preview + '?preset=tiny'" />
+                </div>
+                <clr-dropdown-menu *clrIfOpen clrPosition="bottom-right">
+                    <button
+                        type="button"
+                        [disabled]="isFeatured(asset)"
+                        clrDropdownItem
+                        (click)="setAsFeatured(asset)"
+                    >
+                        {{ 'catalog.set-as-featured-asset' | translate }}
+                    </button>
+                    <div class="dropdown-divider"></div>
+                    <button type="button" class="remove-asset" clrDropdownItem (click)="removeAsset(asset)">
+                        {{ 'catalog.remove-asset' | translate }}
+                    </button>
+                </clr-dropdown-menu>
+            </clr-dropdown>
+        </ng-container>
+    </div>
+</ng-template>

+ 17 - 0
admin-ui/src/app/catalog/components/product-assets/product-assets.component.scss

@@ -3,6 +3,9 @@
 :host {
     width: 340px;
     display: block;
+    &.compact {
+        width: 150px;
+    }
 }
 
 .placeholder {
@@ -14,6 +17,20 @@
     text-align: center;
     background: $color-grey-2;
     padding: 6px;
+
+    &.compact {
+        width: 150px;
+        min-height: 60px;
+        padding: 0;
+        position: relative;
+    }
+
+    .compact-select {
+        position: absolute;
+        bottom: 6px;
+        right: 6px;
+        margin: 0;
+    }
 }
 
 .all-assets {

+ 14 - 1
admin-ui/src/app/catalog/components/product-assets/product-assets.component.ts

@@ -3,6 +3,7 @@ import {
     ChangeDetectorRef,
     Component,
     EventEmitter,
+    HostBinding,
     Input,
     Output,
 } from '@angular/core';
@@ -12,6 +13,11 @@ import { unique } from 'shared/unique';
 import { ModalService } from '../../../shared/providers/modal/modal.service';
 import { AssetPickerDialogComponent } from '../asset-picker-dialog/asset-picker-dialog.component';
 
+export interface AssetChange {
+    assetIds: string[];
+    featuredAssetId: string | undefined;
+}
+
 /**
  * A component which displays the Assets associated with a product, and allows assets to be removed and
  * added, and for the featured asset to be set.
@@ -25,7 +31,10 @@ import { AssetPickerDialogComponent } from '../asset-picker-dialog/asset-picker-
 export class ProductAssetsComponent {
     @Input() assets: Asset[] = [];
     @Input() featuredAsset: Asset | undefined;
-    @Output() change = new EventEmitter<{ assetIds: string[]; featuredAssetId: string | undefined }>();
+    @HostBinding('class.compact')
+    @Input()
+    compact = false;
+    @Output() change = new EventEmitter<AssetChange>();
 
     constructor(private modalService: ModalService, private changeDetector: ChangeDetectorRef) {}
 
@@ -34,6 +43,10 @@ export class ProductAssetsComponent {
         return this.assets.filter(a => a.id !== featuredAssetId);
     }
 
+    getAssetList(): Asset[] {
+        return this.compact ? this.nonFeaturedAssets() : this.assets;
+    }
+
     selectAssets() {
         this.modalService
             .fromComponent(AssetPickerDialogComponent, {

+ 6 - 1
admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -20,7 +20,11 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="(productForm.invalid || productForm.pristine) && !assetsChanged()"
+                [disabled]="
+                    (productForm.invalid || productForm.pristine) &&
+                    !assetsChanged() &&
+                    !variantAssetsChanged()
+                "
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -104,6 +108,7 @@
                             [facets]="facets$ | async"
                             [productVariantsFormArray]="productForm.get('variants')"
                             [taxCategories]="taxCategories$ | async"
+                            (assetChange)="variantAssetChange($event)"
                             #productVariantsList
                         >
                             <button

+ 24 - 2
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -25,6 +25,7 @@ import { DataService } from '../../../data/providers/data.service';
 import { ServerConfigService } from '../../../data/server-config';
 import { ModalService } from '../../../shared/providers/modal/modal.service';
 import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
+import { VariantAssetChange } from '../product-variants-list/product-variants-list.component';
 
 export type TabName = 'details' | 'variants';
 export interface VariantFormValue {
@@ -37,6 +38,11 @@ export interface VariantFormValue {
     facetValueIds: string[];
 }
 
+export interface SelectedAssets {
+    assetIds?: string[];
+    featuredAssetId?: string;
+}
+
 @Component({
     selector: 'vdr-product-detail',
     templateUrl: './product-detail.component.html',
@@ -52,7 +58,8 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     customFields: CustomFieldConfig[];
     customVariantFields: CustomFieldConfig[];
     productForm: FormGroup;
-    assetChanges: { assetIds?: string[]; featuredAssetId?: string } = {};
+    assetChanges: SelectedAssets = {};
+    variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
     facets$ = new BehaviorSubject<FacetWithValues.Fragment[]>([]);
 
     constructor(
@@ -112,6 +119,14 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         return !!Object.values(this.assetChanges).length;
     }
 
+    variantAssetsChanged(): boolean {
+        return !!Object.keys(this.variantAssetChanges).length;
+    }
+
+    variantAssetChange(event: VariantAssetChange) {
+        this.variantAssetChanges[event.variantId] = event;
+    }
+
     /**
      * If creating a new product, automatically generate the slug based on the product name.
      */
@@ -193,6 +208,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                         entity: 'Product',
                     });
                     this.assetChanges = {};
+                    this.variantAssetChanges = {};
                     this.productForm.markAsPristine();
                     this.router.navigate(['../', data.createProduct.id], { relativeTo: this.route });
                 },
@@ -223,7 +239,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                         }
                     }
                     const variantsArray = this.productForm.get('variants');
-                    if (variantsArray && variantsArray.dirty) {
+                    if ((variantsArray && variantsArray.dirty) || this.variantAssetsChanged()) {
                         const newVariants = this.getUpdatedProductVariants(
                             product,
                             variantsArray as FormArray,
@@ -239,6 +255,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                 () => {
                     this.productForm.markAsPristine();
                     this.assetChanges = {};
+                    this.variantAssetChanges = {};
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Product',
                     });
@@ -364,6 +381,11 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                 });
                 result.taxCategoryId = formValue.taxCategoryId;
                 result.facetValueIds = formValue.facetValueIds;
+                const assetChanges = this.variantAssetChanges[variant.id];
+                if (assetChanges) {
+                    result.featuredAssetId = assetChanges.featuredAssetId;
+                    result.assetIds = assetChanges.assetIds;
+                }
                 return result;
             })
             .filter(notNullOrUndefined);

+ 56 - 37
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.html

@@ -37,46 +37,65 @@
             </div>
         </div>
         <div class="card-block">
-            <div class="details">
-                <div class="sku">
-                    <clr-input-container>
-                        <label>{{ 'catalog.sku' | translate }}</label>
-                        <input clrInput type="text" [formControl]="formArray.get([i, 'sku'])" />
-                    </clr-input-container>
+            <div class="row">
+                <div class="assets">
+                    <vdr-product-assets
+                        [compact]="true"
+                        [assets]="variant.assets"
+                        [featuredAsset]="variant.featuredAsset"
+                        (change)="onAssetChange(variant.id, $event)"
+                    ></vdr-product-assets>
                 </div>
-                <div class="name">
-                    <clr-input-container>
-                        <label>{{ 'common.name' | translate }}</label>
-                        <input clrInput type="text" [formControl]="formArray.get([i, 'name'])" />
-                    </clr-input-container>
+                <div class="col">
+                    <div class="details">
+                        <div class="sku">
+                            <clr-input-container>
+                                <label>{{ 'catalog.sku' | translate }}</label>
+                                <input clrInput type="text" [formControl]="formArray.get([i, 'sku'])" />
+                            </clr-input-container>
+                        </div>
+                        <div class="name">
+                            <clr-input-container>
+                                <label>{{ 'common.name' | translate }}</label>
+                                <input clrInput type="text" [formControl]="formArray.get([i, 'name'])" />
+                            </clr-input-container>
+                        </div>
+                    </div>
+                    <div class="pricing">
+                        <div class="tax-category">
+                            <clr-select-container>
+                                <label>{{ 'catalog.tax-category' | translate }}</label>
+                                <select
+                                    clrSelect
+                                    name="options"
+                                    [formControl]="formArray.get([i, 'taxCategoryId'])"
+                                >
+                                    <option
+                                        *ngFor="let taxCategory of taxCategories"
+                                        [value]="taxCategory.id"
+                                    >
+                                        {{ taxCategory.name }}
+                                    </option>
+                                </select>
+                            </clr-select-container>
+                        </div>
+                        <div class="price">
+                            <clr-input-container>
+                                <label>{{ 'catalog.price' | translate }}</label>
+                                <vdr-currency-input
+                                    clrInput
+                                    [formControl]="formArray.get([i, 'price'])"
+                                ></vdr-currency-input>
+                            </clr-input-container>
+                        </div>
+                        <vdr-variant-price-detail
+                            [price]="formArray.get([i, 'price'])!.value"
+                            [priceIncludesTax]="variant.priceIncludesTax"
+                            [taxCategoryId]="formArray.get([i, 'taxCategoryId'])!.value"
+                        ></vdr-variant-price-detail>
+                    </div>
                 </div>
             </div>
-            <div class="pricing">
-                <div class="tax-category">
-                    <clr-select-container>
-                        <label>{{ 'catalog.tax-category' | translate }}</label>
-                        <select clrSelect name="options" [formControl]="formArray.get([i, 'taxCategoryId'])">
-                            <option *ngFor="let taxCategory of taxCategories" [value]="taxCategory.id">
-                                {{ taxCategory.name }}
-                            </option>
-                        </select>
-                    </clr-select-container>
-                </div>
-                <div class="price">
-                    <clr-input-container>
-                        <label>{{ 'catalog.price' | translate }}</label>
-                        <vdr-currency-input
-                            clrInput
-                            [formControl]="formArray.get([i, 'price'])"
-                        ></vdr-currency-input>
-                    </clr-input-container>
-                </div>
-                <vdr-variant-price-detail
-                    [price]="formArray.get([i, 'price'])!.value"
-                    [priceIncludesTax]="variant.priceIncludesTax"
-                    [taxCategoryId]="formArray.get([i, 'taxCategoryId'])!.value"
-                ></vdr-variant-price-detail>
-            </div>
         </div>
     </div>
 </div>

+ 8 - 0
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.scss

@@ -36,6 +36,14 @@
         align-items: center;
     }
 
+    .row {
+        display: flex;
+    }
+
+    .assets {
+        margin-right: 12px;
+    }
+
     .details {
         display: flex;
         margin-bottom: 24px;

+ 17 - 0
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.ts

@@ -2,10 +2,12 @@ import {
     ChangeDetectionStrategy,
     ChangeDetectorRef,
     Component,
+    EventEmitter,
     Input,
     OnChanges,
     OnDestroy,
     OnInit,
+    Output,
     SimpleChanges,
 } from '@angular/core';
 import { FormArray } from '@angular/forms';
@@ -14,8 +16,13 @@ import { FacetValue, FacetWithValues, ProductWithVariants, TaxCategory } from 's
 import { notNullOrUndefined } from 'shared/shared-utils';
 
 import { flattenFacetValues } from '../../../common/utilities/flatten-facet-values';
+import { AssetChange } from '../product-assets/product-assets.component';
 import { VariantFormValue } from '../product-detail/product-detail.component';
 
+export interface VariantAssetChange extends AssetChange {
+    variantId: string;
+}
+
 @Component({
     selector: 'vdr-product-variants-list',
     templateUrl: './product-variants-list.component.html',
@@ -27,6 +34,7 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     @Input() variants: ProductWithVariants.Variants[];
     @Input() taxCategories: TaxCategory[];
     @Input() facets: FacetWithValues.Fragment[];
+    @Output() assetChange = new EventEmitter<VariantAssetChange>();
     selectedVariantIds: string[] = [];
     private facetValues: FacetValue.Fragment[];
     private formSubscription: Subscription;
@@ -55,6 +63,15 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
         return !!this.variants && this.selectedVariantIds.length === this.variants.length;
     }
 
+    onAssetChange(variantId: string, event: AssetChange) {
+        this.assetChange.emit({
+            variantId,
+            ...event,
+        });
+        const index = this.variants.findIndex(v => v.id === variantId);
+        this.formArray.at(index).markAsDirty();
+    }
+
     toggleSelectAll() {
         if (this.areAllSelected()) {
             this.selectedVariantIds = [];

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

@@ -45,12 +45,19 @@ export const PRODUCT_VARIANT_FRAGMENT = gql`
                 name
             }
         }
+        featuredAsset {
+            ...Asset
+        }
+        assets {
+            ...Asset
+        }
         translations {
             id
             languageCode
             name
         }
     }
+    ${ASSET_FRAGMENT}
 `;
 
 export const PRODUCT_WITH_VARIANTS_FRAGMENT = gql`

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

@@ -104,7 +104,16 @@ export class ProductDataService {
     updateProductVariants(variants: UpdateProductVariantInput[]) {
         const input: UpdateProductVariants.Variables = {
             input: variants.map(
-                pick(['id', 'translations', 'sku', 'price', 'taxCategoryId', 'facetValueIds']),
+                pick([
+                    'id',
+                    'translations',
+                    'sku',
+                    'price',
+                    'taxCategoryId',
+                    'facetValueIds',
+                    'featuredAssetId',
+                    'assetIds',
+                ]),
             ),
         };
         return this.baseDataService.mutate<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(

+ 6 - 0
shared/generated-types.ts

@@ -6259,6 +6259,8 @@ export namespace ProductVariant {
         sku: string;
         options: Options[];
         facetValues: FacetValues[];
+        featuredAsset?: FeaturedAsset | null;
+        assets: Assets[];
         translations: Translations[];
     };
 
@@ -6297,6 +6299,10 @@ export namespace ProductVariant {
         name: string;
     };
 
+    export type FeaturedAsset = Asset.Fragment;
+
+    export type Assets = Asset.Fragment;
+
     export type Translations = {
         __typename?: 'ProductVariantTranslation';
         id: string;