Browse Source

refactor(admin-ui): Decompose product option dialogs

Michael Bromley 7 years ago
parent
commit
19d17bbab0
16 changed files with 319 additions and 214 deletions
  1. 6 0
      admin-ui/src/app/catalog/catalog.module.ts
  2. 4 22
      admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.html
  3. 0 14
      admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.scss
  4. 8 72
      admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.ts
  5. 21 0
      admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.html
  6. 14 0
      admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.scss
  7. 24 0
      admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.spec.ts
  8. 79 0
      admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.ts
  9. 14 5
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts
  10. 2 24
      admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.html
  11. 0 42
      admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.scss
  12. 3 35
      admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.ts
  13. 24 0
      admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.html
  14. 42 0
      admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.scss
  15. 24 0
      admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.spec.ts
  16. 54 0
      admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.ts

+ 6 - 0
admin-ui/src/app/catalog/catalog.module.ts

@@ -5,9 +5,12 @@ import { SharedModule } from '../shared/shared.module';
 
 import { catalogRoutes } from './catalog.routes';
 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 { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.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 { ProductUpdaterService } from './providers/product-updater/product-updater.service';
 import { ProductResolver } from './providers/routing/product-resolver';
 
@@ -18,7 +21,10 @@ import { ProductResolver } from './providers/routing/product-resolver';
         ProductListComponent,
         ProductDetailComponent,
         CreateOptionGroupDialogComponent,
+        ProductVariantsWizardComponent,
         SelectOptionGroupDialogComponent,
+        CreateOptionGroupFormComponent,
+        SelectOptionGroupComponent,
     ],
     entryComponents: [CreateOptionGroupDialogComponent, SelectOptionGroupDialogComponent],
     providers: [ProductResolver, ProductUpdaterService],

+ 4 - 22
admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.html

@@ -1,26 +1,8 @@
 <ng-template vdrDialogTitle>{{ 'catalog.create-new-option-group' | translate }}</ng-template>
 
-<form class="form" [formGroup]="optionGroupForm">
-    <section class="form-block">
-        <vdr-form-field [label]="'catalog.option-group-name' | translate" for="name">
-            <input id="name" type="text" formControlName="name" (input)="updateCode($event.target.value)">
-        </vdr-form-field>
-        <vdr-form-field [label]="'catalog.option-group-code' | translate" for="code">
-            <div class="code-input">
-                <input id="code" type="text" formControlName="code" [readonly]="!editCode">
-                <button type="button" class="btn btn-icon btn-sm"
-                        (click)="editCode = true">
-                    <clr-icon shape="edit"></clr-icon>
-                </button>
-            </div>
-        </vdr-form-field>
-        <vdr-form-field [label]="'catalog.option-group-options-label' | translate"
-                        [tooltip]="'catalog.option-group-options-tooltip' | translate: { defaultLanguage: defaultLanguage }"
-                        for="options">
-            <textarea id="options" type="text" formControlName="options" rows="10"></textarea>
-        </vdr-form-field>
-    </section>
-</form>
+<vdr-create-option-group-form #createOptionGroupForm
+                              [productId]="productId"
+                              [productName]="productName"></vdr-create-option-group-form>
 
 <ng-template vdrDialogButtons>
     <button type="button"
@@ -29,7 +11,7 @@
     <button type="submit"
             (click)="createOptionGroup()"
             class="btn btn-primary"
-            [disabled]="optionGroupForm.invalid || optionGroupForm.pristine">
+            [disabled]="createOptionGroupForm.optionGroupForm.invalid || createOptionGroupForm.optionGroupForm.pristine">
         {{ 'catalog.create-group' | translate }}
     </button>
 </ng-template>

+ 0 - 14
admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.scss

@@ -1,14 +0,0 @@
-@import "variables";
-
-.code-input {
-    display: flex;
-    input {
-        flex: 1;
-        &[readonly] {
-            background-color: $color-grey-1;
-        }
-    }
-    button {
-        margin: 0;
-    }
-}

+ 8 - 72
admin-ui/src/app/catalog/components/create-option-group-dialog/create-option-group-dialog.component.ts

