Parcourir la source

feat(admin-ui): Enable stock location selection when creating new Product

Michael Bromley il y a 2 ans
Parent
commit
bb741401b6
20 fichiers modifiés avec 140 ajouts et 80 suppressions
  1. 17 17
      packages/admin-ui/i18n-coverage.json
  2. 64 57
      packages/admin-ui/src/lib/catalog/src/components/generate-product-variants/generate-product-variants.component.html
  3. 29 2
      packages/admin-ui/src/lib/catalog/src/components/generate-product-variants/generate-product-variants.component.ts
  4. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts
  5. 14 2
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts
  6. 1 0
      packages/admin-ui/src/lib/core/src/common/base-detail.component.ts
  7. 1 1
      packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.scss
  8. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  9. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  10. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  11. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  12. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  13. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  14. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  15. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  16. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  17. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  18. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  19. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  20. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

+ 17 - 17
packages/admin-ui/i18n-coverage.json

@@ -1,69 +1,69 @@
 {
-  "generatedOn": "2023-06-04T19:41:40.876Z",
-  "lastCommit": "4a2609b8e17a943077a68447620d46b98c73ea00",
+  "generatedOn": "2023-06-05T19:19:39.286Z",
+  "lastCommit": "f90b244160d4bfe8f4c00c69f2ec6d69de5861df",
   "translationStatus": {
     "cs": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 544,
-      "percentage": 75
+      "percentage": 74
     },
     "de": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 527,
       "percentage": 72
     },
     "en": {
-      "tokenCount": 730,
-      "translatedCount": 729,
+      "tokenCount": 731,
+      "translatedCount": 730,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 569,
       "percentage": 78
     },
     "fr": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 564,
       "percentage": 77
     },
     "it": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 568,
       "percentage": 78
     },
     "pl": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 378,
       "percentage": 52
     },
     "pt_BR": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 542,
       "percentage": 74
     },
     "pt_PT": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 577,
       "percentage": 79
     },
     "ru": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 567,
       "percentage": 78
     },
     "uk": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 567,
       "percentage": 78
     },
     "zh_Hans": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 513,
       "percentage": 70
     },
     "zh_Hant": {
-      "tokenCount": 730,
+      "tokenCount": 731,
       "translatedCount": 358,
       "percentage": 49
     }

+ 64 - 57
packages/admin-ui/src/lib/catalog/src/components/generate-product-variants/generate-product-variants.component.html

@@ -24,7 +24,7 @@
     </div>
     <div class="remove-group">
         <button
-            class="btn btn-icon btn-warning-outline"
+            class="button-small mt-2"
             [title]="'catalog.remove-option' | translate"
             (click)="removeOption(group.name)"
         >
@@ -32,64 +32,71 @@
         </button>
     </div>
 </div>
-<button class="btn btn-primary-outline btn-sm" (click)="addOption()">
+<button class="button mb-2" (click)="addOption()">
     <clr-icon shape="plus"></clr-icon>
     {{ 'catalog.add-option' | translate }}
 </button>
 
