|
|
@@ -1,9 +1,8 @@
|
|
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
|
|
+import { FormControl } from '@angular/forms';
|
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
|
|
import {
|
|
|
- CreateProductOptionGroupMutation,
|
|
|
- CreateProductOptionInput,
|
|
|
CurrencyCode,
|
|
|
DataService,
|
|
|
DeactivateAware,
|
|
|
@@ -13,17 +12,15 @@ import {
|
|
|
LanguageCode,
|
|
|
ModalService,
|
|
|
NotificationService,
|
|
|
- ProductOptionGroupWithOptionsFragment,
|
|
|
+ SelectionManager,
|
|
|
} from '@vendure/admin-ui/core';
|
|
|
import { normalizeString } from '@vendure/common/lib/normalize-string';
|
|
|
-import { pick } from '@vendure/common/lib/pick';
|
|
|
-import { generateAllCombinations, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
|
|
|
-import { unique } from '@vendure/common/lib/unique';
|
|
|
-import { EMPTY, forkJoin, Observable, of } from 'rxjs';
|
|
|
-import { defaultIfEmpty, filter, map, mergeMap, switchMap } from 'rxjs/operators';
|
|
|
+import { EMPTY, Observable, Subject } from 'rxjs';
|
|
|
+import { map, startWith, switchMap } from 'rxjs/operators';
|
|
|
|
|
|
import { ProductDetailService } from '../../providers/product-detail/product-detail.service';
|
|
|
-import { ConfirmVariantDeletionDialogComponent } from '../confirm-variant-deletion-dialog/confirm-variant-deletion-dialog.component';
|
|
|
+import { CreateProductOptionGroupDialogComponent } from '../create-product-option-group-dialog/create-product-option-group-dialog.component';
|
|
|
+import { CreateProductVariantDialogComponent } from '../create-product-variant-dialog/create-product-variant-dialog.component';
|
|
|
|
|
|
export class GeneratedVariant {
|
|
|
isDefault: boolean;
|
|
|
@@ -48,7 +45,7 @@ interface OptionGroupUiModel {
|
|
|
name: string;
|
|
|
locked: boolean;
|
|
|
values: Array<{
|
|
|
- id?: string;
|
|
|
+ id: string;
|
|
|
name: string;
|
|
|
locked: boolean;
|
|
|
}>;
|
|
|
@@ -63,10 +60,21 @@ interface OptionGroupUiModel {
|
|
|
export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
|
|
|
formValueChanged = false;
|
|
|
optionsChanged = false;
|
|
|
- generatedVariants: GeneratedVariant[] = [];
|
|
|
optionGroups: OptionGroupUiModel[];
|
|
|
product: NonNullable<GetProductVariantOptionsQuery['product']>;
|
|
|
+ variants$: Observable<NonNullable<GetProductVariantOptionsQuery['product']>['variants']>;
|
|
|
+ optionGroups$: Observable<NonNullable<GetProductVariantOptionsQuery['product']>['optionGroups']>;
|
|
|
+ totalItems$: Observable<number>;
|
|
|
currencyCode: CurrencyCode;
|
|
|
+ itemsPerPage = 100;
|
|
|
+ currentPage = 1;
|
|
|
+ searchTermControl = new FormControl('');
|
|
|
+ selectionManager = new SelectionManager<any>({
|
|
|
+ multiSelect: true,
|
|
|
+ itemsAreEqual: (a, b) => a.id === b.id,
|
|
|
+ additiveMode: true,
|
|
|
+ });
|
|
|
+ private refresh$ = new Subject<void>();
|
|
|
private languageCode: LanguageCode;
|
|
|
|
|
|
constructor(
|
|
|
@@ -78,12 +86,62 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
|
|
|
) {}
|
|
|
|
|
|
ngOnInit() {
|
|
|
- this.initOptionsAndVariants();
|
|
|
this.languageCode =
|
|
|
(this.route.snapshot.paramMap.get('lang') as LanguageCode) || getDefaultUiLanguage();
|
|
|
this.dataService.settings.getActiveChannel().single$.subscribe(data => {
|
|
|
this.currencyCode = data.activeChannel.currencyCode;
|
|
|
});
|
|
|
+
|
|
|
+ const product$ = this.refresh$.pipe(
|
|
|
+ switchMap(() =>
|
|
|
+ this.dataService.product
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
+ .getProductVariantsOptions(this.route.parent?.snapshot.paramMap.get('id')!)
|
|
|
+ .mapSingle(data => data.product),
|
|
|
+ ),
|
|
|
+ startWith(this.route.snapshot.data.product),
|
|
|
+ );
|
|
|
+
|
|
|
+ this.variants$ = product$.pipe(
|
|
|
+ switchMap(product =>
|
|
|
+ this.searchTermControl.valueChanges.pipe(
|
|
|
+ startWith(''),
|
|
|
+ map(term =>
|
|
|
+ term
|
|
|
+ ? product.variants.filter(v => v.name.toLowerCase().includes(term.toLowerCase()))
|
|
|
+ : product.variants,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ this.optionGroups$ = product$.pipe(map(product => product.optionGroups));
|
|
|
+ this.totalItems$ = this.variants$.pipe(map(variants => variants.length));
|
|
|
+
|
|
|
+ product$.subscribe(p => {
|
|
|
+ this.product = p;
|
|
|
+ const allUsedOptionIds = p.variants.map(v => v.options.map(option => option.id)).flat();
|
|
|
+ const allUsedOptionGroupIds = p.variants.map(v => v.options.map(option => option.groupId)).flat();
|
|
|
+ this.optionGroups = p.optionGroups.map(og => ({
|
|
|
+ id: og.id,
|
|
|
+ isNew: false,
|
|
|
+ name: og.name,
|
|
|
+ locked: allUsedOptionGroupIds.includes(og.id),
|
|
|
+ values: og.options.map(o => ({
|
|
|
+ id: o.id,
|
|
|
+ name: o.name,
|
|
|
+ locked: allUsedOptionIds.includes(o.id),
|
|
|
+ })),
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ setItemsPerPage(itemsPerPage: number) {
|
|
|
+ this.itemsPerPage = itemsPerPage;
|
|
|
+ this.currentPage = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ setPageNumber(page: number) {
|
|
|
+ this.currentPage = page;
|
|
|
}
|
|
|
|
|
|
onFormChanged(variantInfo: GeneratedVariant) {
|
|
|
@@ -95,37 +153,99 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
|
|
|
return !this.formValueChanged;
|
|
|
}
|
|
|
|
|
|
- getVariantsToAdd() {
|
|
|
- return this.generatedVariants.filter(v => !v.existing && v.enabled);
|
|
|
+ addOptionGroup() {
|
|
|
+ this.modalService
|
|
|
+ .fromComponent(CreateProductOptionGroupDialogComponent, {
|
|
|
+ locals: {
|
|
|
+ languageCode: this.languageCode,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ .pipe(
|
|
|
+ switchMap(result => {
|
|
|
+ if (result) {
|
|
|
+ return this.dataService.product.createProductOptionGroups(result).pipe(
|
|
|
+ switchMap(({ createProductOptionGroup }) =>
|
|
|
+ this.dataService.product.addOptionGroupToProduct({
|
|
|
+ optionGroupId: createProductOptionGroup.id,
|
|
|
+ productId: this.product.id,
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ return EMPTY;
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ .subscribe(result => {
|
|
|
+ this.notificationService.success(_('common.notify-create-success'), {
|
|
|
+ entity: 'ProductOptionGroup',
|
|
|
+ });
|
|
|
+ this.refresh$.next();
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- getVariantName(variant: GeneratedVariant) {
|
|
|
- return variant.options.length === 0
|
|
|
- ? _('catalog.default-variant')
|
|
|
- : variant.options.map(o => o.name).join(' ');
|
|
|
+ removeOptionGroup(
|
|
|
+ optionGroup: NonNullable<GetProductVariantOptionsQuery['product']>['optionGroups'][number],
|
|
|
+ ) {
|
|
|
+ const id = optionGroup.id;
|
|
|
+ this.modalService
|
|
|
+ .dialog({
|
|
|
+ title: _('catalog.confirm-delete-product-option-group'),
|
|
|
+ translationVars: { name: optionGroup.name },
|
|
|
+ buttons: [
|
|
|
+ { type: 'secondary', label: _('common.cancel') },
|
|
|
+ { type: 'danger', label: _('common.delete'), returnValue: true },
|
|
|
+ ],
|
|
|
+ })
|
|
|
+ .pipe(
|
|
|
+ switchMap(val => {
|
|
|
+ if (val) {
|
|
|
+ return this.dataService.product.removeOptionGroupFromProduct({
|
|
|
+ optionGroupId: id,
|
|
|
+ productId: this.product.id,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ return EMPTY;
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ .subscribe(({ removeOptionGroupFromProduct }) => {
|
|
|
+ if (removeOptionGroupFromProduct.__typename === 'Product') {
|
|
|
+ this.notificationService.success(_('common.notify-delete-success'), {
|
|
|
+ entity: 'ProductOptionGroup',
|
|
|
+ });
|
|
|
+ this.refresh$.next();
|
|
|
+ } else if (removeOptionGroupFromProduct.__typename === 'ProductOptionInUseError') {
|
|
|
+ this.notificationService.error(removeOptionGroupFromProduct.message ?? '');
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- addOptionGroup() {
|
|
|
- this.optionGroups.push({
|
|
|
- isNew: true,
|
|
|
- locked: false,
|
|
|
- name: '',
|
|
|
- values: [],
|
|
|
- });
|
|
|
- this.optionsChanged = true;
|
|
|
+ addOption(index: number, optionName: string) {
|
|
|
+ const group = this.optionGroups[index];
|
|
|
+ if (group && group.id) {
|
|
|
+ this.dataService.product
|
|
|
+ .addOptionToGroup({
|
|
|
+ productOptionGroupId: group.id,
|
|
|
+ code: normalizeString(optionName, '-'),
|
|
|
+ translations: [{ name: optionName, languageCode: this.languageCode }],
|
|
|
+ })
|
|
|
+ .subscribe(({ createProductOption }) => {
|
|
|
+ this.notificationService.success(_('common.notify-create-success'), {
|
|
|
+ entity: 'ProductOption',
|
|
|
+ });
|
|
|
+ this.refresh$.next();
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- removeOptionGroup(optionGroup: OptionGroupUiModel) {
|
|
|
- const id = optionGroup.id;
|
|
|
- if (optionGroup.isNew) {
|
|
|
- this.optionGroups = this.optionGroups.filter(og => og !== optionGroup);
|
|
|
- this.generateVariants();
|
|
|
- this.optionsChanged = true;
|
|
|
- } else if (id) {
|
|
|
+ removeOption(index: number, { id, name }: { id: string; name: string }) {
|
|
|
+ const optionGroup = this.optionGroups[index];
|
|
|
+ if (optionGroup) {
|
|
|
this.modalService
|
|
|
.dialog({
|
|
|
- title: _('catalog.confirm-delete-product-option-group'),
|
|
|
- translationVars: { name: optionGroup.name },
|
|
|
+ title: _('catalog.confirm-delete-product-option'),
|
|
|
+ translationVars: { name },
|
|
|
buttons: [
|
|
|
{ type: 'secondary', label: _('common.cancel') },
|
|
|
{ type: 'danger', label: _('common.delete'), returnValue: true },
|
|
|
@@ -134,139 +254,31 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
|
|
|
.pipe(
|
|
|
switchMap(val => {
|
|
|
if (val) {
|
|
|
- return this.dataService.product.removeOptionGroupFromProduct({
|
|
|
- optionGroupId: id,
|
|
|
- productId: this.product.id,
|
|
|
- });
|
|
|
+ return this.dataService.product.deleteProductOption(id);
|
|
|
} else {
|
|
|
return EMPTY;
|
|
|
}
|
|
|
}),
|
|
|
)
|
|
|
- .subscribe(({ removeOptionGroupFromProduct }) => {
|
|
|
- if (removeOptionGroupFromProduct.__typename === 'Product') {
|
|
|
+ .subscribe(({ deleteProductOption }) => {
|
|
|
+ if (deleteProductOption.result === DeletionResult.DELETED) {
|
|
|
this.notificationService.success(_('common.notify-delete-success'), {
|
|
|
- entity: 'ProductOptionGroup',
|
|
|
+ entity: 'ProductOption',
|
|
|
});
|
|
|
- this.initOptionsAndVariants();
|
|
|
- this.optionsChanged = true;
|
|
|
- } else if (removeOptionGroupFromProduct.__typename === 'ProductOptionInUseError') {
|
|
|
- this.notificationService.error(removeOptionGroupFromProduct.message ?? '');
|
|
|
+ optionGroup.values = optionGroup.values.filter(v => v.id !== id);
|
|
|
+ this.refresh$.next();
|
|
|
+ } else {
|
|
|
+ this.notificationService.error(deleteProductOption.message ?? '');
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- addOption(index: number, optionName: string) {
|
|
|
- const group = this.optionGroups[index];
|
|
|
- if (group) {
|
|
|
- group.values.push({ name: optionName, locked: false });
|
|
|
- this.generateVariants();
|
|
|
- this.optionsChanged = true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- removeOption(index: number, { id, name }: { id?: string; name: string }) {
|
|
|
- const optionGroup = this.optionGroups[index];
|
|
|
- if (optionGroup) {
|
|
|
- if (!id) {
|
|
|
- optionGroup.values = optionGroup.values.filter(v => v.name !== name);
|
|
|
- this.generateVariants();
|
|
|
- } else {
|
|
|
- this.modalService
|
|
|
- .dialog({
|
|
|
- title: _('catalog.confirm-delete-product-option'),
|
|
|
- translationVars: { name },
|
|
|
- buttons: [
|
|
|
- { type: 'secondary', label: _('common.cancel') },
|
|
|
- { type: 'danger', label: _('common.delete'), returnValue: true },
|
|
|
- ],
|
|
|
- })
|
|
|
- .pipe(
|
|
|
- switchMap(val => {
|
|
|
- if (val) {
|
|
|
- return this.dataService.product.deleteProductOption(id);
|
|
|
- } else {
|
|
|
- return EMPTY;
|
|
|
- }
|
|
|
- }),
|
|
|
- )
|
|
|
- .subscribe(({ deleteProductOption }) => {
|
|
|
- if (deleteProductOption.result === DeletionResult.DELETED) {
|
|
|
- this.notificationService.success(_('common.notify-delete-success'), {
|
|
|
- entity: 'ProductOption',
|
|
|
- });
|
|
|
- optionGroup.values = optionGroup.values.filter(v => v.id !== id);
|
|
|
- this.generateVariants();
|
|
|
- this.optionsChanged = true;
|
|
|
- } else {
|
|
|
- this.notificationService.error(deleteProductOption.message ?? '');
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- generateVariants() {
|
|
|
- const groups = this.optionGroups.map(g => g.values);
|
|
|
- const previousVariants = this.generatedVariants;
|
|
|
- const generatedVariantFactory = (
|
|
|
- isDefault: boolean,
|
|
|
- options: GeneratedVariant['options'],
|
|
|
- existingVariant?: NonNullable<GetProductVariantOptionsQuery['product']>['variants'][number],
|
|
|
- prototypeVariant?: NonNullable<GetProductVariantOptionsQuery['product']>['variants'][number],
|
|
|
- ): GeneratedVariant => {
|
|
|
- const prototype = this.getVariantPrototype(options, previousVariants);
|
|
|
- return new GeneratedVariant({
|
|
|
- enabled: true,
|
|
|
- existing: !!existingVariant,
|
|
|
- productVariantId: existingVariant?.id,
|
|
|
- isDefault,
|
|
|
- options,
|
|
|
- price: existingVariant?.price ?? prototypeVariant?.price ?? prototype.price,
|
|
|
- sku: existingVariant?.sku ?? prototypeVariant?.sku ?? prototype.sku,
|
|
|
- stock: existingVariant?.stockOnHand ?? prototypeVariant?.stockOnHand ?? prototype.stock,
|
|
|
- });
|
|
|
- };
|
|
|
- this.generatedVariants = groups.length
|
|
|
- ? generateAllCombinations(groups).map(options => {
|
|
|
- const existingVariant = this.product.variants.find(v =>
|
|
|
- this.optionsAreEqual(v.options, options),
|
|
|
- );
|
|
|
- const prototypeVariant = this.product.variants.find(v =>
|
|
|
- this.optionsAreSubset(v.options, options),
|
|
|
- );
|
|
|
- return generatedVariantFactory(false, options, existingVariant, prototypeVariant);
|
|
|
- })
|
|
|
- : [generatedVariantFactory(true, [], this.product.variants[0])];
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Returns one of the existing variants to base the newly-generated variant's
|
|
|
- * details off.
|
|
|
- */
|
|
|
- private getVariantPrototype(
|
|
|
- options: GeneratedVariant['options'],
|
|
|
- previousVariants: GeneratedVariant[],
|
|
|
- ): Pick<GeneratedVariant, 'sku' | 'price' | 'stock'> {
|
|
|
- const variantsWithSimilarOptions = previousVariants.filter(v =>
|
|
|
- options.map(o => o.name).filter(name => v.options.map(o => o.name).includes(name)),
|
|
|
- );
|
|
|
- if (variantsWithSimilarOptions.length) {
|
|
|
- return pick(previousVariants[0], ['sku', 'price', 'stock']);
|
|
|
- }
|
|
|
- return {
|
|
|
- sku: '',
|
|
|
- price: 0,
|
|
|
- stock: 0,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- deleteVariant(id: string, options: GeneratedVariant['options']) {
|
|
|
+ deleteVariant(variant: NonNullable<GetProductVariantOptionsQuery['product']>['variants'][number]) {
|
|
|
this.modalService
|
|
|
.dialog({
|
|
|
title: _('catalog.confirm-delete-product-variant'),
|
|
|
- translationVars: { name: options.map(o => o.name).join(' ') },
|
|
|
+ translationVars: { name: variant.name },
|
|
|
buttons: [
|
|
|
{ type: 'secondary', label: _('common.cancel') },
|
|
|
{ type: 'danger', label: _('common.delete'), returnValue: true },
|
|
|
@@ -274,16 +286,17 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
|
|
|
})
|
|
|
.pipe(
|
|
|
switchMap(response =>
|
|
|
- response ? this.productDetailService.deleteProductVariant(id, this.product.id) : EMPTY,
|
|
|
+ response
|
|
|
+ ? this.productDetailService.deleteProductVariant(variant.id, this.product.id)
|
|
|
+ : EMPTY,
|
|
|
),
|
|
|
- switchMap(() => this.reFetchProduct(null)),
|
|
|
)
|
|
|
.subscribe(
|
|
|
() => {
|
|
|
this.notificationService.success(_('common.notify-delete-success'), {
|
|
|
entity: 'ProductVariant',
|
|
|
});
|
|
|
- this.initOptionsAndVariants();
|
|
|
+ this.refresh$.next();
|
|
|
},
|
|
|
err => {
|
|
|
this.notificationService.error(_('common.notify-delete-error'), {
|
|
|
@@ -293,237 +306,34 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- save() {
|
|
|
- this.optionGroups = this.optionGroups.filter(g => g.values.length);
|
|
|
- const newOptionGroups = this.optionGroups
|
|
|
- .filter(og => og.isNew)
|
|
|
- .map(og => ({
|
|
|
- name: og.name,
|
|
|
- values: [],
|
|
|
- }));
|
|
|
-
|
|
|
- this.checkUniqueSkus()
|
|
|
- .pipe(
|
|
|
- mergeMap(() => this.confirmDeletionOfObsoleteVariants()),
|
|
|
- mergeMap(() =>
|
|
|
- this.productDetailService.createProductOptionGroups(newOptionGroups, this.languageCode),
|
|
|
- ),
|
|
|
- mergeMap(createdOptionGroups => this.addOptionGroupsToProduct(createdOptionGroups)),
|
|
|
- mergeMap(createdOptionGroups => this.addNewOptionsToGroups(createdOptionGroups)),
|
|
|
- mergeMap(groupsIds => this.fetchOptionGroups(groupsIds)),
|
|
|
- mergeMap(groups => this.createNewProductVariants(groups)),
|
|
|
- mergeMap(res => this.deleteObsoleteVariants(res.createProductVariants)),
|
|
|
- mergeMap(variants => this.reFetchProduct(variants)),
|
|
|
- )
|
|
|
- .subscribe({
|
|
|
- next: variants => {
|
|
|
- this.formValueChanged = false;
|
|
|
- this.notificationService.success(_('catalog.created-new-variants-success'), {
|
|
|
- count: variants.length,
|
|
|
- });
|
|
|
- this.initOptionsAndVariants();
|
|
|
- this.optionsChanged = false;
|
|
|
+ createNewVariant() {
|
|
|
+ this.modalService
|
|
|
+ .fromComponent(CreateProductVariantDialogComponent, {
|
|
|
+ locals: {
|
|
|
+ product: this.product,
|
|
|
},
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- private checkUniqueSkus() {
|
|
|
- const withDuplicateSkus = this.generatedVariants.filter((variant, index) => (
|
|
|
- variant.enabled &&
|
|
|
- this.generatedVariants.find(gv => gv.sku.trim() === variant.sku.trim() && gv !== variant)
|
|
|
- ));
|
|
|
- if (withDuplicateSkus.length) {
|
|
|
- return this.modalService
|
|
|
- .dialog({
|
|
|
- title: _('catalog.duplicate-sku-warning'),
|
|
|
- body: unique(withDuplicateSkus.map(v => `${v.sku}`)).join(', '),
|
|
|
- buttons: [{ label: _('common.close'), returnValue: false, type: 'primary' }],
|
|
|
- })
|
|
|
- .pipe(mergeMap(res => EMPTY));
|
|
|
- } else {
|
|
|
- return of(true);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private confirmDeletionOfObsoleteVariants(): Observable<boolean> {
|
|
|
- const obsoleteVariants = this.getObsoleteVariants();
|
|
|
- if (obsoleteVariants.length) {
|
|
|
- return this.modalService
|
|
|
- .fromComponent(ConfirmVariantDeletionDialogComponent, {
|
|
|
- locals: {
|
|
|
- variants: obsoleteVariants,
|
|
|
- },
|
|
|
- })
|
|
|
- .pipe(
|
|
|
- mergeMap(res => res === true ? of(true) : EMPTY),
|
|
|
- );
|
|
|
- } else {
|
|
|
- return of(true);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private getObsoleteVariants() {
|
|
|
- return this.product.variants.filter(
|
|
|
- variant => !this.generatedVariants.find(gv => gv.productVariantId === variant.id),
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- private hasOnlyDefaultVariant(product: NonNullable<GetProductVariantOptionsQuery['product']>): boolean {
|
|
|
- return product.variants.length === 1 && product.optionGroups.length === 0;
|
|
|
- }
|
|
|
-
|
|
|
- private addOptionGroupsToProduct(
|
|
|
- createdOptionGroups: Array<CreateProductOptionGroupMutation['createProductOptionGroup']>,
|
|
|
- ): Observable<Array<CreateProductOptionGroupMutation['createProductOptionGroup']>> {
|
|
|
- if (createdOptionGroups.length) {
|
|
|
- return forkJoin(
|
|
|
- createdOptionGroups.map(optionGroup => this.dataService.product.addOptionGroupToProduct({
|
|
|
- productId: this.product.id,
|
|
|
- optionGroupId: optionGroup.id,
|
|
|
- })),
|
|
|
- ).pipe(map(() => createdOptionGroups));
|
|
|
- } else {
|
|
|
- return of([]);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private addNewOptionsToGroups(
|
|
|
- createdOptionGroups: Array<CreateProductOptionGroupMutation['createProductOptionGroup']>,
|
|
|
- ): Observable<string[]> {
|
|
|
- const newOptions: CreateProductOptionInput[] = this.optionGroups
|
|
|
- .map(og => {
|
|
|
- const createdGroup = createdOptionGroups.find(cog => cog.name === og.name);
|
|
|
- const productOptionGroupId = createdGroup ? createdGroup.id : og.id;
|
|
|
- if (!productOptionGroupId) {
|
|
|
- throw new Error('Could not get a productOptionGroupId');
|
|
|
- }
|
|
|
- return og.values
|
|
|
- .filter(v => !v.locked)
|
|
|
- .map(v => ({
|
|
|
- productOptionGroupId,
|
|
|
- code: normalizeString(v.name, '-'),
|
|
|
- translations: [{ name: v.name, languageCode: this.languageCode }],
|
|
|
- }));
|
|
|
})
|
|
|
- .reduce((flat, options) => [...flat, ...options], []);
|
|
|
-
|
|
|
- const allGroupIds = [
|
|
|
- ...createdOptionGroups.map(g => g.id),
|
|
|
- ...this.optionGroups.map(g => g.id).filter(notNullOrUndefined),
|
|
|
- ];
|
|
|
-
|
|
|
- if (newOptions.length) {
|
|
|
- return forkJoin(newOptions.map(input => this.dataService.product.addOptionToGroup(input))).pipe(
|
|
|
- map(() => allGroupIds),
|
|
|
- );
|
|
|
- } else {
|
|
|
- return of(allGroupIds);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private fetchOptionGroups(groupsIds: string[]): Observable<ProductOptionGroupWithOptionsFragment[]> {
|
|
|
- return forkJoin(
|
|
|
- groupsIds.map(id =>
|
|
|
- this.dataService.product
|
|
|
- .getProductOptionGroup(id)
|
|
|
- .mapSingle(data => data.productOptionGroup)
|
|
|
- .pipe(filter(notNullOrUndefined)),
|
|
|
- ),
|
|
|
- ).pipe(defaultIfEmpty([] as ProductOptionGroupWithOptionsFragment[]));
|
|
|
- }
|
|
|
-
|
|
|
- private createNewProductVariants(groups: ProductOptionGroupWithOptionsFragment[]) {
|
|
|
- const options = groups
|
|
|
- .filter(notNullOrUndefined)
|
|
|
- .map(og => og.options)
|
|
|
- .reduce((flat, o) => [...flat, ...o], []);
|
|
|
- const variants = this.generatedVariants
|
|
|
- .filter(v => v.enabled && !v.existing)
|
|
|
- .map(v => {
|
|
|
- const optionIds = groups.map((group, index) => {
|
|
|
- const option = group.options.find(o => o.name === v.options[index].name);
|
|
|
- if (option) {
|
|
|
- return option.id;
|
|
|
+ .pipe(
|
|
|
+ switchMap(result => {
|
|
|
+ if (result) {
|
|
|
+ return this.dataService.product.createProductVariants([result]);
|
|
|
} else {
|
|
|
- throw new Error(`Could not find a matching option for group ${group.name}`);
|
|
|
+ return EMPTY;
|
|
|
}
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ .subscribe(result => {
|
|
|
+ this.notificationService.success(_('common.notify-create-success'), {
|
|
|
+ entity: 'ProductVariant',
|
|
|
});
|
|
|
- return {
|
|
|
- price: v.price,
|
|
|
- sku: v.sku,
|
|
|
- stock: v.stock,
|
|
|
- optionIds,
|
|
|
- };
|
|
|
- });
|
|
|
- return this.productDetailService.createProductVariants(
|
|
|
- this.product,
|
|
|
- variants,
|
|
|
- options,
|
|
|
- this.languageCode,
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- private deleteObsoleteVariants<T>(input: T): Observable<T | T[]> {
|
|
|
- const obsoleteVariants = this.getObsoleteVariants();
|
|
|
- if (obsoleteVariants.length) {
|
|
|
- const deleteOperations = obsoleteVariants.map(v =>
|
|
|
- this.dataService.product.deleteProductVariant(v.id).pipe(map(() => input)),
|
|
|
- );
|
|
|
- return forkJoin(...deleteOperations);
|
|
|
- } else {
|
|
|
- return of(input);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private reFetchProduct<T>(input: T): Observable<T> {
|
|
|
- // Re-fetch the Product to force an update to the view.
|
|
|
- const id = this.route.snapshot.paramMap.get('id');
|
|
|
- if (id) {
|
|
|
- return this.dataService.product.getProduct(id).single$.pipe(map(() => input));
|
|
|
- } else {
|
|
|
- return of(input);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- initOptionsAndVariants() {
|
|
|
- this.dataService.product
|
|
|
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
- .getProductVariantsOptions(this.route.snapshot.paramMap.get('id')!)
|
|
|
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
- .mapSingle(({ product }) => product!)
|
|
|
- .subscribe(p => {
|
|
|
- this.product = p;
|
|
|
- const allUsedOptionIds = p.variants.map(v => v.options.map(option => option.id)).flat();
|
|
|
- const allUsedOptionGroupIds = p.variants
|
|
|
- .map(v => v.options.map(option => option.groupId))
|
|
|
- .flat();
|
|
|
- this.optionGroups = p.optionGroups.map(og => ({
|
|
|
- id: og.id,
|
|
|
- isNew: false,
|
|
|
- name: og.name,
|
|
|
- locked: allUsedOptionGroupIds.includes(og.id),
|
|
|
- values: og.options.map(o => ({
|
|
|
- id: o.id,
|
|
|
- name: o.name,
|
|
|
- locked: allUsedOptionIds.includes(o.id),
|
|
|
- })),
|
|
|
- }));
|
|
|
- this.generateVariants();
|
|
|
+ this.refresh$.next();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private optionsAreEqual(a: Array<{ name: string }>, b: Array<{ name: string }>): boolean {
|
|
|
- return this.toOptionString(a) === this.toOptionString(b);
|
|
|
- }
|
|
|
-
|
|
|
- private optionsAreSubset(a: Array<{ name: string }>, b: Array<{ name: string }>): boolean {
|
|
|
- return this.toOptionString(b).includes(this.toOptionString(a));
|
|
|
- }
|
|
|
-
|
|
|
- private toOptionString(o: Array<{ name: string }>): string {
|
|
|
- return o
|
|
|
- .map(x => x.name)
|
|
|
- .sort()
|
|
|
- .join('|');
|
|
|
+ getOption(
|
|
|
+ variant: NonNullable<GetProductVariantOptionsQuery['product']>['variants'][number],
|
|
|
+ groupId: string,
|
|
|
+ ) {
|
|
|
+ return variant.options.find(o => o.groupId === groupId);
|
|
|
}
|
|
|
}
|