@@ -1,18 +1,8 @@
-import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { FormBuilder, FormGroup } from '@angular/forms';
-import { mergeMap } from 'rxjs/operators';
+import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core';
 
-import { ID } from '../../../../../../shared/shared-types';
-import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
-import { normalizeString } from '../../../common/utilities/normalize-string';
-import { DataService } from '../../../data/providers/data.service';
-import {
-    CreateProductOptionGroup,
-    CreateProductOptionGroupInput,
-    CreateProductOptionInput,
-    LanguageCode,
-} from '../../../data/types/gql-generated-types';
+import { CreateProductOptionGroup } from '../../../data/types/gql-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',
@@ -20,71 +10,17 @@ import { Dialog } from '../../../shared/providers/modal/modal.service';
     styleUrls: ['./create-option-group-dialog.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class CreateOptionGroupDialogComponent implements Dialog<CreateProductOptionGroup>, OnInit {
-    resolveWith: (result?: CreateProductOptionGroup) => void;
-    optionGroupForm: FormGroup;
-    productName = '';
+export class CreateOptionGroupDialogComponent implements Dialog<CreateProductOptionGroup> {
     productId: string;
-    editCode = false;
-    readonly defaultLanguage = getDefaultLanguage();
-
-    constructor(private formBuilder: FormBuilder, private dataService: DataService) {}
-
-    ngOnInit() {
-        this.optionGroupForm = this.formBuilder.group({
-            name: '',
-            code: '',
-            options: '',
-        });
-    }
-
-    updateCode(nameValue: string) {
-        const codeControl = this.optionGroupForm.get('code');
-        if (codeControl && codeControl.pristine) {
-            codeControl.setValue(normalizeString(`${this.productName} ${nameValue}`, '-'));
-        }
-    }
+    productName: string;
+    @ViewChild('createOptionGroupForm') createOptionGroupForm: CreateOptionGroupFormComponent;
+    resolveWith: (result?: CreateProductOptionGroup) => void;
 
     createOptionGroup() {
-        this.dataService.product
-            .createProductOptionGroups(this.createGroupFromForm())
-            .subscribe(data => this.resolveWith(data));
+        this.createOptionGroupForm.createOptionGroup().subscribe(data => this.resolveWith(data));
     }
 
     cancel() {
         this.resolveWith();
     }
-
-    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,
-                        },
-                    ],
-                };
-            });
-    }
 }

+ 21 - 0
admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.html

@@ -0,0 +1,21 @@
+<form class="form" [formGroup]="optionGroupForm">
+    <section class="form-block">
+        <vdr-form-field [label]="'catalog.option-group-name' | translate" for="name">
+            <input id="name" type="text" formControlName="name" (input)="updateCode($event.target.value)">
+        </vdr-form-field>
+        <vdr-form-field [label]="'catalog.option-group-code' | translate" for="code">
+            <div class="code-input">
+                <input id="code" type="text" formControlName="code" [readonly]="!editCode">
+                <button type="button" class="btn btn-icon btn-sm"
+                        (click)="editCode = true">
+                    <clr-icon shape="edit"></clr-icon>
+                </button>
+            </div>
+        </vdr-form-field>
+        <vdr-form-field [label]="'catalog.option-group-options-label' | translate"
+                        [tooltip]="'catalog.option-group-options-tooltip' | translate: { defaultLanguage: defaultLanguage }"
+                        for="options">
+            <textarea id="options" type="text" formControlName="options" rows="10"></textarea>
+        </vdr-form-field>
+    </section>
+</form>

+ 14 - 0
admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.scss

@@ -0,0 +1,14 @@
+@import "variables";
+
+.code-input {
+    display: flex;
+    input {
+        flex: 1;
+        &[readonly] {
+            background-color: $color-grey-1;
+        }
+    }
+    button {
+        margin: 0;
+    }
+}

+ 24 - 0
admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.spec.ts

@@ -0,0 +1,24 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CreateOptionGroupFormComponent } from './create-option-group-form.component';
+
+describe('CreateOptionGroupFormComponent', () => {
+    let component: CreateOptionGroupFormComponent;
+    let fixture: ComponentFixture<CreateOptionGroupFormComponent>;
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            declarations: [CreateOptionGroupFormComponent],
+        }).compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(CreateOptionGroupFormComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});