-<div class="variants-preview">
-    <table class="table">
-        <thead>
-            <tr>
-                <th *ngIf="1 < variants.length">{{ 'common.create' | translate }}</th>
-                <th *ngIf="1 < variants.length">{{ 'catalog.variant' | translate }}</th>
-                <th>{{ 'catalog.sku' | translate }}</th>
-                <th>{{ 'catalog.price' | translate }}</th>
-                <th>{{ 'catalog.stock-on-hand' | translate }}</th>
+<ng-container *ngIf="stockLocations$ | async as stockLocations">
+    <clr-alert *ngIf="stockLocations.length === 0" clrAlertType="warning" [clrAlertClosable]="false" class="">
+        <clr-alert-item>
+            <span class="alert-text">
+                {{ 'catalog.no-stock-locations-available-on-current-channel' | translate }}
+            </span>
+        </clr-alert-item>
+    </clr-alert>
+
+    <div class="form-grid mb-2">
+        <vdr-form-field *ngIf="stockLocations.length" [label]="'catalog.add-stock-to-location'">
+            <select [(ngModel)]="selectedStockLocationId">
+                <option *ngFor="let location of stockLocations" [value]="location.id">
+                    {{ location.name }}
+                </option>
+            </select>
+        </vdr-form-field>
+    </div>
+
+    <div class="variants-preview" *ngIf="0 < stockLocations.length">
+        <table class="table">
+            <thead>
+                <tr>
+                    <th *ngIf="1 < variants.length">{{ 'common.create' | translate }}</th>
+                    <th *ngIf="1 < variants.length">{{ 'catalog.variant' | translate }}</th>
+                    <th>{{ 'catalog.sku' | translate }}</th>
+                    <th>{{ 'catalog.price' | translate }}</th>
+                    <th>{{ 'catalog.stock-on-hand' | translate }}</th>
+                </tr>
+            </thead>
+            <tr
+                *ngFor="let variant of variants; trackBy: trackByFn"
+                [class.disabled]="!variantFormValues[variant.id].value.enabled === false"
+                [formGroup]="variantFormValues[variant.id]"
+            >
+                <td *ngIf="1 < variants.length">
+                    <input type="checkbox" formControlName="enabled" clrCheckbox />
+                </td>
+                <td *ngIf="1 < variants.length">
+                    {{ variant.values.join(' ') }}
+                </td>
+                <td>
+                    <vdr-form-field>
+                        <input type="text" formControlName="sku" [placeholder]="'catalog.sku' | translate" />
+                    </vdr-form-field>
+                </td>
+                <td>
+                    <vdr-form-field>
+                        <vdr-currency-input
+                            formControlName="price"
+                            [currencyCode]="currencyCode"
+                        ></vdr-currency-input>
+                    </vdr-form-field>
+                </td>
+                <td>
+                    <vdr-form-field>
+                        <input type="number" formControlName="stock" min="0" step="1" />
+                    </vdr-form-field>
+                </td>
             </tr>
-        </thead>
-        <tr
-            *ngFor="let variant of variants; trackBy: trackByFn"
-            [class.disabled]="!variantFormValues[variant.id].value.enabled === false"
-            [formGroup]="variantFormValues[variant.id]"
-        >
-            <td *ngIf="1 < variants.length">
-                <input
-                    type="checkbox"
-                    formControlName="enabled"
-                    clrCheckbox
-                />
-            </td>
-            <td *ngIf="1 < variants.length">
-                {{ variant.values.join(' ') }}
-            </td>
-            <td>
-                <vdr-form-field>
-                    <input
-                        type="text"
-                        formControlName="sku"
-                        [placeholder]="'catalog.sku' | translate"
-                    />
-                </vdr-form-field>
-            </td>
-            <td>
-                <vdr-form-field>
-                    <vdr-currency-input
-                        formControlName="price"
-                        [currencyCode]="currencyCode"
-                    ></vdr-currency-input>
-                </vdr-form-field>
-            </td>
-            <td>
-                <vdr-form-field>
-                    <input
-                        type="number"
-                        formControlName="stock"
-                        min="0"
-                        step="1"
-                    />
-                </vdr-form-field>
-            </td>
-        </tr>
-    </table>
-</div>
+        </table>
+    </div>
+</ng-container>

+ 29 - 2
packages/admin-ui/src/lib/catalog/src/components/generate-product-variants/generate-product-variants.component.ts

@@ -1,7 +1,15 @@
 import { Component, ElementRef, EventEmitter, OnInit, Output, QueryList, ViewChildren } from '@angular/core';
 import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';
-import { CurrencyCode, DataService } from '@vendure/admin-ui/core';
+import {
+    CurrencyCode,
+    DataService,
+    GetStockLocationListDocument,
+    GetStockLocationListQuery,
+    ItemOf,
+} from '@vendure/admin-ui/core';
 import { generateAllCombinations } from '@vendure/common/lib/shared-utils';
