Răsfoiți Sursa

feat(admin-ui): Improve layout of detail page components

Michael Bromley 2 ani în urmă
părinte
comite
628b50dccc
18 a modificat fișierele cu 557 adăugiri și 104 ștergeri
  1. 2 3
      packages/admin-ui/src/lib/catalog/src/catalog.module.ts
  2. 19 1
      packages/admin-ui/src/lib/catalog/src/components/assets/assets.component.scss
  3. 14 11
      packages/admin-ui/src/lib/catalog/src/components/product-detail2/product-detail.component.html
  4. 120 2
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html
  5. 234 15
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.ts
  6. 80 57
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.graphql.ts
  7. 7 1
      packages/admin-ui/src/lib/core/src/common/base-detail.component.ts
  8. 1 2
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  9. 1 1
      packages/admin-ui/src/lib/core/src/data/providers/data.service.ts
  10. 3 1
      packages/admin-ui/src/lib/core/src/shared/components/card/card.component.html
  11. 26 0
      packages/admin-ui/src/lib/core/src/shared/components/card/card.component.scss
  12. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/form-field/form-field.component.html
  13. 11 3
      packages/admin-ui/src/lib/core/src/shared/components/form-field/form-field.component.scss
  14. 4 1
      packages/admin-ui/src/lib/core/src/shared/components/form-item/form-item.component.html
  15. 18 3
      packages/admin-ui/src/lib/core/src/shared/components/form-item/form-item.component.scss
  16. 1 0
      packages/admin-ui/src/lib/core/src/shared/components/form-item/form-item.component.ts
  17. 9 0
      packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-layout.component.scss
  18. 6 2
      packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.scss

+ 2 - 3
packages/admin-ui/src/lib/catalog/src/catalog.module.ts

@@ -4,9 +4,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BulkActionRegistryService,
     detailComponentWithResolver,
-    GetProductVariantDetailDocument,
     PageService,
-    PageBlockComponent,
+    ProductVariantDetailQueryDocument,
     SharedModule,
 } from '@vendure/admin-ui/core';
 