+ 79 - 0
admin-ui/src/app/catalog/components/create-option-group-form/create-option-group-form.component.ts

@@ -0,0 +1,79 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { Observable } from 'rxjs';
+
+import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
+import { normalizeString } from '../../../common/utilities/normalize-string';
+import { DataService } from '../../../data/providers/data.service';
+import {
+    CreateProductOptionGroup,
+    CreateProductOptionGroupInput,
+    CreateProductOptionInput,
+} from '../../../data/types/gql-generated-types';
+
+@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;
+    editCode = false;
+    readonly defaultLanguage = getDefaultLanguage();
+
+    constructor(private formBuilder: FormBuilder, private dataService: DataService) {}
+
+    ngOnInit() {
+        this.optionGroupForm = this.formBuilder.group({
+            name: '',
+            code: '',
+            options: '',
+        });
+    }
+
+    updateCode(nameValue: string) {
+        const codeControl = this.optionGroupForm.get('code');
+        if (codeControl && codeControl.pristine) {
+            codeControl.setValue(normalizeString(`${this.productName} ${nameValue}`, '-'));
+        }
+    }
+
+    createOptionGroup(): Observable<CreateProductOptionGroup> {
+        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,
+                        },
+                    ],
+                };
+            });
+    }
+}

+ 14 - 5
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -1,4 +1,4 @@
-import { Component, OnDestroy } from '@angular/core';
+import { Component, OnDestroy, ViewChild } from '@angular/core';
 import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { combineLatest, forkJoin, Observable, Subject } from 'rxjs';
@@ -10,13 +10,14 @@ import { _ } from '../../../core/providers/i18n/mark-for-extraction';
 import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { DataService } from '../../../data/providers/data.service';
 import {
-    GetProductWithVariants_product,
     GetProductWithVariants_product_variants,
     LanguageCode,
+    ProductWithVariants,
 } from '../../../data/types/gql-generated-types';
 import { ModalService } from '../../../shared/providers/modal/modal.service';
 import { ProductUpdaterService } from '../../providers/product-updater/product-updater.service';
 import { CreateOptionGroupDialogComponent } from '../create-option-group-dialog/create-option-group-dialog.component';
+import { ProductVariantsWizardComponent } from '../product-variants-wizard/product-variants-wizard.component';
 import { SelectOptionGroupDialogComponent } from '../select-option-group-dialog/select-option-group-dialog.component';
 
 @Component({
@@ -25,12 +26,13 @@ import { SelectOptionGroupDialogComponent } from '../select-option-group-dialog/
     styleUrls: ['./product-detail.component.scss'],
 })
 export class ProductDetailComponent implements OnDestroy {
-    product$: Observable<GetProductWithVariants_product>;
+    product$: Observable<ProductWithVariants>;
     variants$: Observable<GetProductWithVariants_product_variants[]>;
     availableLanguages$: Observable<LanguageCode[]>;
     languageCode$: Observable<LanguageCode>;
     isNew$: Observable<boolean>;
     productForm: FormGroup;
+    @ViewChild('productVariantsWizard') productVariantsWizard: ProductVariantsWizardComponent;
     private destroy$ = new Subject<void>();
 
     constructor(
@@ -110,8 +112,9 @@ export class ProductDetailComponent implements OnDestroy {
             .pipe(
                 take(1),
                 mergeMap(product => {
-                    const nameControl = this.productForm.get('name');
-                    const productName = nameControl ? nameControl.value : '';
+                    const productFormGroup = this.productForm.get('product');
+                    const productNameControl = productFormGroup && productFormGroup.get('name');
+                    const productName = productNameControl ? productNameControl.value : '';
                     return this.modalService
                         .fromComponent(CreateOptionGroupDialogComponent, {
                             closable: true,
@@ -247,6 +250,12 @@ export class ProductDetailComponent implements OnDestroy {
             );
     }
 
+    startProductVariantsWizard() {
+        this.product$.pipe(mergeMap(product => this.productVariantsWizard.start(product))).subscribe(val => {
+            // console.log('finished', val);
+        });
+    }
+
     generateProductVariants() {
         this.product$
             .pipe(

+ 2 - 24
admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.html

@@ -1,29 +1,7 @@
 <ng-template vdrDialogTitle>{{ 'catalog.select-option-group' | translate }}</ng-template>
-<input type="text"
-       class="filter-input"
-       [placeholder]="'catalog.filter-by-group-name' | translate"
-       [formControl]="filterInput">
-<div class="group-list">
-    <div class="group" *ngFor="let group of optionGroups$ | async">
-        <div class="select-button">
-            <button class="btn btn-icon btn-primary"
-                    [disabled]="!isAvailable(group)"
-                    (click)="selectGroup(group)">
-                <clr-icon shape="check"></clr-icon>
-            </button>
-        </div>
-        <div class="name-code">
-            <div class="name">{{ group.name }}</div>
-            <div class="code">{{ group.code }}</div>
-        </div>
-        <div class="options">
-            <div class="option" *ngFor="let option of group.options">
-                {{ option.name }}
-            </div>
-        </div>
-    </div>
-</div>
 
+<vdr-select-option-group [existingOptionGroupIds]="existingOptionGroupIds"
+                         (selectGroup)="selectGroup($event)"></vdr-select-option-group>
 
 <ng-template vdrDialogButtons>
     <button type="button"

+ 0 - 42
admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.scss

@@ -1,42 +0,0 @@
-@import "variables";
-
-.filter-input {
-    width: 100%;
-}
-
-.group-list {
-    margin-top: 24px;
-    height: 400px;
-    max-height: 60vh;
-    overflow: auto;
-}
-
-.group {
-    display: flex;
-    padding: 6px 12px;
-    border-bottom: 1px solid $color-grey-2;
-
-    .name-code {
-        flex: 1;
-    }
-
-    .code {
-        color: $color-grey-4;
-    }
-
-    .options {
-        text-align: right;
-    }
-
-    .option {
-        display: inline-block;
-        padding: 0 6px;
-        border: 1px solid $color-grey-3;
-        color: $color-grey-5;
-        border-radius: 3px;
-        margin-right: 3px;
-        &:last-of-type {
-            margin-right: 0;
-        }
-    }
-}

+ 3 - 35
admin-ui/src/app/catalog/components/select-option-group-dialog/select-option-group-dialog.component.ts

@@ -1,9 +1,5 @@
-import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
-import { FormControl } from '@angular/forms';
-import { Observable, Subject } from 'rxjs';
-import { debounceTime, map, takeUntil } from 'rxjs/operators';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
 
-import { DataService } from '../../../data/providers/data.service';
 import { GetProductOptionGroups_productOptionGroups } from '../../../data/types/gql-generated-types';
 import { Dialog } from '../../../shared/providers/modal/modal.service';
 
@@ -15,37 +11,9 @@ export type ProductOptionGroup = GetProductOptionGroups_productOptionGroups;
     styleUrls: ['./select-option-group-dialog.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class SelectOptionGroupDialogComponent implements Dialog<ProductOptionGroup>, OnInit, OnDestroy {
-    resolveWith: (result?: ProductOptionGroup) => void;
+export class SelectOptionGroupDialogComponent implements Dialog<ProductOptionGroup> {
     existingOptionGroupIds: string[];
-    optionGroups$: Observable<ProductOptionGroup[]>;
-    filterInput = new FormControl();
-    private destroy$ = new Subject<void>();
-
-    constructor(private dataService: DataService) {}
-
-    ngOnInit() {
-        const optionGroupsQuery = this.dataService.product.getProductOptionGroups();
-        this.optionGroups$ = optionGroupsQuery.stream$.pipe(map(data => data.productOptionGroups));
-
-        this.filterInput.valueChanges
-            .pipe(
-                debounceTime(300),
-                takeUntil(this.destroy$),
-            )
-            .subscribe(filterTerm => {
-                optionGroupsQuery.ref.refetch({ filterTerm });
-            });
-    }
-
-    ngOnDestroy() {
-        this.destroy$.next();
-        this.destroy$.complete();
-    }
-
-    isAvailable(group: ProductOptionGroup): boolean {
-        return !this.existingOptionGroupIds.includes(group.id);
-    }
+    resolveWith: (result?: ProductOptionGroup) => void;
 
     selectGroup(group: ProductOptionGroup) {
         this.resolveWith(group);

+ 24 - 0
admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.html

@@ -0,0 +1,24 @@
+<input type="text"
+       class="filter-input"
+       [placeholder]="'catalog.filter-by-group-name' | translate"
+       [formControl]="filterInput">
+<div class="group-list">
+    <div class="group" *ngFor="let group of optionGroups$ | async">
+        <div class="select-button">
+            <button class="btn btn-icon btn-primary"
+                    [disabled]="!isAvailable(group)"
+                    (click)="selectGroup.emit(group)">
+                <clr-icon shape="check"></clr-icon>
+            </button>
+        </div>
+        <div class="name-code">
+            <div class="name">{{ group.name }}</div>
+            <div class="code">{{ group.code }}</div>
+        </div>
+        <div class="options">
+            <div class="option" *ngFor="let option of group.options">
+                {{ option.name }}
+            </div>
+        </div>
+    </div>
+</div>

+ 42 - 0
admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.scss

@@ -0,0 +1,42 @@
+@import "variables";
+
+.filter-input {
+    width: 100%;
+}
+
+.group-list {
+    margin-top: 24px;
+    height: 400px;
+    max-height: 60vh;
+    overflow: auto;
+}
+
+.group {
+    display: flex;
+    padding: 6px 12px;
+    border-bottom: 1px solid $color-grey-2;
+
+    .name-code {
+        flex: 1;
+    }
+
+    .code {
+        color: $color-grey-4;
+    }
+
+    .options {
+        text-align: right;
+    }
+
+    .option {
+        display: inline-block;
+        padding: 0 6px;
+        border: 1px solid $color-grey-3;
+        color: $color-grey-5;
+        border-radius: 3px;
+        margin-right: 3px;
+        &:last-of-type {
+            margin-right: 0;
+        }
+    }
+}

+ 24 - 0
admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.spec.ts

@@ -0,0 +1,24 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SelectOptionGroupComponent } from './select-option-group.component';
+
+describe('SelectOptionGroupComponent', () => {
+    let component: SelectOptionGroupComponent;
+    let fixture: ComponentFixture<SelectOptionGroupComponent>;
+
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            declarations: [SelectOptionGroupComponent],
+        }).compileComponents();
+    }));
+
+    beforeEach(() => {
+        fixture = TestBed.createComponent(SelectOptionGroupComponent);
+        component = fixture.componentInstance;
+        fixture.detectChanges();
+    });
+
+    it('should create', () => {
+        expect(component).toBeTruthy();
+    });
+});

+ 54 - 0
admin-ui/src/app/catalog/components/select-option-group/select-option-group.component.ts

@@ -0,0 +1,54 @@
+import {
+    ChangeDetectionStrategy,
+    Component,
+    EventEmitter,
+    Input,
+    OnDestroy,
+    OnInit,
+    Output,
+} from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { Observable, Subject } from 'rxjs';
+import { debounceTime, map, takeUntil } from 'rxjs/operators';
+
+import { DataService } from '../../../data/providers/data.service';
+import { ProductOptionGroup } from '../select-option-group-dialog/select-option-group-dialog.component';
+
+@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, OnDestroy {
+    @Input() existingOptionGroupIds: string[];
+    @Output() selectGroup = new EventEmitter<ProductOptionGroup>();
+    optionGroups$: Observable<ProductOptionGroup[]>;
+    filterInput = new FormControl();
+    private destroy$ = new Subject<void>();
+
+    constructor(private dataService: DataService) {}
+
+    ngOnInit() {
+        const optionGroupsQuery = this.dataService.product.getProductOptionGroups();
+        this.optionGroups$ = optionGroupsQuery.stream$.pipe(map(data => data.productOptionGroups));
+
+        this.filterInput.valueChanges
+            .pipe(
+                debounceTime(300),
+                takeUntil(this.destroy$),
+            )
+            .subscribe(filterTerm => {
+                optionGroupsQuery.ref.refetch({ filterTerm });
+            });
+    }
+
+    ngOnDestroy() {
+        this.destroy$.next();
+        this.destroy$.complete();
+    }
+
+    isAvailable(group: ProductOptionGroup): boolean {
+        return !this.existingOptionGroupIds.includes(group.id);
+    }
+}