+import { Observable } from 'rxjs';
+import { tap } from 'rxjs/operators';
 
 import { OptionValueInputComponent } from '../option-value-input/option-value-input.component';
 
@@ -16,6 +24,7 @@ export type CreateVariantValues = {
 export type CreateProductVariantsConfig = {
     groups: Array<{ name: string; values: string[] }>;
     variants: CreateVariantValues[];
+    stockLocationId: string;
 };
 
 @Component({
@@ -38,12 +47,29 @@ export class GenerateProductVariantsComponent implements OnInit {
             stock: FormControl<number>;
         }>;
     } = {};
+    stockLocations$: Observable<Array<ItemOf<GetStockLocationListQuery, 'stockLocations'>>>;
+    selectedStockLocationId: string | null = null;
     constructor(private dataService: DataService, private formBuilder: FormBuilder) {}
 
     ngOnInit() {
         this.dataService.settings.getActiveChannel().single$.subscribe(data => {
             this.currencyCode = data.activeChannel.defaultCurrencyCode;
         });
+        this.stockLocations$ = this.dataService
+            .query(GetStockLocationListDocument, {
+                options: {
+                    take: 999,
+                },
+            })
+            .refetchOnChannelChange()
+            .mapStream(({ stockLocations }) => stockLocations.items)
+            .pipe(
+                tap(items => {
+                    if (items.length) {
+                        this.selectedStockLocationId = items[0].id;
+                    }
+                }),
+            );
 
         this.generateVariants();
     }
@@ -80,7 +106,6 @@ export class GenerateProductVariantsComponent implements OnInit {
                 });
                 formGroup.valueChanges.subscribe(() => this.onFormChange());
                 if (index === 0) {
-                    console.log(`registering valueChanges for ${variant.id}`);
                     formGroup.get('price')?.valueChanges.subscribe(value => {
                         this.copyValuesToPristine('price', formGroup.get('price'));
                     });
@@ -125,6 +150,8 @@ export class GenerateProductVariantsComponent implements OnInit {
         this.variantsChange.emit({
             groups: this.optionGroups.map(og => ({ name: og.name, values: og.values.map(v => v.name) })),
             variants: variantsToCreate,
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            stockLocationId: this.selectedStockLocationId!,
         });
     }
 

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -91,7 +91,7 @@ export class ProductDetailComponent
     assetChanges: SelectedAssets = {};
     productChannels$: Observable<ProductDetailFragment['channels']>;
     facetValues$: Observable<ProductDetailFragment['facetValues']>;
-    createVariantsConfig: CreateProductVariantsConfig = { groups: [], variants: [] };
+    createVariantsConfig: CreateProductVariantsConfig = { groups: [], variants: [], stockLocationId: '' };
     public readonly updatePermissions = [Permission.UpdateCatalog, Permission.UpdateProduct];
 
     constructor(

+ 14 - 2
packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts

@@ -83,7 +83,13 @@ export class ProductDetailService {
                     };
                 });
                 const options = optionGroups.map(og => og.options).reduce((flat, o) => [...flat, ...o], []);
-                return this.createProductVariants(createProduct, variants, options, languageCode);
+                return this.createProductVariants(
+                    createProduct,
+                    variants,
+                    options,
+                    languageCode,
+                    createVariantsConfig.stockLocationId,
+                );
             }),
         );
     }