@@ -150,7 +149,7 @@ export class CatalogModule {
             route: '',
             component: detailComponentWithResolver({
                 component: ProductVariantDetailComponent,
-                query: GetProductVariantDetailDocument,
+                query: ProductVariantDetailQueryDocument,
                 getEntity: result => result.productVariant,
                 getBreadcrumbs: result => [
                     {

+ 19 - 1
packages/admin-ui/src/lib/catalog/src/components/assets/assets.component.scss

@@ -1,6 +1,8 @@
 
 :host {
     display: block;
+    container-type: inline-size;
+
     &.compact {
         width: 162px;
     }
@@ -11,6 +13,7 @@
     gap: calc(var(--space-unit) * 2);
 }
 
+
 .all-assets-container {
     display: flex;
     max-width: 50%;
@@ -18,6 +21,16 @@
     justify-content: space-between;
 }
 
+
+@container (max-width: 500px) {
+    .standard-view-container {
+        flex-direction: column;
+    }
+    .all-assets-container {
+        max-width: 100%;
+    }
+}
+
 .placeholder {
     text-align: center;
     color: var(--color-grey-300);
@@ -32,6 +45,9 @@
 
     img {
         border-radius: var(--border-radius-img);
+        max-height: 400px;
+        max-width: 100%;
+        object-fit: contain;
     }
 
     &.compact {
@@ -85,6 +101,7 @@
         .drop-list {
             min-width: 54px;
         }
+
         .asset-thumb {
             margin: 1px;
             border-width: 1px;
@@ -94,6 +111,7 @@
 
 .all-assets.compact .cdk-drag-placeholder {
     width: 50px;
+
     .asset-thumb {
         width: 50px;
     }
@@ -112,5 +130,5 @@
 }
 
 .cdk-drop-list-dragging > *:not(.cdk-drag-placeholder) {
-  display: none;
+    display: none;
 }

+ 14 - 11
packages/admin-ui/src/lib/catalog/src/components/product-detail2/product-detail.component.html

@@ -123,7 +123,6 @@
                     </clr-checkbox-wrapper>
                 </div>
                 <vdr-form-field
-                    class="mt-2"
                     [label]="'catalog.slug' | translate"
                     for="slug"
                     [errors]="{ pattern: 'catalog.slug-pattern-error' | translate }"
@@ -139,6 +138,7 @@
                     [label]="'common.description' | translate"
                     for="slug"
                     [errors]="{ pattern: 'catalog.slug-pattern-error' | translate }"
+                    class="card-span"
                 >
                     <vdr-rich-text-editor
                         formControlName="description"
@@ -161,6 +161,7 @@
             ></vdr-custom-detail-component-host>
             <vdr-card [title]="'catalog.assets' | translate">
                 <vdr-assets
+                    class="card-span"
                     [assets]="assetChanges.assets || product.assets"
                     [featuredAsset]="assetChanges.featuredAsset || product.featuredAsset"
                     [updatePermissions]="updatePermissions"
@@ -169,17 +170,19 @@
             </vdr-card>
 
             <vdr-card [title]="'catalog.product-variants' | translate" [paddingX]="false">
-                <div *ngIf="isNew$ | async; else variantList">
-                    <vdr-generate-product-variants
-                        (variantsChange)="createVariantsConfig = $event"
-                    ></vdr-generate-product-variants>
+                <div class="card-span">
+                    <div *ngIf="isNew$ | async; else variantList">
+                        <vdr-generate-product-variants
+                            (variantsChange)="createVariantsConfig = $event"
+                        ></vdr-generate-product-variants>
+                    </div>
+                    <ng-template #variantList>
+                        <vdr-product-variant-list
+                            [productId]="this.id"
+                            [hideLanguageSelect]="true"
+                        ></vdr-product-variant-list>
+                    </ng-template>
                 </div>
-                <ng-template #variantList>
-                    <vdr-product-variant-list
-                        [productId]="this.id"
-                        [hideLanguageSelect]="true"
-                    ></vdr-product-variant-list>
-                </ng-template>
             </vdr-card>
         </vdr-page-block>
     </vdr-page-detail-layout>

+ 120 - 2
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html

@@ -16,7 +16,12 @@
                 *vdrIfPermissions="['UpdateCatalog', 'UpdateProduct']"
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="(detailForm.invalid || detailForm.pristine) && !assetsChanged()"
+                [disabled]="
+                    (detailForm.invalid ||
+                        stockLevelsForm.invalid ||
+                        (detailForm.pristine && stockLevelsForm.pristine)) &&
+                    !assetsChanged()
+                "
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -39,6 +44,41 @@
                     </clr-toggle-wrapper>
                 </vdr-form-field>
             </vdr-card>
+            <vdr-card *ngIf="variant.options.length" [title]="'catalog.product-options' | translate">
+                <div class="options">
+                    <vdr-chip
+                        *ngFor="let option of variant.options | sort : 'groupId'"
+                        [colorFrom]="optionGroupName(option.groupId)"
+                        [invert]="true"
+                        (iconClick)="editOption(option)"
+                        [icon]="(updatePermissions | hasPermission) && 'pencil'"
+                    >
+                        <span class="mr-1">{{ optionGroupName(option.groupId) }}:</span>
+                        {{ optionName(option) }}
+                    </vdr-chip>
+                    <a [routerLink]="['./', 'options']" class="btn btn-link btn-sm"
+                        >{{ 'catalog.edit-options' | translate }}...</a
+                    >
+                </div>
+            </vdr-card>
+            <vdr-card [title]="'catalog.facets' | translate">
+                <div class="facets">
+                    <vdr-facet-value-chip
+                        *ngFor="let facetValue of facetValues$ | async"
+                        [facetValue]="facetValue"
+                        [removable]="updatePermissions | hasPermission"
+                        (remove)="removeFacetValue(facetValue.id)"
+                    ></vdr-facet-value-chip>
+                    <button
+                        class="btn btn-sm btn-secondary"
+                        *vdrIfPermissions="updatePermissions"
+                        (click)="selectFacetValue()"
+                    >
+                        <clr-icon shape="plus"></clr-icon>
+                        {{ 'catalog.add-facets' | translate }}
+                    </button>
+                </div>
+            </vdr-card>
 
             <vdr-card>
                 <vdr-page-entity-info *ngIf="entity$ | async as entity" [entity]="entity" />
@@ -115,8 +155,86 @@
                     [taxCategoryId]="detailForm.get('taxCategoryId')!.value"
                 />
             </vdr-card>
-            <vdr-card [title]="'catalog.stock' | translate">
+            <vdr-card [title]="'catalog.stock-levels' | translate">
+                <vdr-form-field
+                    for="track-inventory"
+                    [label]="'catalog.track-inventory' | translate"
+                    [tooltip]="'catalog.track-inventory-tooltip' | translate"
+                >
+                    <select
+                        name="track-inventory"
+                        formControlName="trackInventory"
+                        [disabled]="!(updatePermissions | hasPermission)"
+                    >
+                        <option [value]="GlobalFlag.TRUE">
+                            {{ 'catalog.track-inventory-true' | translate }}
+                        </option>
+                        <option [value]="GlobalFlag.FALSE">
+                            {{ 'catalog.track-inventory-false' | translate }}
+                        </option>
+                        <option [value]="GlobalFlag.INHERIT">
+                            {{ 'catalog.track-inventory-inherit' | translate }}
+                        </option>
+                    </select>
+                </vdr-form-field>
+
+                <vdr-form-item
+                    [label]="'catalog.out-of-stock-threshold' | translate"
+                    [tooltip]="'catalog.out-of-stock-threshold-tooltip' | translate"
+                >
+                    <input
+                        type="number"
+                        formControlName="outOfStockThreshold"
+                        [readonly]="!(updatePermissions | hasPermission)"
+                        [vdrDisabled]="
+                            detailForm.get('useGlobalOutOfStockThreshold')?.value !== false ||
+                            inventoryIsNotTracked(detailForm)
+                        "
+                    />
+                    <clr-toggle-wrapper>
+                        <input
+                            type="checkbox"
+                            clrToggle
+                            name="useGlobalOutOfStockThreshold"
+                            formControlName="useGlobalOutOfStockThreshold"
+                            [vdrDisabled]="
+                                !(updatePermissions | hasPermission) || inventoryIsNotTracked(detailForm)
+                            "
+                        />
+                        <label
+                            >{{ 'catalog.use-global-value' | translate }} ({{
+                                globalOutOfStockThreshold
+                            }})</label
+                        >
+                    </clr-toggle-wrapper>
+                </vdr-form-item>
 
+                <ng-container *ngFor="let stockLevel of stockLevelsForm.controls" [formGroup]="stockLevel">
+                    <vdr-form-field
+                        [label]="
+                            stockLevel.get('stockLocationName')?.value +
+                            ': ' +
+                            ('catalog.stock-on-hand' | translate)
+                        "
+                        [for]="'stockOnHand_' + stockLevel.get('stockLocationId')?.value"
+                    >
+                        <input
+                            [id]="'stockOnHand_' + stockLevel.get('stockLocationId')?.value"
+                            type="number"
+                            formControlName="stockOnHand"
+                            [readonly]="!(updatePermissions | hasPermission)"
+                        />
+                    </vdr-form-field>
+                    <vdr-form-item
+                        [label]="
+                            stockLevel.get('stockLocationName')?.value +
+                            ': ' +
+                            ('catalog.stock-allocated' | translate)
+                        "
+                    >
+                        {{ stockLevel.get('stockAllocated')?.value }}
+                    </vdr-form-item>
+                </ng-container>
             </vdr-card>
         </vdr-page-block>
     </vdr-page-detail-layout>

+ 234 - 15
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.ts

@@ -1,27 +1,44 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
-import { FormBuilder } from '@angular/forms';
+import { FormBuilder, FormControl, FormGroup, UntypedFormGroup } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     Asset,
-    BaseDetailComponent,
+    CreateProductVariantInput,
+    createUpdatedTranslatable,
     DataService,
     findTranslation,
-    GetProductVariantDetailDocument,
-    GetProductVariantDetailQuery,
     GlobalFlag,
     ItemOf,
     LanguageCode,
     ModalService,
     NotificationService,
     Permission,
+    ProductOptionFragment,
+    ProductVariantDetailQueryDocument,
+    ProductVariantDetailQueryQuery,
+    ProductVariantFragment,
+    ProductVariantUpdateMutationDocument,
     ServerConfigService,
-    TaxCategoryFragment,
     TypedBaseDetailComponent,
-    TypedBaseListComponent,
+    UpdateProductVariantInput,
 } from '@vendure/admin-ui/core';
-import { Observable } from 'rxjs';
-import { map, shareReplay, takeUntil } from 'rxjs/operators';
+import { pick } from '@vendure/common/lib/pick';
+import { unique } from '@vendure/common/lib/unique';
+import { combineLatest, concat, Observable } from 'rxjs';
+import {
+    distinctUntilChanged,
+    map,
+    mergeMap,
+    shareReplay,
+    skip,
+    switchMap,
+    switchMapTo,
+    take,
+    tap,
+} from 'rxjs/operators';
 import { ProductDetailService } from '../../providers/product-detail/product-detail.service';
+import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
 
 interface SelectedAssets {
     assets?: Asset[];
@@ -43,7 +60,8 @@ interface VariantFormValue {
     facetValueIds: string[][];
     customFields?: any;
 }
-
+type T = NonNullable<ProductVariantDetailQueryQuery['productVariant']>;
+type T1 = T['stockLevels'];
 @Component({
     selector: 'vdr-product-variant-detail',
     templateUrl: './product-variant-detail.component.html',
@@ -51,11 +69,12 @@ interface VariantFormValue {
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class ProductVariantDetailComponent
-    extends TypedBaseDetailComponent<typeof GetProductVariantDetailDocument, 'productVariant'>
+    extends TypedBaseDetailComponent<typeof ProductVariantDetailQueryDocument, 'productVariant'>
     implements OnInit
 {
     public readonly updatePermissions = [Permission.UpdateCatalog, Permission.UpdateProduct];
     readonly customFields = this.getCustomFieldConfig('ProductVariant');
+    stockLevels$: Observable<NonNullable<ProductVariantDetailQueryQuery['productVariant']>['stockLevels']>;
     detailForm = this.formBuilder.group<VariantFormValue>({
         id: '',
         enabled: false,
@@ -73,10 +92,22 @@ export class ProductVariantDetailComponent
             this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
         ),
     });
+    stockLevelsForm = this.formBuilder.array<
+        FormGroup<{
+            stockLocationId: FormControl<string | null>;
+            stockLocationName: FormControl<string | null>;
+            stockOnHand: FormControl<number | null>;
+            stockAllocated: FormControl<number | null>;
+        }>
+    >([]);
     assetChanges: SelectedAssets = {};
-    taxCategories$: Observable<Array<ItemOf<GetProductVariantDetailQuery, 'taxCategories'>>>;
-    stockLocations$: Observable<ItemOf<GetProductVariantDetailQuery, 'stockLocations'>>;
+    taxCategories$: Observable<Array<ItemOf<ProductVariantDetailQueryQuery, 'taxCategories'>>>;
+    stockLocations$: Observable<ItemOf<ProductVariantDetailQueryQuery, 'stockLocations'>>;
     channelPriceIncludesTax$: Observable<boolean>;
+    readonly GlobalFlag = GlobalFlag;
+    globalTrackInventory: boolean;
+    globalOutOfStockThreshold: number;
+    facetValues$: Observable<NonNullable<ProductVariantDetailQueryQuery['productVariant']>['facetValues']>;
 
     constructor(
         route: ActivatedRoute,
@@ -94,24 +125,175 @@ export class ProductVariantDetailComponent
 
     ngOnInit() {
         this.init();
+        this.dataService.settings.getGlobalSettings('cache-first').single$.subscribe(({ globalSettings }) => {
+            this.globalTrackInventory = globalSettings.trackInventory;
+            this.globalOutOfStockThreshold = globalSettings.outOfStockThreshold;
+            this.changeDetector.markForCheck();
+        });
         this.taxCategories$ = this.result$.pipe(map(data => data.taxCategories.items));
         this.channelPriceIncludesTax$ = this.dataService.settings
             .getActiveChannel('cache-first')
             .refetchOnChannelChange()
             .mapStream(data => data.activeChannel.pricesIncludeTax)
             .pipe(shareReplay(1));
+        this.stockLevels$ = this.entity$.pipe(map(entity => entity?.stockLevels ?? []));
+        const facetValues$ = this.entity$.pipe(map(variant => variant.facetValues ?? []));
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        const formFacetValueIdChanges$ = this.detailForm.get('facetValueIds')!.valueChanges.pipe(
+            skip(1),
+            distinctUntilChanged(),
+            switchMap(ids =>
+                this.dataService.facet
+                    .getFacetValues({ filter: { id: { in: ids } } })
+                    .mapSingle(({ facetValues }) => facetValues.items),
+            ),
+            shareReplay(1),
+        );
+        this.facetValues$ = concat(
+            facetValues$.pipe(take(1)),
+            facetValues$.pipe(switchMapTo(formFacetValueIdChanges$)),
+        );
     }
 
     save() {
-        /**/
+        combineLatest(this.entity$, this.languageCode$)
+            .pipe(
+                take(1),
+                mergeMap(([variant, languageCode]) => {
+                    const formValue = this.detailForm.value;
+                    const input = pick(
+                        this.getUpdatedVariant(
+                            variant,
+                            this.detailForm,
+                            languageCode,
+                        ) as UpdateProductVariantInput,
+                        [
+                            'id',
+                            'enabled',
+                            'translations',
+                            'sku',
+                            'price',
+                            'taxCategoryId',
+                            'facetValueIds',
+                            'featuredAssetId',
+                            'assetIds',
+                            'trackInventory',
+                            'outOfStockThreshold',
+                            'useGlobalOutOfStockThreshold',
+                            'stockOnHand',
+                            'customFields',
+                        ],
+                    ) as UpdateProductVariantInput;
+                    if (this.stockLevelsForm.dirty) {
+                        const stockLevelsFormValue = this.stockLevelsForm.value;
+                        input.stockLevels = this.stockLevelsForm.controls
+                            .filter(control => control.dirty)
+                            .map(control => ({
+                                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                                stockLocationId: control.value.stockLocationId!,
+                                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                                stockOnHand: control.value.stockOnHand!,
+                            }));
+                    }
+                    return this.dataService.mutate(ProductVariantUpdateMutationDocument, {
+                        input: [input],
+                    });
+                }),
+            )
+            .subscribe(
+                result => {
+                    this.detailForm.markAsPristine();
+                    this.stockLevelsForm.markAsPristine();
+                    this.assetChanges = {};
+                    this.notificationService.success(_('common.notify-update-success'), {
+                        entity: 'ProductVariant',
+                    });
+                    this.changeDetector.markForCheck();
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-update-error'), {
+                        entity: 'ProductVariant',
+                    });
+                },
+            );
     }
 
     assetsChanged(): boolean {
         return false;
     }
 
+    inventoryIsNotTracked(formGroup: UntypedFormGroup): boolean {
+        const trackInventory = formGroup.get('trackInventory')?.value;
+        return (
+            trackInventory === GlobalFlag.FALSE ||
+            (trackInventory === GlobalFlag.INHERIT && this.globalTrackInventory === false)
+        );
+    }
+
+    optionGroupName(optionGroupId: string): string | undefined {
+        const group = this.entity.product.optionGroups.find(g => g.id === optionGroupId);
+        if (group) {
+            const translation =
+                group?.translations.find(t => t.languageCode === this.languageCode) ?? group.translations[0];
+            return translation.name;
+        }
+    }
+
+    optionName(option: ProductOptionFragment) {
+        const translation =
+            option.translations.find(t => t.languageCode === this.languageCode) ?? option.translations[0];
+        return translation.name;
+    }
+
+    editOption(option: ProductVariantFragment['options'][number]) {
+        /*     this.modalService
+                .fromComponent(UpdateProductOptionDialogComponent, {
+                    size: 'md',
+                    locals: {
+                        productOption: option,
+                        activeLanguage: this.languageCode,
+                        customFields: this.customOptionFields,
+                    },
+                })
+                .subscribe(result => {
+                    if (result) {
+                        this.updateProductOption.emit(result);
+                    }
+                }); */
+    }
+
+    removeFacetValue(facetValueId: string) {
+        const productGroup = this.detailForm;
+        const currentFacetValueIds = productGroup.value.facetValueIds ?? [];
+        productGroup.patchValue({
+            facetValueIds: currentFacetValueIds.filter(id => id !== facetValueId),
+        });
+        productGroup.markAsDirty();
+    }
+
+    selectFacetValue() {
+        this.displayFacetValueModal().subscribe(facetValueIds => {
+            if (facetValueIds) {
+                const currentFacetValueIds = this.detailForm.value.facetValueIds ?? [];
+                this.detailForm.patchValue({
+                    facetValueIds: unique([...currentFacetValueIds, ...facetValueIds]),
+                });
+                this.detailForm.markAsDirty();
+            }
+        });
+    }
+
+    private displayFacetValueModal(): Observable<string[] | undefined> {
+        return this.modalService
+            .fromComponent(ApplyFacetDialogComponent, {
+                size: 'md',
+                closable: true,
+            })
+            .pipe(map(facetValues => facetValues && facetValues.map(v => v.id)));
+    }
+
     protected setFormValues(
-        variant: NonNullable<GetProductVariantDetailQuery['productVariant']>,
+        variant: NonNullable<ProductVariantDetailQueryQuery['productVariant']>,
         languageCode: LanguageCode,
     ): void {
         const variantTranslation = findTranslation(variant, languageCode);
@@ -130,7 +312,17 @@ export class ProductVariantDetailComponent
             trackInventory: variant.trackInventory,
             facetValueIds,
         });
-
+        this.stockLevelsForm.clear();
+        for (const stockLevel of variant.stockLevels) {
+            this.stockLevelsForm.push(
+                this.formBuilder.group({
+                    stockLocationId: stockLevel.stockLocation.id,
+                    stockLocationName: stockLevel.stockLocation.name,
+                    stockOnHand: stockLevel.stockOnHand,
+                    stockAllocated: stockLevel.stockAllocated,
+                }),
+            );
+        }
         if (this.customFields.length) {
             this.setCustomFieldFormValues(
                 this.customFields,
@@ -140,4 +332,31 @@ export class ProductVariantDetailComponent
             );
         }
     }
+
+    /**
+     * Given a product and the value of the detailForm, this method creates an updated copy of the product which
+     * can then be persisted to the API.
+     */
+    private getUpdatedVariant(
+        variant: NonNullable<ProductVariantDetailQueryQuery['productVariant']>,
+        variantFormGroup: typeof this.detailForm,
+        languageCode: LanguageCode,
+    ): UpdateProductVariantInput | CreateProductVariantInput {
+        const updatedProduct = createUpdatedTranslatable({
+            translatable: variant,
+            updatedFields: variantFormGroup.value,
+            customFieldConfig: this.customFields,
+            languageCode,
+            defaultTranslation: {
+                languageCode,
+                name: variant.name || '',
+            },
+        });
+        return {
+            ...updatedProduct,
+            assetIds: this.assetChanges.assets?.map(a => a.id),
+            featuredAssetId: this.assetChanges.featuredAsset?.id,
+            facetValueIds: variantFormGroup.value.facetValueIds,
+        } as UpdateProductVariantInput | CreateProductVariantInput;
+    }
 }

+ 80 - 57
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.graphql.ts

@@ -1,78 +1,93 @@
 import { ASSET_FRAGMENT, PRODUCT_OPTION_FRAGMENT } from '@vendure/admin-ui/core';
 import { gql } from 'apollo-angular';
 
-export const GET_PRODUCT_VARIANT_DETAIL = gql`
-    query GetProductVariantDetail($id: ID!) {
-        productVariant(id: $id) {
+export const PRODUCT_VARIANT_DETAIL_QUERY_PRODUCT_VARIANT_FRAGMENT = gql`
+    fragment ProductVariantDetailQueryProductVariantFragment on ProductVariant {
+        id
+        createdAt
+        updatedAt
+        enabled
+        languageCode
+        name
+        price
+        currencyCode
+        priceWithTax
+        stockOnHand
+        stockAllocated
+        trackInventory
+        outOfStockThreshold
+        useGlobalOutOfStockThreshold
+        taxRateApplied {
+            id
+            name
+            value
+        }
+        taxCategory {
+            id
+            name
+        }
+        sku
+        options {
+            ...ProductOption
+        }
+        stockLevels {
             id
             createdAt
             updatedAt
-            enabled
-            languageCode
-            name
-            price
-            currencyCode
-            priceWithTax
             stockOnHand
             stockAllocated
-            trackInventory
-            outOfStockThreshold
-            useGlobalOutOfStockThreshold
-            taxRateApplied {
+            stockLocationId
+            stockLocation {
                 id
+                createdAt
+                updatedAt
                 name
-                value
             }
-            taxCategory {
+        }
+        facetValues {
+            id
+            code
+            name
+            facet {
                 id
                 name
             }
-            sku
-            options {
-                ...ProductOption
-            }
-            stockLevels {
-                id
-                createdAt
-                updatedAt
-                stockOnHand
-                stockAllocated
-                stockLocationId
-                stockLocation {
-                    id
-                    createdAt
-                    updatedAt
-                    name
-                }
-            }
-            facetValues {
+        }
+        featuredAsset {
+            ...Asset
+        }
+        assets {
+            ...Asset
+        }
+        translations {
+            id
+            languageCode
+            name
+        }
+        channels {
+            id
+            code
+        }
+        product {
+            id
+            name
+            optionGroups {
                 id
-                code
                 name
-                facet {
+                translations {
                     id
+                    languageCode
                     name
                 }
             }
-            featuredAsset {
-                ...Asset
-            }
-            assets {
-                ...Asset
-            }
-            translations {
-                id
-                languageCode
-                name
-            }
-            channels {
-                id
-                code
-            }
-            product {
-                id
-                name
-            }
+        }
+    }
+`;
+
+export const PRODUCT_VARIANT_DETAIL_QUERY = gql`
+    query ProductVariantDetailQuery($id: ID!) {
+        productVariant(id: $id) {
+            ...ProductVariantDetailQueryProductVariantFragment
         }
         stockLocations(options: { take: 100 }) {
             items {
@@ -94,6 +109,14 @@ export const GET_PRODUCT_VARIANT_DETAIL = gql`
             totalItems
         }
     }
-    ${PRODUCT_OPTION_FRAGMENT}
-    ${ASSET_FRAGMENT}
+    ${PRODUCT_VARIANT_DETAIL_QUERY_PRODUCT_VARIANT_FRAGMENT}
+`;
+
+export const PRODUCT_VARIANT_UPDATE_MUTATION = gql`
+    mutation ProductVariantUpdateMutation($input: [UpdateProductVariantInput!]!) {
+        updateProductVariants(input: $input) {
+            ...ProductVariantDetailQueryProductVariantFragment
+        }
+    }
+    ${PRODUCT_VARIANT_DETAIL_QUERY_PRODUCT_VARIANT_FRAGMENT}
 `;

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

@@ -59,6 +59,7 @@ export abstract class BaseDetailComponent<Entity extends { id: string; updatedAt
     entity$: Observable<Entity>;
     availableLanguages$: Observable<LanguageCode[]>;
     languageCode$: Observable<LanguageCode>;
+    languageCode: LanguageCode;
     isNew$: Observable<boolean>;
     id: string;
     abstract detailForm: UntypedFormGroup;
@@ -95,6 +96,7 @@ export abstract class BaseDetailComponent<Entity extends { id: string; updatedAt
                 }
             }),
             distinctUntilChanged(),
+            tap(val => (this.languageCode = val)),
             shareReplay(1),
         );
 
@@ -169,12 +171,16 @@ export abstract class TypedBaseDetailComponent<
     Field extends keyof ResultOf<T>,
 > extends BaseDetailComponent<NonNullable<ResultOf<T>[Field]>> {
     protected result$: Observable<ResultOf<T>>;
+    protected entity: NonNullable<ResultOf<T>[Field]>;
     override init() {
         this.entity$ = this.route.data.pipe(
             switchMap(data =>
                 (data.detail.entity as Observable<ResultOf<T>[Field]>).pipe(takeUntil(this.destroy$)),
             ),
-            tap(entity => (this.id = entity.id)),
+            tap(entity => {
+                this.id = entity.id;
+                this.entity = entity;
+            }),
             shareReplay(1),
         );
         this.result$ = this.route.data.pipe(

Fișier diff suprimat deoarece este prea mare
+ 1 - 2
packages/admin-ui/src/lib/core/src/common/generated-types.ts


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

@@ -104,7 +104,7 @@ export class DataService {
      * ```
      */
     mutate<T, V extends Record<string, any> = Record<string, any>>(
-        mutation: DocumentNode,
+        mutation: DocumentNode | TypedDocumentNode<T, V>,
         variables?: V,
         update?: MutationUpdaterFn<T>,
     ): Observable<T> {

+ 3 - 1
packages/admin-ui/src/lib/core/src/shared/components/card/card.component.html

@@ -1,4 +1,6 @@
 <div class="card-container" [class.padding-x]="paddingX">
     <div *ngIf="title" class="title">{{ title }}</div>
-    <ng-content></ng-content>
+    <div class="contents">
+        <ng-content></ng-content>
+    </div>
 </div>

+ 26 - 0
packages/admin-ui/src/lib/core/src/shared/components/card/card.component.scss

@@ -31,3 +31,29 @@
 ::ng-deep vdr-card + vdr-card {
     margin-top: calc(var(--space-unit) * 2);
 }
+
+.contents {
+    display: grid;
+    gap: calc(var(--space-unit) * 4);
+    grid-template-columns: 1fr 1fr;
+
+    ::ng-deep .card-span {
+        grid-column: span 2;
+    }
+}
+
+
+@container (max-width: 700px) {
+    .contents {
+        gap: calc(var(--space-unit) * 2);
+    }
+}
+@container (max-width: 500px) {
+    .contents {
+        grid-template-columns: 1fr;
+
+        ::ng-deep .card-span {
+               grid-column: initial;
+           }
+    }
+}

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/form-field/form-field.component.html

@@ -6,7 +6,7 @@
     <label *ngIf="label" [for]="for" class="">
         {{ label }}
     </label>
-    <div *ngIf="tooltip">
+    <div *ngIf="tooltip" class="tooltip-text">
         {{ tooltip }}
     </div>
     <div

+ 11 - 3
packages/admin-ui/src/lib/core/src/shared/components/form-field/form-field.component.scss

@@ -2,9 +2,13 @@
     display: block;
 }
 
-::ng-deep vdr-form-field + vdr-form-field {
-    margin-top: calc(var(--space-unit) * 2);
-}
+//::ng-deep vdr-form-field + vdr-form-field {
+//    margin-top: calc(var(--space-unit) * 2);
+//}
+//
+//::ng-deep vdr-form-field + vdr-form-item {
+//    margin-top: calc(var(--space-unit) * 2);
+//}
 
 .form-group {
     label {
@@ -12,6 +16,10 @@
         color: var(--color-weight-800);
     }
 }
+.tooltip-text {
+    font-size: var(--font-size-xs);
+    line-height: var(--font-size-sm);
+}
 .input-row {
     display: flex;
 }

+ 4 - 1
packages/admin-ui/src/lib/core/src/shared/components/form-item/form-item.component.html

@@ -1,4 +1,7 @@
 <div class="form-group">
-    <label class="clr-control-label">{{ label }}</label>
+    <label class="">{{ label }}</label>
+    <div *ngIf="tooltip" class="tooltip-text">
+        {{ tooltip }}
+    </div>
     <div class="content"><ng-content></ng-content></div>
 </div>

+ 18 - 3
packages/admin-ui/src/lib/core/src/shared/components/form-item/form-item.component.scss

@@ -1,7 +1,22 @@
 :host {
     display: block;
-    .form-group >.content {
-        flex: 1;
-        max-width: 20rem;
+}
+
+.form-group {
+    label {
+        font-size: var(--font-size-sm);
+        color: var(--color-weight-800);
     }
 }
+.tooltip-text {
+    font-size: var(--font-size-xs);
+    line-height: var(--font-size-sm);
+}
+
+//::ng-deep vdr-form-item + vdr-form-item {
+//    margin-top: calc(var(--space-unit) * 2);
+//}
+//
+//::ng-deep vdr-form-item + vdr-form-field {
+//    margin-top: calc(var(--space-unit) * 2);
+//}

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/components/form-item/form-item.component.ts

@@ -12,4 +12,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 })
 export class FormItemComponent {
     @Input() label: string;
+    @Input() tooltip: string;
 }

+ 9 - 0
packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-layout.component.scss

@@ -1,8 +1,17 @@
+@import "variables";
+
+
 :host {
     display: grid;
     grid-template-columns: 3fr 1fr;
 }
 
+@media screen and (max-width: $breakpoint-medium) {
+    :host {
+        grid-template-columns: 1fr;
+    }
+}
+
 .sidebar {
     padding: var(--space-unit);
 }

+ 6 - 2
packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.scss

@@ -1,3 +1,7 @@
-vdr-custom-field-control + vdr-custom-field-control {
-    margin-top: calc(var(--space-unit) * 2);
+:host {
+    display: contents;
 }
+
+//vdr-custom-field-control + vdr-custom-field-control {
+//    margin-top: calc(var(--space-unit) * 2);
+//}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff