diff --git a/admin-ui/src/app/catalog/catalog.module.ts b/admin-ui/src/app/catalog/catalog.module.ts index 6f408b5e55..5b5cdba593 100644 --- a/admin-ui/src/app/catalog/catalog.module.ts +++ b/admin-ui/src/app/catalog/catalog.module.ts @@ -16,20 +16,16 @@ import { CollectionDetailComponent } from './components/collection-detail/collec import { CollectionListComponent } from './components/collection-list/collection-list.component'; import { CollectionTreeNodeComponent } from './components/collection-tree/collection-tree-node.component'; import { CollectionTreeComponent } from './components/collection-tree/collection-tree.component'; -import { CreateOptionGroupDialogComponent } from './components/create-option-group-dialog/create-option-group-dialog.component'; -import { CreateOptionGroupFormComponent } from './components/create-option-group-form/create-option-group-form.component'; import { FacetDetailComponent } from './components/facet-detail/facet-detail.component'; import { FacetListComponent } from './components/facet-list/facet-list.component'; import { GenerateProductVariantsComponent } from './components/generate-product-variants/generate-product-variants.component'; +import { OptionValueInputComponent } from './components/option-value-input/option-value-input.component'; import { ProductAssetsComponent } from './components/product-assets/product-assets.component'; import { ProductDetailComponent } from './components/product-detail/product-detail.component'; import { ProductListComponent } from './components/product-list/product-list.component'; import { ProductSearchInputComponent } from './components/product-search-input/product-search-input.component'; import { ProductVariantsListComponent } from './components/product-variants-list/product-variants-list.component'; import { ProductVariantsTableComponent } from './components/product-variants-table/product-variants-table.component'; -import { ProductVariantsWizardComponent } from './components/product-variants-wizard/product-variants-wizard.component'; -import { SelectOptionGroupDialogComponent } from './components/select-option-group-dialog/select-option-group-dialog.component'; -import { SelectOptionGroupComponent } from './components/select-option-group/select-option-group.component'; import { VariantPriceDetailComponent } from './components/variant-price-detail/variant-price-detail.component'; import { CollectionResolver } from './providers/routing/collection-resolver'; import { FacetResolver } from './providers/routing/facet-resolver'; @@ -41,11 +37,6 @@ import { ProductResolver } from './providers/routing/product-resolver'; declarations: [ ProductListComponent, ProductDetailComponent, - CreateOptionGroupDialogComponent, - ProductVariantsWizardComponent, - SelectOptionGroupDialogComponent, - CreateOptionGroupFormComponent, - SelectOptionGroupComponent, FacetListComponent, FacetDetailComponent, GenerateProductVariantsComponent, @@ -65,14 +56,9 @@ import { ProductResolver } from './providers/routing/product-resolver'; ProductVariantsTableComponent, AssetPreviewComponent, ProductSearchInputComponent, + OptionValueInputComponent, ], - entryComponents: [ - AssetPickerDialogComponent, - CreateOptionGroupDialogComponent, - SelectOptionGroupDialogComponent, - ApplyFacetDialogComponent, - AssetPreviewComponent, - ], + entryComponents: [AssetPickerDialogComponent, ApplyFacetDialogComponent, AssetPreviewComponent], providers: [ProductResolver, FacetResolver, CollectionResolver], }) export class CatalogModule {} diff --git a/admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.html b/admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.html deleted file mode 100644 index 87c935ab5d..0000000000 --- a/admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.html +++ /dev/null @@ -1,21 +0,0 @@ -{{ 'catalog.create-new-option-group' | translate }} - - - - - {{ 'common.cancel' | translate }} - - {{ 'catalog.create-group' | translate }} - - diff --git a/admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.scss b/admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.ts b/admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.ts deleted file mode 100644 index 3396f01548..0000000000 --- a/admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; - -import { CreateProductOptionGroup } from '../../../common/generated-types'; -import { Dialog } from '../../../shared/providers/modal/modal.service'; -import { CreateOptionGroupFormComponent } from '../create-option-group-form/create-option-group-form.component'; - -@Component({ - selector: 'vdr-create-option-group-dialog', - templateUrl: './create-option-group-dialog.component.html', - styleUrls: ['./create-option-group-dialog.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class CreateOptionGroupDialogComponent implements Dialog { - productId: string; - productName: string; - @ViewChild('createOptionGroupForm', { static: true }) - createOptionGroupForm: CreateOptionGroupFormComponent; - resolveWith: (result?: CreateProductOptionGroup.Mutation) => void; - - createOptionGroup() { - this.createOptionGroupForm.createOptionGroup().subscribe(data => this.resolveWith(data)); - } - - cancel() { - this.resolveWith(); - } -} diff --git a/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.html b/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.html deleted file mode 100644 index 4f81e858a1..0000000000 --- a/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - diff --git a/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.scss b/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.spec.ts b/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.spec.ts deleted file mode 100644 index 330435d5e9..0000000000 --- a/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; - -import { TestingCommonModule } from '../../../../testing/testing-common.module'; - -import { CreateOptionGroupFormComponent } from './create-option-group-form.component'; - -describe('CreateOptionGroupFormComponent', () => { - let component: CreateOptionGroupFormComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [TestingCommonModule, ReactiveFormsModule], - declarations: [CreateOptionGroupFormComponent], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CreateOptionGroupFormComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.ts b/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.ts deleted file mode 100644 index f0b3c8e4a8..0000000000 --- a/admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { Observable } from 'rxjs'; -import { normalizeString } from 'shared/normalize-string'; - -import { - CreateProductOptionGroup, - CreateProductOptionGroupInput, - CreateProductOptionInput, -} from '../../../common/generated-types'; -import { getDefaultLanguage } from '../../../common/utilities/get-default-language'; -import { DataService } from '../../../data/providers/data.service'; - -@Component({ - selector: 'vdr-create-option-group-form', - templateUrl: './create-option-group-form.component.html', - styleUrls: ['./create-option-group-form.component.scss'], -}) -export class CreateOptionGroupFormComponent implements OnInit { - @Input() productName = ''; - @Input() productId: string; - optionGroupForm: FormGroup; - readonly defaultLanguage = getDefaultLanguage(); - - constructor(private formBuilder: FormBuilder, private dataService: DataService) {} - - ngOnInit() { - this.optionGroupForm = this.formBuilder.group({ - name: '', - code: '', - options: '', - }); - } - - resetForm() { - this.optionGroupForm.reset(); - } - - updateCode(nameValue: string) { - const codeControl = this.optionGroupForm.get('code'); - if (codeControl && codeControl.pristine) { - codeControl.setValue(normalizeString(`${this.productName} ${nameValue}`, '-')); - } - } - - createOptionGroup(): Observable { - return this.dataService.product.createProductOptionGroups(this.createGroupFromForm()); - } - - private createGroupFromForm(): CreateProductOptionGroupInput { - const name = this.optionGroupForm.value.name; - const code = this.optionGroupForm.value.code; - const rawOptions = this.optionGroupForm.value.options; - return { - code, - translations: [ - { - languageCode: getDefaultLanguage(), - name, - }, - ], - options: this.createGroupOptions(rawOptions), - }; - } - - private createGroupOptions(rawOptions: string): CreateProductOptionInput[] { - return rawOptions - .split('\n') - .map(line => line.trim()) - .map(name => { - return { - code: normalizeString(name, '-'), - translations: [ - { - languageCode: getDefaultLanguage(), - name, - }, - ], - }; - }); - } -} diff --git a/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.html b/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.html index 741d7c66fe..82f3c576a1 100644 --- a/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.html +++ b/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.html @@ -1,17 +1,99 @@ - - - - {{ 'catalog.generate-product-variants' | translate }} - - - - - {{ 'catalog.generate-variants-default-only' | translate }} + + + {{ 'catalog.option' | translate }} + + + + {{ 'catalog.option-values' | translate }} + + + + + - - {{ 'catalog.generate-variants-with-options' | translate }} - - - + + + + + {{ 'catalog.add-option' | translate }} + - + + + + + {{ 'common.create' | translate }} + {{ 'catalog.variant' | translate }} + {{ 'catalog.sku' | translate }} + {{ 'catalog.price' | translate }} + {{ 'catalog.stock-on-hand' | translate }} + + + + + + + + {{ variant.values.join(' ') }} + + + + + + + + + + + + + + + + + + + diff --git a/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.scss b/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.scss index 7f26ddcb55..02134a413e 100644 --- a/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.scss +++ b/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.scss @@ -1,3 +1,32 @@ +@import "variables"; + :host { display: block; + margin-bottom: 120px; +} + +.option-groups { + display: flex; +} + +::ng-deep ng-dropdown-panel { + display: none; +} + +.values { + flex: 1; + margin: 0 6px; +} + +.remove-group { + padding-top: 18px; +} + +.variants-preview { + tr.disabled { + td { + background-color: $color-grey-100; + color: $color-grey-400; + } + } } diff --git a/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.ts b/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.ts index fbb157be6a..d454e7d6d2 100644 --- a/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.ts +++ b/admin-ui/src/app/catalog/components/generate-product-variants/generate-product-variants.component.ts @@ -1,29 +1,97 @@ -import { Component, Input, ViewChild } from '@angular/core'; +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { generateAllCombinations } from 'shared/shared-utils'; -import { ProductWithVariants } from '../../../common/generated-types'; +import { CurrencyCode } from '../../../common/generated-types'; import { DataService } from '../../../data/providers/data.service'; -import { ProductVariantsWizardComponent } from '../product-variants-wizard/product-variants-wizard.component'; +import { OptionValueInputComponent } from '../option-value-input/option-value-input.component'; + +const DEFAULT_VARIANT_CODE = '__DEFAULT_VARIANT__'; +export type CreateVariantValues = { + optionValues: string[]; + enabled: boolean; + sku: string; + price: number; + stock: number; +}; +export type CreateProductVariantsConfig = { + groups: Array<{ name: string; values: string[] }>; + variants: CreateVariantValues[]; +}; @Component({ selector: 'vdr-generate-product-variants', templateUrl: './generate-product-variants.component.html', styleUrls: ['./generate-product-variants.component.scss'], }) -export class GenerateProductVariantsComponent { - @Input() product: ProductWithVariants.Fragment; - @ViewChild('productVariantsWizard', { static: true }) - productVariantsWizard: ProductVariantsWizardComponent; +export class GenerateProductVariantsComponent implements OnInit { + @Output() variantsChange = new EventEmitter(); + optionGroups: Array<{ name: string; values: string[] }> = []; + currencyCode: CurrencyCode; + variants: Array<{ id: string; values: string[] }>; + variantFormValues: { [id: string]: CreateVariantValues } = {}; constructor(private dataService: DataService) {} - startProductVariantsWizard() { - this.productVariantsWizard.start().subscribe(({ defaultPrice, defaultSku }) => { - this.generateProductVariants(defaultPrice, defaultSku); + ngOnInit() { + this.dataService.settings.getActiveChannel().single$.subscribe(data => { + this.currencyCode = data.activeChannel.currencyCode; + }); + + this.generateVariants(); + } + + addOption() { + this.optionGroups.push({ name: '', values: [] }); + } + + removeOption(name: string) { + this.optionGroups = this.optionGroups.filter(g => g.name !== name); + this.generateVariants(); + } + + generateVariants() { + const totalValuesCount = this.optionGroups.reduce((sum, group) => sum + group.values.length, 0); + const groups = totalValuesCount ? this.optionGroups.map(g => g.values) : [[DEFAULT_VARIANT_CODE]]; + this.variants = generateAllCombinations(groups).map(values => ({ id: values.join('|'), values })); + + this.variants.forEach(variant => { + if (!this.variantFormValues[variant.id]) { + this.variantFormValues[variant.id] = { + optionValues: variant.values, + enabled: true, + price: this.copyFromDefault(variant.id, 'price', 0), + sku: this.copyFromDefault(variant.id, 'sku', ''), + stock: this.copyFromDefault(variant.id, 'stock', 0), + }; + } + }); + this.onFormChange(); + } + + trackByFn(index: number, variant: { name: string; values: string[] }) { + return variant.values.join('|'); + } + + handleEnter(event: KeyboardEvent, optionValueInputComponent: OptionValueInputComponent) { + event.preventDefault(); + event.stopPropagation(); + optionValueInputComponent.focus(); + } + + onFormChange() { + const variantsToCreate = this.variants.map(v => this.variantFormValues[v.id]).filter(v => v.enabled); + this.variantsChange.emit({ + groups: this.optionGroups, + variants: variantsToCreate, }); } - generateProductVariants(defaultPrice?: number, defaultSku?: string) { - this.dataService.product - .generateProductVariants(this.product.id, defaultPrice, defaultSku) - .subscribe(); + private copyFromDefault( + variantId: string, + prop: T, + value: CreateVariantValues[T], + ): CreateVariantValues[T] { + return variantId !== DEFAULT_VARIANT_CODE + ? this.variantFormValues[DEFAULT_VARIANT_CODE][prop] + : value; } } diff --git a/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.html b/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.html new file mode 100644 index 0000000000..521db240dd --- /dev/null +++ b/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.html @@ -0,0 +1,21 @@ + + + + {{ option }} + + + + diff --git a/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.scss b/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.scss new file mode 100644 index 0000000000..ca74ce3d92 --- /dev/null +++ b/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.scss @@ -0,0 +1,44 @@ +@import "variables"; + +.input-wrapper { + background-color: white; + border-radius: 3px !important; + border: 1px solid $color-grey-300 !important; + cursor: text; + + &.focus { + border-color: $color-primary-500 !important; + box-shadow: 0 0 1px 1px $color-primary-100; + } + + .chips { + padding: 5px; + } + + textarea { + border: none; + width: 100%; + height: 24px; + margin-top: 3px; + padding: 0 6px; + &:focus { + outline: none; + } + &:disabled { + background-color: $color-grey-100; + } + } +} + +vdr-chip { + ::ng-deep .wrapper { + margin: 0 3px; + } + &.selected { + ::ng-deep .wrapper { + border-color: $color-warning-500 !important; + box-shadow: 0 0 1px 1px $color-warning-400; + opacity: 0.6; + } + } +} diff --git a/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.ts b/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.ts new file mode 100644 index 0000000000..3f2f6a8358 --- /dev/null +++ b/admin-ui/src/app/catalog/components/option-value-input/option-value-input.component.ts @@ -0,0 +1,98 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + Provider, + ViewChild, +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { unique } from 'shared/unique'; + +export const OPTION_VALUE_INPUT_VALUE_ACCESSOR: Provider = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => OptionValueInputComponent), + multi: true, +}; + +@Component({ + selector: 'vdr-option-value-input', + templateUrl: './option-value-input.component.html', + styleUrls: ['./option-value-input.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [OPTION_VALUE_INPUT_VALUE_ACCESSOR], +}) +export class OptionValueInputComponent implements ControlValueAccessor { + @Input() groupName = ''; + @ViewChild('textArea', { static: true }) textArea: ElementRef; + options: string[]; + disabled = false; + input = ''; + isFocussed = false; + lastSelected = false; + onChangeFn: (value: any) => void; + onTouchFn: (value: any) => void; + + constructor(private changeDetector: ChangeDetectorRef) {} + + registerOnChange(fn: any): void { + this.onChangeFn = fn; + } + + registerOnTouched(fn: any): void { + this.onTouchFn = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.changeDetector.markForCheck(); + } + + writeValue(obj: any): void { + this.options = obj || []; + } + + focus() { + this.textArea.nativeElement.focus(); + } + + removeOption(option: string) { + this.options = this.options.filter(o => o !== option); + this.onChangeFn(this.options); + } + + handleKey(event: KeyboardEvent) { + switch (event.key) { + case ',': + case 'Enter': + this.options = unique([...this.options, ...this.parseInputIntoOptions(this.input)]); + this.input = ''; + this.onChangeFn(this.options); + event.preventDefault(); + break; + case 'Backspace': + if (this.lastSelected) { + this.removeLastOption(); + this.lastSelected = false; + } else if (this.input === '') { + this.lastSelected = true; + } + break; + default: + this.lastSelected = false; + } + } + + private parseInputIntoOptions(input: string): string[] { + return input + .split(/[,\n]/) + .map(s => s.trim()) + .filter(s => s !== ''); + } + + private removeLastOption() { + this.options = this.options.slice(0, this.options.length - 1); + } +} diff --git a/admin-ui/src/app/catalog/components/product-detail/product-detail.component.html b/admin-ui/src/app/catalog/components/product-detail/product-detail.component.html index 58f1bb44bf..7d5400963c 100644 --- a/admin-ui/src/app/catalog/components/product-detail/product-detail.component.html +++ b/admin-ui/src/app/catalog/components/product-detail/product-detail.component.html @@ -20,9 +20,9 @@ {{ 'common.create' | translate }} @@ -40,7 +40,7 @@ - + @@ -83,7 +83,7 @@ @@ -102,14 +102,21 @@ > + + + {{ 'catalog.product-variants' | translate }} + + - + {{ 'catalog.product-variants' | translate }} - + - - - - - + + diff --git a/admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts b/admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts index ab45491087..dd219628a0 100644 --- a/admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts +++ b/admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts @@ -2,7 +2,7 @@ import { Location } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, combineLatest, forkJoin, merge, Observable } from 'rxjs'; +import { BehaviorSubject, combineLatest, forkJoin, merge, Observable, of } from 'rxjs'; import { distinctUntilChanged, map, mergeMap, shareReplay, skip, take, withLatestFrom } from 'rxjs/operators'; import { normalizeString } from 'shared/normalize-string'; import { CustomFieldConfig } from 'shared/shared-types'; @@ -13,6 +13,7 @@ import { IGNORE_CAN_DEACTIVATE_GUARD } from 'src/app/shared/providers/routing/ca import { BaseDetailComponent } from '../../../common/base-detail.component'; import { CreateProductInput, + CreateProductVariantInput, FacetWithValues, LanguageCode, ProductWithVariants, @@ -30,6 +31,7 @@ import { DataService } from '../../../data/providers/data.service'; import { ServerConfigService } from '../../../data/server-config'; import { ModalService } from '../../../shared/providers/modal/modal.service'; import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component'; +import { CreateProductVariantsConfig } from '../generate-product-variants/generate-product-variants.component'; import { VariantAssetChange } from '../product-variants-list/product-variants-list.component'; export type TabName = 'details' | 'variants'; @@ -72,6 +74,7 @@ export class ProductDetailComponent extends BaseDetailComponent([]); selectedVariantIds: string[] = []; variantDisplayMode: 'card' | 'table' = 'card'; + createVariantsConfig: CreateProductVariantsConfig = { groups: [], variants: [] }; constructor( route: ActivatedRoute, @@ -228,6 +231,15 @@ export class ProductDetailComponent extends BaseDetailComponent { + return v.sku !== ''; + }) + ); + } + private displayFacetValueModal(): Observable { let skipValue = 0; if (this.facets$.value.length === 0) { @@ -265,20 +277,101 @@ export class ProductDetailComponent extends BaseDetailComponent { + return this.dataService.product.createProductOptionGroups({ + code: normalizeString(c.name, '-'), + translations: [{ languageCode, name: c.name }], + options: c.values.map(v => ({ + code: normalizeString(v, '-'), + translations: [{ languageCode, name: v }], + })), + }); + }), + ) + : of([]); + + return forkJoin(createProduct$, createOptionGroups$).pipe( + mergeMap(([{ createProduct }, createOptionGroups]) => { + const optionGroups = createOptionGroups.map(g => g.createProductOptionGroup); + const addOptionsToProduct$ = optionGroups.length + ? forkJoin( + optionGroups.map(optionGroup => { + return this.dataService.product.addOptionGroupToProduct({ + productId: createProduct.id, + optionGroupId: optionGroup.id, + }); + }), + ) + : of([]); + return addOptionsToProduct$.pipe( + map(() => { + return { createProduct, optionGroups, languageCode }; + }), + ); + }), + ); + }), + mergeMap(({ createProduct, optionGroups, languageCode }) => { + const variants: CreateProductVariantInput[] = this.createVariantsConfig.variants.map( + v => { + const optionIds = optionGroups.length + ? v.optionValues.map((optionName, index) => { + const option = optionGroups[index].options.find( + o => o.name === optionName, + ); + if (!option) { + throw new Error( + `Could not find a matching ProductOption "${optionName}" when creating variant`, + ); + } + return option.id; + }) + : []; + const name = optionGroups.length + ? `${createProduct.name} ${v.optionValues.join(' ')}` + : createProduct.name; + return { + productId: createProduct.id, + price: v.price, + sku: v.sku, + stockOnHand: v.stock, + translations: [ + { + languageCode, + name, + }, + ], + optionIds, + }; + }, + ); + return this.dataService.product + .createProductVariants(variants) + .pipe( + map(({ createProductVariants }) => ({ + createProductVariants, + productId: createProduct.id, + })), + ); }), ) .subscribe( - data => { + ({ createProductVariants, productId }) => { this.notificationService.success(_('common.notify-create-success'), { entity: 'Product', }); this.assetChanges = {}; this.variantAssetChanges = {}; this.detailForm.markAsPristine(); - this.router.navigate(['../', data.createProduct.id], { relativeTo: this.route }); + this.router.navigate(['../', productId], { relativeTo: this.route }); }, err => { + // tslint:disable-next-line:no-console + console.error(err); this.notificationService.error(_('common.notify-create-error'), { entity: 'Product', }); diff --git a/admin-ui/src/app/catalog/components/product-variants-wizard/product-variants-wizard.component.html b/admin-ui/src/app/catalog/components/product-variants-wizard/product-variants-wizard.component.html deleted file mode 100644 index 4c33ee17df..0000000000 --- a/admin-ui/src/app/catalog/components/product-variants-wizard/product-variants-wizard.component.html +++ /dev/null @@ -1,55 +0,0 @@ - - {{ 'catalog.generate-product-variants' | translate }} - - {{ 'common.cancel' | translate }} - {{ 'common.back' | translate }} - {{ 'common.next' | translate }} - {{ 'common.finish' | translate }} - - - {{ 'catalog.create-new-option-group' | translate }} - - - {{ 'catalog.create-group' | translate }} - - - - - {{ 'catalog.select-option-group' | translate }} - - - - - {{ 'common.confirm' | translate }} - - - - - - - {{ 'catalog.selected-option-groups' | translate }}: - {{ selectedGroup.code }} - {{ 'catalog.confirm-generate-product-variants' | translate: { count: getVariantCount() } }} - - {{ item }} - - - diff --git a/admin-ui/src/app/catalog/components/product-variants-wizard/product-variants-wizard.component.scss b/admin-ui/src/app/catalog/components/product-variants-wizard/product-variants-wizard.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/admin-ui/src/app/catalog/components/product-variants-wizard/product-variants-wizard.component.ts b/admin-ui/src/app/catalog/components/product-variants-wizard/product-variants-wizard.component.ts deleted file mode 100644 index 38a4f8aeca..0000000000 --- a/admin-ui/src/app/catalog/components/product-variants-wizard/product-variants-wizard.component.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Component, Input, OnChanges, ViewChild } from '@angular/core'; -import { ClrWizard } from '@clr/angular'; -import { forkJoin, Observable } from 'rxjs'; -import { map, mergeMap, take, takeUntil } from 'rxjs/operators'; -import { generateAllCombinations } from 'shared/shared-utils'; - -import { ProductOptionGroup, ProductWithVariants } from '../../../common/generated-types'; -import { _ } from '../../../core/providers/i18n/mark-for-extraction'; -import { NotificationService } from '../../../core/providers/notification/notification.service'; -import { DataService } from '../../../data/providers/data.service'; -import { CreateOptionGroupFormComponent } from '../create-option-group-form/create-option-group-form.component'; -import { SelectOptionGroupComponent } from '../select-option-group/select-option-group.component'; - -@Component({ - selector: 'vdr-product-variants-wizard', - templateUrl: './product-variants-wizard.component.html', - styleUrls: ['./product-variants-wizard.component.scss'], -}) -export class ProductVariantsWizardComponent implements OnChanges { - @Input() product: ProductWithVariants.Fragment; - @ViewChild('wizard', { static: true }) wizard: ClrWizard; - @ViewChild('createOptionGroupForm', { static: true }) - createOptionGroupForm: CreateOptionGroupFormComponent; - @ViewChild('selectOptionGroup', { static: true }) selectOptionGroup: SelectOptionGroupComponent; - selectedOptionGroups: Array> = []; - productVariantPreviewList: string[] = []; - defaultPrice = 0; - defaultSku = ''; - - constructor(private notificationService: NotificationService, private dataService: DataService) {} - - ngOnChanges() { - if (this.product) { - this.selectedOptionGroups = this.product.optionGroups; - } - } - - /** - * Opens the wizard and begins the steps. - */ - start(): Observable<{ defaultPrice: number; defaultSku: string }> { - this.wizard.open(); - - return this.wizard.wizardFinished.pipe( - takeUntil(this.wizard.onCancel), - take(1), - mergeMap(() => { - const addOptionsOperations = this.selectedOptionGroups.map(og => { - if (og.id) { - return this.dataService.product.addOptionGroupToProduct({ - productId: this.product.id, - optionGroupId: og.id, - }); - } else { - return []; - } - }); - - return forkJoin(addOptionsOperations); - }), - map(() => ({ - defaultPrice: this.defaultPrice, - defaultSku: this.defaultSku, - })), - ); - } - - createOptionGroup() { - this.createOptionGroupForm.createOptionGroup().subscribe(data => { - this.toggleSelectedGroup(data.createProductOptionGroup); - this.notificationService.success(_('common.notify-create-success'), { entity: 'OptionGroup' }); - this.createOptionGroupForm.resetForm(); - }); - } - - toggleSelectedGroup(optionGroup: ProductOptionGroup.Fragment) { - const selected = !!this.selectedOptionGroups.find(og => og.id === optionGroup.id); - if (selected) { - this.selectedOptionGroups = this.selectedOptionGroups.filter(og => og.id !== optionGroup.id); - } else { - this.selectedOptionGroups = this.selectedOptionGroups.concat(optionGroup); - } - this.generateVariantPreviews(); - } - - /** - * The total number of variants to be generated is the product of all the options in the - * selected option groups. - */ - getVariantCount(): number { - return this.selectedOptionGroups.reduce((total, og) => { - const length = og.options ? og.options.length || 1 : 1; - return total * length; - }, 1); - } - - private generateVariantPreviews() { - const optionsArray = this.selectedOptionGroups.map(og => og.options || []); - this.productVariantPreviewList = generateAllCombinations(optionsArray).map(options => { - return `${this.product.name} ${options.map(o => o.name).join(' ')}`; - }); - } -} diff --git a/admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.html b/admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.html deleted file mode 100644 index 24938eccca..0000000000 --- a/admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.html +++ /dev/null @@ -1,10 +0,0 @@ -{{ 'catalog.select-option-group' | translate }} - - - - - {{ 'common.cancel' | translate }} - diff --git a/admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.scss b/admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.ts b/admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.ts deleted file mode 100644 index c882e70a05..0000000000 --- a/admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; - -import { ProductOptionGroup } from '../../../common/generated-types'; -import { Dialog } from '../../../shared/providers/modal/modal.service'; - -@Component({ - selector: 'vdr-select-option-group-dialog', - templateUrl: './select-option-group-dialog.component.html', - styleUrls: ['./select-option-group-dialog.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SelectOptionGroupDialogComponent implements Dialog { - existingOptionGroups: Array>; - resolveWith: (result?: ProductOptionGroup.Fragment) => void; - - selectGroup(group: ProductOptionGroup.Fragment) { - this.resolveWith(group); - } - - cancel() { - this.resolveWith(); - } -} diff --git a/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.html b/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.html deleted file mode 100644 index 19d3027804..0000000000 --- a/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - {{ group.name }} - {{ group.code }} - - - - {{ option.name }} - - - - - (+ - {{ - 'catalog.truncated-options-count' - | translate: { count: optionsTrucatedCount(group) } - }}) - - - - {{ option.name }} - - - - - - - -{{ 'catalog.selected-option-groups' | translate }}: - - {{ selectedGroup.code }} - diff --git a/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.scss b/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.scss deleted file mode 100644 index 2b8ff401fc..0000000000 --- a/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -@import "variables"; - -.filter-input { - width: 100%; -} - -.group-list { - margin-top: 24px; - height: 400px; - max-height: 60vh; - overflow: auto; -} - -vdr-select-toggle { - margin-right: 12px; -} - -.group { - display: flex; - padding: 6px 12px; - border-bottom: 1px solid $color-grey-200; - - .name-code { - flex: 1; - } - - .code { - color: $color-grey-400; - } - - .options { - text-align: right; - } -} - -.selected-groups { - padding: 12px; -} - -.full-options-list { - text-align: left; -} diff --git a/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.ts b/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.ts deleted file mode 100644 index 15cbc587ae..0000000000 --- a/admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, -} from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { Observable, Subject } from 'rxjs'; -import { debounceTime, map, takeUntil } from 'rxjs/operators'; -import { DeepPartial } from 'shared/shared-types'; - -import { GetProductOptionGroups, ProductOptionGroup } from '../../../common/generated-types'; -import { DataService } from '../../../data/providers/data.service'; -import { QueryResult } from '../../../data/query-result'; - -@Component({ - selector: 'vdr-select-option-group', - templateUrl: './select-option-group.component.html', - styleUrls: ['./select-option-group.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SelectOptionGroupComponent implements OnInit, OnChanges, OnDestroy { - @Input() selectedGroups: ProductOptionGroup[]; - @Output() selectGroup = new EventEmitter(); - optionGroups$: Observable>>; - filterInput = new FormControl(); - optionGroupsQuery: QueryResult; - truncateOptionsTo = 4; - private inputChange$ = new Subject(); - private destroy$ = new Subject(); - - constructor(private dataService: DataService) {} - - ngOnInit() { - this.optionGroupsQuery = this.dataService.product.getProductOptionGroups(); - this.optionGroups$ = this.optionGroupsQuery.stream$.pipe(map(data => data.productOptionGroups)); - - this.filterInput.valueChanges - .pipe( - debounceTime(300), - takeUntil(this.destroy$), - ) - .subscribe(filterTerm => { - this.optionGroupsQuery.ref.refetch({ filterTerm }); - }); - } - - ngOnChanges() { - this.inputChange$.next(this.selectedGroups); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - refresh() { - this.optionGroupsQuery.ref.refetch(); - } - - isSelected(group: ProductOptionGroup): boolean { - return this.selectedGroups && !!this.selectedGroups.find(g => g.id === group.id); - } - - optionsTruncated(group: ProductOptionGroup): boolean { - return 0 < this.optionsTrucatedCount(group); - } - - optionsTrucatedCount(group: ProductOptionGroup): number { - return Math.max(group.options.length - this.truncateOptionsTo, 0); - } -} diff --git a/admin-ui/src/app/common/generated-types.ts b/admin-ui/src/app/common/generated-types.ts index 28cc31a418..ec78ee6e76 100644 --- a/admin-ui/src/app/common/generated-types.ts +++ b/admin-ui/src/app/common/generated-types.ts @@ -481,11 +481,12 @@ export type CreateProductOptionInput = { }; export type CreateProductVariantInput = { + productId: Scalars['ID'], translations: Array, facetValueIds?: Maybe>, sku: Scalars['String'], price?: Maybe, - taxCategoryId: Scalars['ID'], + taxCategoryId?: Maybe, optionIds?: Maybe>, featuredAssetId?: Maybe, assetIds?: Maybe>, @@ -494,6 +495,12 @@ export type CreateProductVariantInput = { customFields?: Maybe, }; +export type CreateProductVariantOptionInput = { + optionGroupId: Scalars['ID'], + code: Scalars['String'], + translations: Array, +}; + export type CreatePromotionInput = { name: Scalars['String'], enabled: Scalars['Boolean'], @@ -1540,12 +1547,12 @@ export type Mutation = { assignRoleToAdministrator: Administrator, /** Create a new Asset */ createAssets: Array, - login: LoginResult, - logout: Scalars['Boolean'], /** Create a new Channel */ createChannel: Channel, /** Update an existing Channel */ updateChannel: Channel, + login: LoginResult, + logout: Scalars['Boolean'], /** Create a new Collection */ createCollection: Collection, /** Update an existing Collection */ @@ -1616,9 +1623,11 @@ export type Mutation = { /** Remove an OptionGroup from a Product */ removeOptionGroupFromProduct: Product, /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */ - generateVariantsForProduct: Product, + createProductVariants: Array>, /** Update existing ProductVariants */ updateProductVariants: Array>, + /** Delete a ProductVariant */ + deleteProductVariant: DeletionResponse, createPromotion: Promotion, updatePromotion: Promotion, deletePromotion: DeletionResponse, @@ -1677,13 +1686,6 @@ export type MutationCreateAssetsArgs = { }; -export type MutationLoginArgs = { - username: Scalars['String'], - password: Scalars['String'], - rememberMe?: Maybe -}; - - export type MutationCreateChannelArgs = { input: CreateChannelInput }; @@ -1694,6 +1696,13 @@ export type MutationUpdateChannelArgs = { }; +export type MutationLoginArgs = { + username: Scalars['String'], + password: Scalars['String'], + rememberMe?: Maybe +}; + + export type MutationCreateCollectionArgs = { input: CreateCollectionInput }; @@ -1892,11 +1901,8 @@ export type MutationRemoveOptionGroupFromProductArgs = { }; -export type MutationGenerateVariantsForProductArgs = { - productId: Scalars['ID'], - defaultTaxCategoryId?: Maybe, - defaultPrice?: Maybe, - defaultSku?: Maybe +export type MutationCreateProductVariantsArgs = { + input: Array }; @@ -1905,6 +1911,11 @@ export type MutationUpdateProductVariantsArgs = { }; +export type MutationDeleteProductVariantArgs = { + id: Scalars['ID'] +}; + + export type MutationCreatePromotionArgs = { input: CreatePromotionInput }; @@ -2291,6 +2302,7 @@ export type ProductOption = Node & { languageCode?: Maybe, code?: Maybe, name?: Maybe, + groupId: Scalars['ID'], translations: Array, customFields?: Maybe, }; @@ -2501,10 +2513,10 @@ export type Query = { administrator?: Maybe, assets: AssetList, asset?: Maybe, - me?: Maybe, channels: Array, channel?: Maybe, activeChannel: Channel, + me?: Maybe, collections: CollectionList, collection?: Maybe, collectionFilters: Array, @@ -3617,7 +3629,7 @@ export type AddNoteToOrderMutationVariables = { }; -export type AddNoteToOrderMutation = ({ __typename?: 'Mutation' } & { addNoteToOrder: ({ __typename?: 'Order' } & Pick & { history: ({ __typename?: 'HistoryEntryList' } & Pick) }) }); +export type AddNoteToOrderMutation = ({ __typename?: 'Mutation' } & { addNoteToOrder: ({ __typename?: 'Order' } & Pick) }); export type AssetFragment = ({ __typename?: 'Asset' } & Pick); @@ -3648,15 +3660,12 @@ export type DeleteProductMutationVariables = { export type DeleteProductMutation = ({ __typename?: 'Mutation' } & { deleteProduct: ({ __typename?: 'DeletionResponse' } & Pick) }); -export type GenerateProductVariantsMutationVariables = { - productId: Scalars['ID'], - defaultTaxCategoryId?: Maybe, - defaultPrice?: Maybe, - defaultSku?: Maybe +export type CreateProductVariantsMutationVariables = { + input: Array }; -export type GenerateProductVariantsMutation = ({ __typename?: 'Mutation' } & { generateVariantsForProduct: ({ __typename?: 'Product' } & ProductWithVariantsFragment) }); +export type CreateProductVariantsMutation = ({ __typename?: 'Mutation' } & { createProductVariants: Array> }); export type UpdateProductVariantsMutationVariables = { input: Array @@ -4441,7 +4450,6 @@ export namespace AddNoteToOrder { export type Variables = AddNoteToOrderMutationVariables; export type Mutation = AddNoteToOrderMutation; export type AddNoteToOrder = AddNoteToOrderMutation['addNoteToOrder']; - export type History = AddNoteToOrderMutation['addNoteToOrder']['history']; } export namespace Asset { @@ -4496,10 +4504,10 @@ export namespace DeleteProduct { export type DeleteProduct = DeleteProductMutation['deleteProduct']; } -export namespace GenerateProductVariants { - export type Variables = GenerateProductVariantsMutationVariables; - export type Mutation = GenerateProductVariantsMutation; - export type GenerateVariantsForProduct = ProductWithVariantsFragment; +export namespace CreateProductVariants { + export type Variables = CreateProductVariantsMutationVariables; + export type Mutation = CreateProductVariantsMutation; + export type CreateProductVariants = ProductVariantFragment; } export namespace UpdateProductVariants { diff --git a/admin-ui/src/app/data/definitions/product-definitions.ts b/admin-ui/src/app/data/definitions/product-definitions.ts index b7c4df44dd..44e2ab9bdf 100644 --- a/admin-ui/src/app/data/definitions/product-definitions.ts +++ b/admin-ui/src/app/data/definitions/product-definitions.ts @@ -156,23 +156,13 @@ export const DELETE_PRODUCT = gql` } `; -export const GENERATE_PRODUCT_VARIANTS = gql` - mutation GenerateProductVariants( - $productId: ID! - $defaultTaxCategoryId: ID - $defaultPrice: Int - $defaultSku: String - ) { - generateVariantsForProduct( - productId: $productId - defaultTaxCategoryId: $defaultTaxCategoryId - defaultPrice: $defaultPrice - defaultSku: $defaultSku - ) { - ...ProductWithVariants +export const CREATE_PRODUCT_VARIANTS = gql` + mutation CreateProductVariants($input: [CreateProductVariantInput!]!) { + createProductVariants(input: $input) { + ...ProductVariant } } - ${PRODUCT_WITH_VARIANTS_FRAGMENT} + ${PRODUCT_VARIANT_FRAGMENT} `; export const UPDATE_PRODUCT_VARIANTS = gql` diff --git a/admin-ui/src/app/data/providers/product-data.service.ts b/admin-ui/src/app/data/providers/product-data.service.ts index df2dd0c538..cf442ef6ea 100644 --- a/admin-ui/src/app/data/providers/product-data.service.ts +++ b/admin-ui/src/app/data/providers/product-data.service.ts @@ -7,8 +7,9 @@ import { CreateProductInput, CreateProductOptionGroup, CreateProductOptionGroupInput, + CreateProductVariantInput, + CreateProductVariants, DeleteProduct, - GenerateProductVariants, GetAssetList, GetProductList, GetProductOptionGroups, @@ -28,8 +29,8 @@ import { CREATE_ASSETS, CREATE_PRODUCT, CREATE_PRODUCT_OPTION_GROUP, + CREATE_PRODUCT_VARIANTS, DELETE_PRODUCT, - GENERATE_PRODUCT_VARIANTS, GET_ASSET_LIST, GET_PRODUCT_LIST, GET_PRODUCT_OPTION_GROUPS, @@ -121,11 +122,13 @@ export class ProductDataService { }); } - generateProductVariants(productId: string, defaultPrice?: number, defaultSku?: string) { - return this.baseDataService.mutate< - GenerateProductVariants.Mutation, - GenerateProductVariants.Variables - >(GENERATE_PRODUCT_VARIANTS, { productId, defaultPrice, defaultSku }); + createProductVariants(input: CreateProductVariantInput[]) { + return this.baseDataService.mutate( + CREATE_PRODUCT_VARIANTS, + { + input, + }, + ); } updateProductVariants(variants: UpdateProductVariantInput[]) { diff --git a/admin-ui/src/i18n-messages/en.json b/admin-ui/src/i18n-messages/en.json index 7875ba8f7e..8eaaa84997 100644 --- a/admin-ui/src/i18n-messages/en.json +++ b/admin-ui/src/i18n-messages/en.json @@ -26,28 +26,22 @@ "add-asset-with-count": "Add {count, plural, 0 {assets} one {1 asset} other {{count} assets}}", "add-facet-value": "Add facet value", "add-facets": "Add facets", + "add-option": "Add option", "assets-selected-count": "{ count } assets selected", "collection-contents": "Collection contents", "confirm-delete-country": "Delete country?", "confirm-delete-facet": "Delete facet?", "confirm-delete-facet-value": "Delete facet value?", "confirm-delete-product": "Delete product?", - "confirm-generate-product-variants": "Click 'Finish' to generate {count} product variants.", - "create-group": "Create option group", "create-new-collection": "Create new collection", "create-new-facet": "Create new facet", - "create-new-option-group": "Create new option group", "create-new-product": "New product", "display-variant-cards": "View details", "display-variant-table": "View as table", "drop-files-to-upload": "Drop files to upload", "facet-values": "Facet values", - "filter-by-group-name": "Filter by group name", "filter-by-name": "Filter by name", "filters": "Filters", - "generate-product-variants": "Generate product variants", - "generate-variants-default-only": "This product does not have options", - "generate-variants-with-options": "This product has options", "group-by-product": "Group by product", "height": "Height", "move-down": "Move down", @@ -57,10 +51,8 @@ "no-selection": "No selection", "notify-create-assets-success": "Created {count, plural, one {new Asset} other {{count} new Assets}}", "open-asset-source": "Open asset source", - "option-group-code": "Code", - "option-group-name": "Option group name", - "option-group-options-label": "Options", - "option-group-options-tooltip": "Enter each option on a new line in the default language ({ defaultLanguage })", + "option": "Option", + "option-values": "Option values", "options": "Options", "original-asset-size": "Source size", "preview": "Preview", @@ -78,12 +70,11 @@ "reindex-successful": "Indexed {count, plural, one {product variant} other {{count} product variants}} in {time}ms", "reindexing": "Rebuilding search index", "remove-asset": "Remove asset", + "remove-option": "Remove option", "search-asset-name": "Search assets by name", "search-for-term": "Search for term", "search-product-name-or-code": "Search by product name or code", "select-assets": "Select assets", - "select-option-group": "Select option group", - "selected-option-groups": "Selected option groups", "set-as-featured-asset": "Set as featured asset", "sku": "SKU", "slug": "Slug", @@ -92,9 +83,9 @@ "tax-category": "Tax category", "taxes": "Taxes", "track-inventory": "Track inventory", - "truncated-options-count": "{count} further {count, plural, one {option} other {options}}", "upload-assets": "Upload assets", "values": "Values", + "variant": "Variant", "view-contents": "View contents", "visibility": "Visibility", "width": "Width" @@ -103,11 +94,9 @@ "ID": "ID", "actions": "Actions", "available-languages": "Available languages", - "back": "Back", "cancel": "Cancel", "cancel-navigation": "Cancel navigation", "code": "Code", - "confirm": "Confirm", "confirm-navigation": "Confirm navigation", "create": "Create", "created-at": "Created at", @@ -120,7 +109,6 @@ "edit": "Edit", "edit-field": "Edit field", "enabled": "Enabled", - "finish": "Finish", "guest": "Guest", "items-per-page-option": "{ count } per page", "jobs-in-progress": "{ count } {count, plural, one {job} other {jobs}} in progress", @@ -129,7 +117,6 @@ "login": "Log in", "more": "More...", "name": "Name", - "next": "Next", "no-results": "No results", "notify-create-error": "An error occurred, could not create { entity }", "notify-create-success": "Created new { entity }", @@ -526,4 +513,4 @@ "update": "Update", "zone": "Zone" } -} +} \ No newline at end of file