@@ -112,6 +118,7 @@ export class ProductDetailService {
         variantData: Array<{ price: number; sku: string; stock: number; optionIds: string[] }>,
         options: Array<{ id: string; name: string }>,
         languageCode: LanguageCode,
+        stockLocationId: string,
     ) {
         const variants: CreateProductVariantInput[] = variantData.map(v => {
             const name = options.length
@@ -125,13 +132,18 @@ export class ProductDetailService {
                 productId: product.id,
                 price: v.price,
                 sku: v.sku,
-                stockOnHand: v.stock,
                 translations: [
                     {
                         languageCode,
                         name,
                     },
                 ],
+                stockLevels: [
+                    {
+                        stockLocationId,
+                        stockOnHand: v.stock,
+                    },
+                ],
                 optionIds: v.optionIds,
             };
         });

+ 1 - 0
packages/admin-ui/src/lib/core/src/common/base-detail.component.ts

@@ -234,6 +234,7 @@ export function detailComponentWithResolver<
         } else {
             const result$ = dataService
                 .query(config.query, { id })
+                .refetchOnChannelChange()
                 .stream$.pipe(takeUntil(navigateAway$), shareReplay(1));
             const entity$ = result$.pipe(map(result => result[config.entityKey]));
             const entityStream$ = entity$.pipe(filter(notNullOrUndefined));

+ 1 - 1
packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.scss

@@ -34,6 +34,6 @@
     margin: 0 3px;
     overflow: hidden;
     flex: 1;
-    max-width: 100px;
+    white-space: nowrap;
     text-overflow: ellipsis;
 }

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Žádný kanál nevybrán",
     "no-featured-asset": "Žádné zvýrazněné médium",
     "no-selection": "Žádný výběr",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Produkt se nepovedlo odebrat z kanálu",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Kein Kanal ausgewählt",
     "no-featured-asset": "Kein \"Featured Asset\"",
     "no-selection": "Keine Auswahl",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Das Produkt konnte nicht aus dem Kanal entfernt werden",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "No channel selected",
     "no-featured-asset": "No featured asset",
     "no-selection": "No selection",
+    "no-stock-locations-available-on-current-channel": "No stock locations are available on the current channel. Set up at least one stock location before adding products.",
     "notify-bulk-delete-products-success": "Successfully deleted {count, plural, one {1 product} other {{count} products}}",
     "notify-remove-facets-from-channel-success": "Successfully removed {count, plural, one {1 facet} other {{count} facets}} from { channelCode }",
     "notify-remove-product-from-channel-error": "Could not remove product from channel",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Ninún canal seleccionado",
     "no-featured-asset": "Sin recurso destacado",
     "no-selection": "Sin selección",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "No fue posible eliminar el producto del canal",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Pas de canal sélectionné",
     "no-featured-asset": "Pas de fichier vedette",
     "no-selection": "Pas de sélection",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Retrait du produit du canal échoué",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Nessun canale selezionato",
     "no-featured-asset": "Nessun media in evidenza",
     "no-selection": "Nessuna selezione",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Impossibile rimuovere il prodotto dal canale",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Brak zaznaczonego kanału",
     "no-featured-asset": "Brak polecanego zasobu",
     "no-selection": "Brak zaznaczenia",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Błąd usuwania produktu z kanału",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Nenhum canal selecionado",
     "no-featured-asset": "Nenhum recurso em destaque",
     "no-selection": "Nenhuma seleção",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Não foi possível remover o produto do canal",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Nenhum canal seleccionado",
     "no-featured-asset": "Nenhum recurso em destaque",
     "no-selection": "Nenhuma imagem seleccionada",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Não foi possível remover o produto do canal",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Канал не выбран",
     "no-featured-asset": "Нет избранного медиа-объекта",
     "no-selection": "Не выбрано",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Не удалось удалить товар из канала",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "Канал не вибрано",
     "no-featured-asset": "Немає обраного медіа-об'єкта",
     "no-selection": "Не вибрано",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "Не вдалося видалити товар з каналу",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "未选择销售渠道",
     "no-featured-asset": "无特征图片",
     "no-selection": "尚未选择",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "从渠道中移除商品失败",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -118,6 +118,7 @@
     "no-channel-selected": "並未選擇渠道",
     "no-featured-asset": "並無特徵圖片",
     "no-selection": "尚未選擇",
+    "no-stock-locations-available-on-current-channel": "",
     "notify-bulk-delete-products-success": "",
     "notify-remove-facets-from-channel-success": "",
     "notify-remove-product-from-channel-error": "從渠道中移除商品失敗",