|
|
@@ -1,32 +1,29 @@
|
|
|
-import { Location } from '@angular/common';
|
|
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
|
|
-import {
|
|
|
- UntypedFormArray,
|
|
|
- UntypedFormBuilder,
|
|
|
- UntypedFormControl,
|
|
|
- UntypedFormGroup,
|
|
|
- Validators,
|
|
|
-} from '@angular/forms';
|
|
|
+import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
|
|
import {
|
|
|
+ Asset,
|
|
|
BaseDetailComponent,
|
|
|
CreateProductInput,
|
|
|
createUpdatedTranslatable,
|
|
|
- CustomFieldConfig,
|
|
|
DataService,
|
|
|
findTranslation,
|
|
|
getChannelCodeFromUserStatus,
|
|
|
+ getDefaultUiLanguage,
|
|
|
+ GetProductDetailDocument,
|
|
|
+ GetProductDetailQuery,
|
|
|
GetProductWithVariantsQuery,
|
|
|
+ ItemOf,
|
|
|
LanguageCode,
|
|
|
- LogicalOperator,
|
|
|
ModalService,
|
|
|
NotificationService,
|
|
|
Permission,
|
|
|
+ PRODUCT_DETAIL_FRAGMENT,
|
|
|
ProductDetailFragment,
|
|
|
ProductVariantFragment,
|
|
|
ServerConfigService,
|
|
|
- TaxCategoryFragment,
|
|
|
+ TypedBaseDetailComponent,
|
|
|
unicodePatternValidator,
|
|
|
UpdateProductInput,
|
|
|
UpdateProductMutation,
|
|
|
@@ -36,69 +33,65 @@ import {
|
|
|
} from '@vendure/admin-ui/core';
|
|
|
import { normalizeString } from '@vendure/common/lib/normalize-string';
|
|
|
import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
|
|
|
-import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
|
|
|
import { unique } from '@vendure/common/lib/unique';
|
|
|
-import { BehaviorSubject, combineLatest, concat, EMPTY, from, merge, Observable } from 'rxjs';
|
|
|
+import { gql } from 'apollo-angular';
|
|
|
+import { combineLatest, concat, EMPTY, from, Observable } from 'rxjs';
|
|
|
import {
|
|
|
- debounceTime,
|
|
|
distinctUntilChanged,
|
|
|
map,
|
|
|
mergeMap,
|
|
|
shareReplay,
|
|
|
skip,
|
|
|
- skipUntil,
|
|
|
- startWith,
|
|
|
switchMap,
|
|
|
switchMapTo,
|
|
|
take,
|
|
|
- takeUntil,
|
|
|
- tap,
|
|
|
} from 'rxjs/operators';
|
|
|
|
|
|
import { ProductDetailService } from '../../providers/product-detail/product-detail.service';
|
|
|
import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
|
|
|
import { AssignProductsToChannelDialogComponent } from '../assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
|
|
|
import { CreateProductVariantsConfig } from '../generate-product-variants/generate-product-variants.component';
|
|
|
-import { VariantAssetChange } from '../product-variants-list/product-variants-list.component';
|
|
|
|
|
|
-import { PaginationConfig, SelectedAssets, TabName, VariantFormValue } from './product-detail.types';
|
|
|
+interface SelectedAssets {
|
|
|
+ assets?: Asset[];
|
|
|
+ featuredAsset?: Asset;
|
|
|
+}
|
|
|
+
|
|
|
+export const GET_PRODUCT_DETAIL = gql`
|
|
|
+ query GetProductDetail($id: ID!) {
|
|
|
+ product(id: $id) {
|
|
|
+ ...ProductDetail
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ${PRODUCT_DETAIL_FRAGMENT}
|
|
|
+`;
|
|
|
|
|
|
@Component({
|
|
|
- selector: 'vdr-product-detail',
|
|
|
+ selector: 'vdr-product-detail2',
|
|
|
templateUrl: './product-detail.component.html',
|
|
|
styleUrls: ['./product-detail.component.scss'],
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
})
|
|
|
export class ProductDetailComponent
|
|
|
- extends BaseDetailComponent<NonNullable<GetProductWithVariantsQuery['product']>>
|
|
|
+ extends TypedBaseDetailComponent<typeof GetProductDetailDocument, 'product'>
|
|
|
implements OnInit, OnDestroy
|
|
|
{
|
|
|
- activeTab$: Observable<TabName>;
|
|
|
- product$: Observable<NonNullable<GetProductWithVariantsQuery['product']>>;
|
|
|
- variants$: Observable<ProductVariantFragment[]>;
|
|
|
- taxCategories$: Observable<TaxCategoryFragment[]>;
|
|
|
- customFields: CustomFieldConfig[];
|
|
|
- customVariantFields: CustomFieldConfig[];
|
|
|
- customOptionGroupFields: CustomFieldConfig[];
|
|
|
- customOptionFields: CustomFieldConfig[];
|
|
|
- detailForm: UntypedFormGroup;
|
|
|
- filterInput = new UntypedFormControl('');
|
|
|
+ readonly customFields = this.getCustomFieldConfig('Product');
|
|
|
+ detailForm = this.formBuilder.group({
|
|
|
+ enabled: true,
|
|
|
+ name: ['', Validators.required],
|
|
|
+ autoUpdateVariantNames: true,
|
|
|
+ slug: ['', unicodePatternValidator(/^[\p{Letter}0-9_-]+$/)],
|
|
|
+ description: '',
|
|
|
+ facetValueIds: [[] as string[]],
|
|
|
+ customFields: this.formBuilder.group(
|
|
|
+ this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
|
|
|
+ ),
|
|
|
+ });
|
|
|
assetChanges: SelectedAssets = {};
|
|
|
- variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
|
|
|
- variantFacetValueChanges: { [variantId: string]: ProductVariantFragment['facetValues'] } = {};
|
|
|
productChannels$: Observable<ProductDetailFragment['channels']>;
|
|
|
facetValues$: Observable<ProductDetailFragment['facetValues']>;
|
|
|
- totalItems$: Observable<number>;
|
|
|
- currentPage$ = new BehaviorSubject(1);
|
|
|
- itemsPerPage$ = new BehaviorSubject(10);
|
|
|
- paginationConfig$: Observable<PaginationConfig>;
|
|
|
- selectedVariantIds: string[] = [];
|
|
|
- variantDisplayMode: 'card' | 'table' = 'card';
|
|
|
createVariantsConfig: CreateProductVariantsConfig = { groups: [], variants: [] };
|
|
|
- channelPriceIncludesTax$: Observable<boolean>;
|
|
|
- // Used to store all ProductVariants which have been loaded.
|
|
|
- // It is needed when saving changes to variants.
|
|
|
- private productVariantMap = new Map<string, ProductVariantFragment>();
|
|
|
public readonly updatePermissions = [Permission.UpdateCatalog, Permission.UpdateProduct];
|
|
|
|
|
|
constructor(
|
|
|
@@ -106,95 +99,19 @@ export class ProductDetailComponent
|
|
|
router: Router,
|
|
|
serverConfigService: ServerConfigService,
|
|
|
private productDetailService: ProductDetailService,
|
|
|
- private formBuilder: UntypedFormBuilder,
|
|
|
+ private formBuilder: FormBuilder,
|
|
|
private modalService: ModalService,
|
|
|
private notificationService: NotificationService,
|
|
|
protected dataService: DataService,
|
|
|
- private location: Location,
|
|
|
private changeDetector: ChangeDetectorRef,
|
|
|
) {
|
|
|
- super(route, router, serverConfigService, dataService);
|
|
|
- this.customFields = this.getCustomFieldConfig('Product');
|
|
|
- this.customVariantFields = this.getCustomFieldConfig('ProductVariant');
|
|
|
- this.customOptionGroupFields = this.getCustomFieldConfig('ProductOptionGroup');
|
|
|
- this.customOptionFields = this.getCustomFieldConfig('ProductOption');
|
|
|
- this.detailForm = this.formBuilder.group({
|
|
|
- product: this.formBuilder.group({
|
|
|
- enabled: true,
|
|
|
- name: ['', Validators.required],
|
|
|
- autoUpdateVariantNames: true,
|
|
|
- slug: ['', unicodePatternValidator(/^[\p{Letter}0-9_-]+$/)],
|
|
|
- description: '',
|
|
|
- facetValueIds: [[]],
|
|
|
- customFields: this.formBuilder.group(
|
|
|
- this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
|
|
|
- ),
|
|
|
- }),
|
|
|
- variants: this.formBuilder.array([]),
|
|
|
- });
|
|
|
+ super();
|
|
|
}
|
|
|
|
|
|
ngOnInit() {
|
|
|
this.init();
|
|
|
- this.product$ = this.entity$;
|
|
|
- const filterTerm$ = this.filterInput.valueChanges.pipe(
|
|
|
- startWith(''),
|
|
|
- debounceTime(200),
|
|
|
- shareReplay(),
|
|
|
- tap(() => this.currentPage$.next(1)),
|
|
|
- );
|
|
|
- const initialVariants$ = this.product$.pipe(map(p => p.variantList.items));
|
|
|
- const variantsList$ = combineLatest(filterTerm$, this.currentPage$, this.itemsPerPage$).pipe(
|
|
|
- skipUntil(initialVariants$),
|
|
|
- skip(1),
|
|
|
- debounceTime(100),
|
|
|
- switchMap(([term, currentPage, itemsPerPage]) =>
|
|
|
- this.dataService.product
|
|
|
- .getProductVariantsForProduct(
|
|
|
- {
|
|
|
- skip: (currentPage - 1) * itemsPerPage,
|
|
|
- take: itemsPerPage,
|
|
|
- ...(term
|
|
|
- ? { filter: { name: { contains: term }, sku: { contains: term } } }
|
|
|
- : {}),
|
|
|
- filterOperator: LogicalOperator.OR,
|
|
|
- },
|
|
|
- this.id,
|
|
|
- )
|
|
|
- .mapStream(({ productVariants }) => productVariants),
|
|
|
- ),
|
|
|
- shareReplay({ bufferSize: 1, refCount: true }),
|
|
|
- );
|
|
|
- const updatedVariants$ = variantsList$.pipe(map(result => result.items));
|
|
|
- this.variants$ = merge(initialVariants$, updatedVariants$).pipe(
|
|
|
- tap(variants => {
|
|
|
- for (const variant of variants) {
|
|
|
- this.productVariantMap.set(variant.id, variant);
|
|
|
- }
|
|
|
- }),
|
|
|
- );
|
|
|
- this.totalItems$ = merge(
|
|
|
- this.product$.pipe(map(product => product.variantList.totalItems)),
|
|
|
- variantsList$.pipe(map(result => result.totalItems)),
|
|
|
- );
|
|
|
- this.paginationConfig$ = combineLatest(this.totalItems$, this.itemsPerPage$, this.currentPage$).pipe(
|
|
|
- map(([totalItems, itemsPerPage, currentPage]) => ({
|
|
|
- totalItems,
|
|
|
- itemsPerPage,
|
|
|
- currentPage,
|
|
|
- })),
|
|
|
- );
|
|
|
- this.taxCategories$ = this.productDetailService.getTaxCategories().pipe(takeUntil(this.destroy$));
|
|
|
- this.activeTab$ = this.route.paramMap.pipe(map(qpm => qpm.get('tab') as any));
|
|
|
-
|
|
|
- combineLatest(updatedVariants$, this.languageCode$)
|
|
|
- .pipe(takeUntil(this.destroy$))
|
|
|
- .subscribe(([variants, languageCode]) => {
|
|
|
- this.buildVariantFormArray(variants, languageCode);
|
|
|
- });
|
|
|
-
|
|
|
- const productFacetValues$ = this.product$.pipe(map(product => product.facetValues));
|
|
|
- const productGroup = this.getProductFormGroup();
|
|
|
+ const productFacetValues$ = this.entity$.pipe(map(product => product.facetValues));
|
|
|
+ const productGroup = this.detailForm;
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
const formFacetValueIdChanges$ = productGroup.get('facetValueIds')!.valueChanges.pipe(
|
|
|
skip(1),
|
|
|
@@ -210,42 +127,17 @@ export class ProductDetailComponent
|
|
|
productFacetValues$.pipe(take(1)),
|
|
|
productFacetValues$.pipe(switchMapTo(formFacetValueIdChanges$)),
|
|
|
);
|
|
|
- this.productChannels$ = this.product$.pipe(map(p => p.channels));
|
|
|
- this.channelPriceIncludesTax$ = this.dataService.settings
|
|
|
- .getActiveChannel('cache-first')
|
|
|
- .refetchOnChannelChange()
|
|
|
- .mapStream(data => data.activeChannel.pricesIncludeTax)
|
|
|
- .pipe(shareReplay(1));
|
|
|
+ this.productChannels$ = this.entity$.pipe(map(p => p.channels));
|
|
|
}
|
|
|
|
|
|
ngOnDestroy() {
|
|
|
this.destroy();
|
|
|
}
|
|
|
|
|
|
- navigateToTab(tabName: TabName) {
|
|
|
- this.location.replaceState(
|
|
|
- this.router
|
|
|
- .createUrlTree(['./', { ...this.route.snapshot.params, tab: tabName }], {
|
|
|
- queryParamsHandling: 'merge',
|
|
|
- relativeTo: this.route,
|
|
|
- })
|
|
|
- .toString(),
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
isDefaultChannel(channelCode: string): boolean {
|
|
|
return channelCode === DEFAULT_CHANNEL_CODE;
|
|
|
}
|
|
|
|
|
|
- setPage(page: number) {
|
|
|
- this.currentPage$.next(page);
|
|
|
- }
|
|
|
-
|
|
|
- setItemsPerPage(value: string) {
|
|
|
- this.itemsPerPage$.next(+value);
|
|
|
- this.currentPage$.next(1);
|
|
|
- }
|
|
|
-
|
|
|
assignToChannel() {
|
|
|
this.productChannels$
|
|
|
.pipe(
|
|
|
@@ -352,35 +244,23 @@ export class ProductDetailComponent
|
|
|
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.
|
|
|
*/
|
|
|
updateSlug(nameValue: string) {
|
|
|
- combineLatest(this.entity$, this.languageCode$)
|
|
|
- .pipe(take(1))
|
|
|
- .subscribe(([entity, languageCode]) => {
|
|
|
- const slugControl = this.detailForm.get(['product', 'slug']);
|
|
|
- const currentTranslation = findTranslation(entity, languageCode);
|
|
|
- const currentSlugIsEmpty = !currentTranslation || !currentTranslation.slug;
|
|
|
- if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
|
|
|
- slugControl.setValue(normalizeString(`${nameValue}`, '-'));
|
|
|
- }
|
|
|
- });
|
|
|
+ const slugControl = this.detailForm.get('slug');
|
|
|
+ const currentTranslation = this.entity ? findTranslation(this.entity, this.languageCode) : undefined;
|
|
|
+ const currentSlugIsEmpty = !currentTranslation || !currentTranslation.slug;
|
|
|
+ if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
|
|
|
+ slugControl.setValue(normalizeString(`${nameValue}`, '-'));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
selectProductFacetValue() {
|
|
|
this.displayFacetValueModal().subscribe(facetValueIds => {
|
|
|
if (facetValueIds) {
|
|
|
- const productGroup = this.getProductFormGroup();
|
|
|
- const currentFacetValueIds = productGroup.value.facetValueIds;
|
|
|
+ const productGroup = this.detailForm;
|
|
|
+ const currentFacetValueIds = productGroup.value.facetValueIds ?? [];
|
|
|
productGroup.patchValue({
|
|
|
facetValueIds: unique([...currentFacetValueIds, ...facetValueIds]),
|
|
|
});
|
|
|
@@ -390,7 +270,7 @@ export class ProductDetailComponent
|
|
|
}
|
|
|
|
|
|
updateProductOption(input: UpdateProductOptionInput & { autoUpdate: boolean }) {
|
|
|
- combineLatest(this.product$, this.languageCode$)
|
|
|
+ combineLatest(this.entity$, this.languageCode$)
|
|
|
.pipe(
|
|
|
take(1),
|
|
|
mergeMap(([product, languageCode]) =>
|
|
|
@@ -412,21 +292,14 @@ export class ProductDetailComponent
|
|
|
}
|
|
|
|
|
|
removeProductFacetValue(facetValueId: string) {
|
|
|
- const productGroup = this.getProductFormGroup();
|
|
|
- const currentFacetValueIds = productGroup.value.facetValueIds;
|
|
|
+ const productGroup = this.detailForm;
|
|
|
+ const currentFacetValueIds = productGroup.value.facetValueIds ?? [];
|
|
|
productGroup.patchValue({
|
|
|
facetValueIds: currentFacetValueIds.filter(id => id !== facetValueId),
|
|
|
});
|
|
|
productGroup.markAsDirty();
|
|
|
}
|
|
|
|
|
|
- variantsToCreateAreValid(): boolean {
|
|
|
- return (
|
|
|
- 0 < this.createVariantsConfig.variants.length &&
|
|
|
- this.createVariantsConfig.variants.every(v => v.sku !== '')
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
private displayFacetValueModal(): Observable<string[] | undefined> {
|
|
|
return this.modalService
|
|
|
.fromComponent(ApplyFacetDialogComponent, {
|
|
|
@@ -437,33 +310,39 @@ export class ProductDetailComponent
|
|
|
}
|
|
|
|
|
|
create() {
|
|
|
- const productGroup = this.getProductFormGroup();
|
|
|
+ const productGroup = this.detailForm;
|
|
|
if (!productGroup.dirty) {
|
|
|
return;
|
|
|
}
|
|
|
- combineLatest(this.product$, this.languageCode$)
|
|
|
- .pipe(
|
|
|
- take(1),
|
|
|
- mergeMap(([product, languageCode]) => {
|
|
|
- const newProduct = this.getUpdatedProduct(
|
|
|
- product,
|
|
|
- productGroup as UntypedFormGroup,
|
|
|
- languageCode,
|
|
|
- ) as CreateProductInput;
|
|
|
- return this.productDetailService.createProductWithVariants(
|
|
|
- newProduct,
|
|
|
- this.createVariantsConfig,
|
|
|
- languageCode,
|
|
|
- );
|
|
|
- }),
|
|
|
- )
|
|
|
+
|
|
|
+ const newProduct = this.getUpdatedProduct(
|
|
|
+ {
|
|
|
+ id: '',
|
|
|
+ createdAt: '',
|
|
|
+ updatedAt: '',
|
|
|
+ enabled: true,
|
|
|
+ languageCode: this.languageCode,
|
|
|
+ name: '',
|
|
|
+ slug: '',
|
|
|
+ featuredAsset: null,
|
|
|
+ assets: [],
|
|
|
+ description: '',
|
|
|
+ translations: [],
|
|
|
+ optionGroups: [],
|
|
|
+ facetValues: [],
|
|
|
+ channels: [],
|
|
|
+ },
|
|
|
+ productGroup as UntypedFormGroup,
|
|
|
+ this.languageCode,
|
|
|
+ ) as CreateProductInput;
|
|
|
+ this.productDetailService
|
|
|
+ .createProductWithVariants(newProduct, this.createVariantsConfig, this.languageCode)
|
|
|
.subscribe(
|
|
|
({ createProductVariants, productId }) => {
|
|
|
this.notificationService.success(_('common.notify-create-success'), {
|
|
|
entity: 'Product',
|
|
|
});
|
|
|
this.assetChanges = {};
|
|
|
- this.variantAssetChanges = {};
|
|
|
this.detailForm.markAsPristine();
|
|
|
this.router.navigate(['../', productId], { relativeTo: this.route });
|
|
|
},
|
|
|
@@ -478,11 +357,11 @@ export class ProductDetailComponent
|
|
|
}
|
|
|
|
|
|
save() {
|
|
|
- combineLatest(this.product$, this.languageCode$, this.channelPriceIncludesTax$)
|
|
|
+ combineLatest(this.entity$, this.languageCode$)
|
|
|
.pipe(
|
|
|
take(1),
|
|
|
- mergeMap(([product, languageCode, priceIncludesTax]) => {
|
|
|
- const productGroup = this.getProductFormGroup();
|
|
|
+ mergeMap(([product, languageCode]) => {
|
|
|
+ const productGroup = this.detailForm;
|
|
|
let productInput: UpdateProductInput | undefined;
|
|
|
let variantsInput: UpdateProductVariantInput[] | undefined;
|
|
|
|
|
|
@@ -493,15 +372,6 @@ export class ProductDetailComponent
|
|
|
languageCode,
|
|
|
) as UpdateProductInput;
|
|
|
}
|
|
|
- const variantsArray = this.detailForm.get('variants');
|
|
|
- if ((variantsArray && variantsArray.dirty) || this.variantAssetsChanged()) {
|
|
|
- variantsInput = this.getUpdatedProductVariants(
|
|
|
- product,
|
|
|
- variantsArray as UntypedFormArray,
|
|
|
- languageCode,
|
|
|
- priceIncludesTax,
|
|
|
- );
|
|
|
- }
|
|
|
|
|
|
return this.productDetailService.updateProduct({
|
|
|
product,
|
|
|
@@ -518,8 +388,6 @@ export class ProductDetailComponent
|
|
|
this.updateSlugAfterSave(result);
|
|
|
this.detailForm.markAsPristine();
|
|
|
this.assetChanges = {};
|
|
|
- this.variantAssetChanges = {};
|
|
|
- this.variantFacetValueChanges = {};
|
|
|
this.notificationService.success(_('common.notify-update-success'), {
|
|
|
entity: 'Product',
|
|
|
});
|
|
|
@@ -546,82 +414,21 @@ export class ProductDetailComponent
|
|
|
) {
|
|
|
const currentTranslation = findTranslation(product, languageCode);
|
|
|
this.detailForm.patchValue({
|
|
|
- product: {
|
|
|
- enabled: product.enabled,
|
|
|
- name: currentTranslation ? currentTranslation.name : '',
|
|
|
- slug: currentTranslation ? currentTranslation.slug : '',
|
|
|
- description: currentTranslation ? currentTranslation.description : '',
|
|
|
- facetValueIds: product.facetValues.map(fv => fv.id),
|
|
|
- },
|
|
|
+ enabled: product.enabled,
|
|
|
+ name: currentTranslation ? currentTranslation.name : '',
|
|
|
+ slug: currentTranslation ? currentTranslation.slug : '',
|
|
|
+ description: currentTranslation ? currentTranslation.description : '',
|
|
|
+ facetValueIds: product.facetValues.map(fv => fv.id),
|
|
|
});
|
|
|
|
|
|
if (this.customFields.length) {
|
|
|
this.setCustomFieldFormValues(
|
|
|
this.customFields,
|
|
|
- this.detailForm.get(['product', 'customFields']),
|
|
|
+ this.detailForm.get(['customFields']),
|
|
|
product,
|
|
|
currentTranslation,
|
|
|
);
|
|
|
}
|
|
|
- this.buildVariantFormArray(product.variantList.items, languageCode);
|
|
|
- }
|
|
|
-
|
|
|
- private buildVariantFormArray(variants: ProductVariantFragment[], languageCode: LanguageCode) {
|
|
|
- const variantsFormArray = this.detailForm.get('variants') as UntypedFormArray;
|
|
|
- variants.forEach((variant, i) => {
|
|
|
- const variantTranslation = findTranslation(variant, languageCode);
|
|
|
- const pendingFacetValueChanges = this.variantFacetValueChanges[variant.id];
|
|
|
- const facetValueIds = pendingFacetValueChanges
|
|
|
- ? pendingFacetValueChanges.map(fv => fv.id)
|
|
|
- : variant.facetValues.map(fv => fv.id);
|
|
|
- const group: VariantFormValue = {
|
|
|
- id: variant.id,
|
|
|
- enabled: variant.enabled,
|
|
|
- sku: variant.sku,
|
|
|
- name: variantTranslation ? variantTranslation.name : '',
|
|
|
- price: variant.price,
|
|
|
- priceWithTax: variant.priceWithTax,
|
|
|
- taxCategoryId: variant.taxCategory.id,
|
|
|
- stockOnHand: variant.stockOnHand,
|
|
|
- useGlobalOutOfStockThreshold: variant.useGlobalOutOfStockThreshold,
|
|
|
- outOfStockThreshold: variant.outOfStockThreshold,
|
|
|
- trackInventory: variant.trackInventory,
|
|
|
- facetValueIds,
|
|
|
- };
|
|
|
-
|
|
|
- let variantFormGroup = variantsFormArray.controls.find(c => c.value.id === variant.id) as
|
|
|
- | UntypedFormGroup
|
|
|
- | undefined;
|
|
|
- if (variantFormGroup) {
|
|
|
- if (variantFormGroup.pristine) {
|
|
|
- variantFormGroup.patchValue(group);
|
|
|
- }
|
|
|
- } else {
|
|
|
- variantFormGroup = this.formBuilder.group({
|
|
|
- ...group,
|
|
|
- facetValueIds: this.formBuilder.control(facetValueIds),
|
|
|
- });
|
|
|
- variantsFormArray.insert(i, variantFormGroup);
|
|
|
- }
|
|
|
- if (this.customVariantFields.length) {
|
|
|
- let customFieldsGroup = variantFormGroup.get(['customFields']) as
|
|
|
- | UntypedFormGroup
|
|
|
- | undefined;
|
|
|
-
|
|
|
- if (!customFieldsGroup) {
|
|
|
- customFieldsGroup = this.formBuilder.group(
|
|
|
- this.customVariantFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
|
|
|
- );
|
|
|
- variantFormGroup.addControl('customFields', customFieldsGroup);
|
|
|
- }
|
|
|
- this.setCustomFieldFormValues(
|
|
|
- this.customVariantFields,
|
|
|
- customFieldsGroup,
|
|
|
- variant,
|
|
|
- variantTranslation,
|
|
|
- );
|
|
|
- }
|
|
|
- });
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -629,7 +436,7 @@ export class ProductDetailComponent
|
|
|
* can then be persisted to the API.
|
|
|
*/
|
|
|
private getUpdatedProduct(
|
|
|
- product: NonNullable<GetProductWithVariantsQuery['product']>,
|
|
|
+ product: NonNullable<GetProductDetailQuery['product']>,
|
|
|
productFormGroup: UntypedFormGroup,
|
|
|
languageCode: LanguageCode,
|
|
|
): UpdateProductInput | CreateProductInput {
|
|
|
@@ -653,55 +460,6 @@ export class ProductDetailComponent
|
|
|
} as UpdateProductInput | CreateProductInput;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Given an array of product variants and the values from the detailForm, this method creates an new array
|
|
|
- * which can be persisted to the API.
|
|
|
- */
|
|
|
- private getUpdatedProductVariants(
|
|
|
- product: NonNullable<GetProductWithVariantsQuery['product']>,
|
|
|
- variantsFormArray: UntypedFormArray,
|
|
|
- languageCode: LanguageCode,
|
|
|
- priceIncludesTax: boolean,
|
|
|
- ): UpdateProductVariantInput[] {
|
|
|
- const dirtyFormControls = variantsFormArray.controls.filter(c => c.dirty);
|
|
|
- const dirtyVariants = dirtyFormControls
|
|
|
- .map(c => this.productVariantMap.get(c.value.id))
|
|
|
- .filter(notNullOrUndefined);
|
|
|
- const dirtyVariantValues = dirtyFormControls.map(c => c.value);
|
|
|
-
|
|
|
- if (dirtyVariants.length !== dirtyVariantValues.length) {
|
|
|
- throw new Error(_(`error.product-variant-form-values-do-not-match`));
|
|
|
- }
|
|
|
- return dirtyVariants
|
|
|
- .map((variant, i) => {
|
|
|
- const formValue: VariantFormValue = dirtyVariantValues.find(value => value.id === variant.id);
|
|
|
- const result: UpdateProductVariantInput = createUpdatedTranslatable({
|
|
|
- translatable: variant,
|
|
|
- updatedFields: formValue,
|
|
|
- customFieldConfig: this.customVariantFields,
|
|
|
- languageCode,
|
|
|
- defaultTranslation: {
|
|
|
- languageCode,
|
|
|
- name: '',
|
|
|
- },
|
|
|
- });
|
|
|
- result.taxCategoryId = formValue.taxCategoryId;
|
|
|
- result.facetValueIds = formValue.facetValueIds;
|
|
|
- result.price = priceIncludesTax ? formValue.priceWithTax : formValue.price;
|
|
|
- const assetChanges = this.variantAssetChanges[variant.id];
|
|
|
- if (assetChanges) {
|
|
|
- result.featuredAssetId = assetChanges.featuredAsset?.id;
|
|
|
- result.assetIds = assetChanges.assets?.map(a => a.id);
|
|
|
- }
|
|
|
- return result;
|
|
|
- })
|
|
|
- .filter(notNullOrUndefined);
|
|
|
- }
|
|
|
-
|
|
|
- private getProductFormGroup(): UntypedFormGroup {
|
|
|
- return this.detailForm.get('product') as UntypedFormGroup;
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* The server may alter the slug value in order to normalize and ensure uniqueness upon saving.
|
|
|
*/
|