Skip to content

Commit

Permalink
feat(admin-ui): Implement adding new variants by extending options
Browse files Browse the repository at this point in the history
Relates to #162
  • Loading branch information
michaelbromley committed Sep 18, 2019
1 parent ddb18e4 commit fefe0ea
Show file tree
Hide file tree
Showing 21 changed files with 798 additions and 84 deletions.
11 changes: 10 additions & 1 deletion packages/admin-ui/src/app/catalog/catalog.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ProductAssetsComponent } from './components/product-assets/product-asse
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 { ProductVariantsEditorComponent } from './components/product-variants-editor/product-variants-editor.component';
import { ProductVariantsListComponent } from './components/product-variants-list/product-variants-list.component';
import { ProductVariantsTableComponent } from './components/product-variants-table/product-variants-table.component';
import { UpdateProductOptionDialogComponent } from './components/update-product-option-dialog/update-product-option-dialog.component';
Expand All @@ -32,6 +33,7 @@ import { ProductDetailService } from './providers/product-detail.service';
import { CollectionResolver } from './providers/routing/collection-resolver';
import { FacetResolver } from './providers/routing/facet-resolver';
import { ProductResolver } from './providers/routing/product-resolver';
import { ProductVariantsResolver } from './providers/routing/product-variants-resolver';

@NgModule({
imports: [SharedModule, RouterModule.forChild(catalogRoutes), DragDropModule],
Expand Down Expand Up @@ -60,13 +62,20 @@ import { ProductResolver } from './providers/routing/product-resolver';
ProductSearchInputComponent,
OptionValueInputComponent,
UpdateProductOptionDialogComponent,
ProductVariantsEditorComponent,
],
entryComponents: [
AssetPickerDialogComponent,
ApplyFacetDialogComponent,
AssetPreviewComponent,
UpdateProductOptionDialogComponent,
],
providers: [ProductResolver, FacetResolver, CollectionResolver, ProductDetailService],
providers: [
ProductResolver,
FacetResolver,
CollectionResolver,
ProductDetailService,
ProductVariantsResolver,
],
})
export class CatalogModule {}
32 changes: 32 additions & 0 deletions packages/admin-ui/src/app/catalog/catalog.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import { FacetDetailComponent } from './components/facet-detail/facet-detail.com
import { FacetListComponent } from './components/facet-list/facet-list.component';
import { ProductDetailComponent } from './components/product-detail/product-detail.component';
import { ProductListComponent } from './components/product-list/product-list.component';
import { ProductVariantsEditorComponent } from './components/product-variants-editor/product-variants-editor.component';
import { CollectionResolver } from './providers/routing/collection-resolver';
import { FacetResolver } from './providers/routing/facet-resolver';
import { ProductResolver } from './providers/routing/product-resolver';
import { ProductVariantsResolver } from './providers/routing/product-variants-resolver';

export const catalogRoutes: Route[] = [
{
Expand All @@ -37,6 +39,15 @@ export const catalogRoutes: Route[] = [
breadcrumb: productBreadcrumb,
},
},
{
path: 'products/:id/manage-variants',
component: ProductVariantsEditorComponent,
resolve: createResolveData(ProductVariantsResolver),
canDeactivate: [CanDeactivateDetailGuard],
data: {
breadcrumb: productVariantEditorBreadcrumb,
},
},
{
path: 'facets',
component: FacetListComponent,
Expand Down Expand Up @@ -88,6 +99,27 @@ export function productBreadcrumb(data: any, params: any) {
});
}

export function productVariantEditorBreadcrumb(data: any, params: any) {
return data.entity.pipe(
map((entity: any) => {
return [
{
label: _('breadcrumb.products'),
link: ['../', 'products'],
},
{
label: `#${params.id} (${entity.name})`,
link: ['../', 'products', params.id, { tab: 'variants' }],
},
{
label: _('breadcrumb.manage-variants'),
link: ['manage-variants'],
},
];
}),
);
}

export function facetBreadcrumb(data: any, params: any) {
return detailBreadcrumb<FacetWithValues.Fragment>({
entity: data.entity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type CreateProductVariantsConfig = {
})
export class GenerateProductVariantsComponent implements OnInit {
@Output() variantsChange = new EventEmitter<CreateProductVariantsConfig>();
optionGroups: Array<{ name: string; values: string[] }> = [];
optionGroups: Array<{ name: string; values: Array<{ name: string; locked: boolean }> }> = [];
currencyCode: CurrencyCode;
variants: Array<{ id: string; values: string[] }>;
variantFormValues: { [id: string]: CreateVariantValues } = {};
Expand All @@ -50,7 +50,9 @@ export class GenerateProductVariantsComponent implements OnInit {

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]];
const groups = totalValuesCount
? this.optionGroups.map(g => g.values.map(v => v.name))
: [[DEFAULT_VARIANT_CODE]];
this.variants = generateAllCombinations(groups).map(values => ({ id: values.join('|'), values }));

this.variants.forEach(variant => {
Expand Down Expand Up @@ -80,7 +82,7 @@ export class GenerateProductVariantsComponent implements OnInit {
onFormChange() {
const variantsToCreate = this.variants.map(v => this.variantFormValues[v.id]).filter(v => v.enabled);
this.variantsChange.emit({
groups: this.optionGroups,
groups: this.optionGroups.map(og => ({ name: og.name, values: og.values.map(v => v.name) })),
variants: variantsToCreate,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
<div class="chips" *ngIf="0 < options.length">
<vdr-chip
*ngFor="let option of options; last as isLast"
icon="times"
[icon]="option.locked ? 'lock' : 'times'"
[class.selected]="isLast && lastSelected"
[class.locked]="option.locked"
[colorFrom]="groupName"
(iconClick)="removeOption(option)"
>
{{ option }}
{{ option.name }}
</vdr-chip>
</div>
<textarea
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ vdr-chip {
::ng-deep .wrapper {
margin: 0 3px;
}
&.locked {
opacity: 0.8;
}
&.selected {
::ng-deep .wrapper {
border-color: $color-warning-500 !important;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ export const OPTION_VALUE_INPUT_VALUE_ACCESSOR: Provider = {
selector: 'vdr-option-value-input',
templateUrl: './option-value-input.component.html',
styleUrls: ['./option-value-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.Default,
providers: [OPTION_VALUE_INPUT_VALUE_ACCESSOR],
})
export class OptionValueInputComponent implements ControlValueAccessor {
@Input() groupName = '';
@ViewChild('textArea', { static: true }) textArea: ElementRef<HTMLTextAreaElement>;
options: string[];
options: Array<{ name: string; locked: boolean }>;
disabled = false;
input = '';
isFocussed = false;
Expand Down Expand Up @@ -58,9 +58,11 @@ export class OptionValueInputComponent implements ControlValueAccessor {
this.textArea.nativeElement.focus();
}

removeOption(option: string) {
this.options = this.options.filter(o => o !== option);
this.onChangeFn(this.options);
removeOption(option: { name: string; locked: boolean }) {
if (!option.locked) {
this.options = this.options.filter(o => o.name !== option.name);
this.onChangeFn(this.options);
}
}

handleKey(event: KeyboardEvent) {
Expand Down Expand Up @@ -94,14 +96,17 @@ export class OptionValueInputComponent implements ControlValueAccessor {
this.onChangeFn(this.options);
}

private parseInputIntoOptions(input: string): string[] {
private parseInputIntoOptions(input: string): Array<{ name: string; locked: boolean }> {
return input
.split(/[,\n]/)
.map(s => s.trim())
.filter(s => s !== '');
.filter(s => s !== '')
.map(s => ({ name: s, locked: false }));
}

private removeLastOption() {
this.options = this.options.slice(0, this.options.length - 1);
if (!this.options[this.options.length - 1].locked) {
this.options = this.options.slice(0, this.options.length - 1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ <h4>{{ 'catalog.product-variants' | translate }}</h4>
{{ 'catalog.display-variant-table' | translate }}
</button>
</div>
<div class="flex-spacer"></div>
<a [routerLink]="['./', 'manage-variants']"
class="btn btn-secondary btn-sm edit-variants-btn">
<clr-icon shape="add-text"></clr-icon>
{{ 'catalog.manage-variants' | translate }}
</a>
</div>

<vdr-product-variants-table
Expand All @@ -156,7 +162,6 @@ <h4>{{ 'catalog.product-variants' | translate }}</h4>
(updateProductOption)="updateProductOption($event)"
(selectionChange)="selectedVariantIds = $event"
(selectFacetValueClick)="selectVariantFacetValue($event)"
(deleteVariant)="deleteVariant($event)"
></vdr-product-variants-list>
</section>
</clr-tab-content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ vdr-action-bar clr-toggle-wrapper {
display: flex;
justify-content: flex-end;
}

.edit-variants-btn {
margin-top: 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,34 +254,6 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
);
}

deleteVariant(id: string) {
this.modalService
.dialog({
title: _('catalog.confirm-delete-product-variant'),
buttons: [
{ type: 'seconday', label: _('common.cancel') },
{ type: 'danger', label: _('common.delete'), returnValue: true },
],
})
.pipe(
switchMap(response =>
response ? this.productDetailService.deleteProductVariant(id, this.id) : EMPTY,
),
)
.subscribe(
() => {
this.notificationService.success(_('common.notify-delete-success'), {
entity: 'ProductVariant',
});
},
err => {
this.notificationService.error(_('common.notify-delete-error'), {
entity: 'ProductVariant',
});
},
);
}

private displayFacetValueModal(): Observable<string[] | undefined> {
return this.productDetailService.getFacets().pipe(
mergeMap(facets =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<vdr-action-bar>
<vdr-ab-right>
<button
class="btn btn-primary"
(click)="save()"
[disabled]="!formValueChanged || getVariantsToAdd().length === 0"
>
{{ 'common.add-new-variants' | translate: { count: getVariantsToAdd().length } }}
</button>
</vdr-ab-right>
</vdr-action-bar>

<div *ngFor="let group of optionGroups" class="option-groups">
<div class="name">
<label>{{ 'catalog.option' | translate }}</label>
<input
clrInput
[(ngModel)]="group.name"
name="name"
readonly
/>
</div>
<div class="values">
<label>{{ 'catalog.option-values' | translate }}</label>
<vdr-option-value-input
#optionValueInputComponent
[(ngModel)]="group.values"
(ngModelChange)="generateVariants()"
[groupName]="group.name"
[disabled]="group.name === ''"
></vdr-option-value-input>
</div>
</div>
<button class="btn btn-primary-outline btn-sm" (click)="addOption()">
<clr-icon shape="plus"></clr-icon>
{{ 'catalog.add-option' | translate }}
</button>

<div class="variants-preview">
<table class="table">
<thead>
<tr>
<th>{{ 'common.create' | translate }}</th>
<th>{{ 'catalog.variant' | translate }}</th>
<th>{{ 'catalog.sku' | translate }}</th>
<th>{{ 'catalog.price' | translate }}</th>
<th>{{ 'catalog.stock-on-hand' | translate }}</th>
<th></th>
</tr>
</thead>
<tr
*ngFor="let variant of variants; trackBy: trackByFn"
[class.disabled]="!variantFormValues[variant.id].enabled || variantFormValues[variant.id].existing"
>
<td>
<input
type="checkbox"
*ngIf="!variantFormValues[variant.id].existing"
[(ngModel)]="variantFormValues[variant.id].enabled"
name="enabled"
clrCheckbox
(ngModelChange)="onFormChanged(variantFormValues[variant.id])"
/>
</td>
<td>
{{ getVariantName(variant) }}
</td>
<td>
<clr-input-container *ngIf="!variantFormValues[variant.id].existing">
<input
clrInput
type="text"
[(ngModel)]="variantFormValues[variant.id].sku"
[placeholder]="'catalog.sku' | translate"
name="sku"
required
(ngModelChange)="onFormChanged(variantFormValues[variant.id])"
/>
</clr-input-container>
<span *ngIf="variantFormValues[variant.id].existing">{{ variantFormValues[variant.id].sku }}</span>
</td>
<td>
<clr-input-container *ngIf="!variantFormValues[variant.id].existing">
<vdr-currency-input
clrInput
[(ngModel)]="variantFormValues[variant.id].price"
name="price"
[currencyCode]="currencyCode"
(ngModelChange)="onFormChanged(variantFormValues[variant.id])"
></vdr-currency-input>
</clr-input-container>
<span *ngIf="variantFormValues[variant.id].existing">{{ variantFormValues[variant.id].price / 100 | currency: currencyCode }}</span>
</td>
<td>
<clr-input-container *ngIf="!variantFormValues[variant.id].existing">
<input
clrInput
type="number"
[(ngModel)]="variantFormValues[variant.id].stock"
name="stock"
min="0"
step="1"
(ngModelChange)="onFormChanged(variantFormValues[variant.id])"
/>
</clr-input-container>
<span *ngIf="variantFormValues[variant.id].existing">{{ variantFormValues[variant.id].stock }}</span>
</td>
<td>
<vdr-dropdown *ngIf="variantFormValues[variant.id].productVariantId as productVariantId">
<button class="icon-button" vdrDropdownTrigger>
<clr-icon shape="ellipsis-vertical"></clr-icon>
</button>
<vdr-dropdown-menu vdrPosition="bottom-right">
<button
type="button"
class="delete-button"
(click)="deleteVariant(productVariantId)"
vdrDropdownItem
>
<clr-icon shape="trash" class="is-danger"></clr-icon>
{{ 'common.delete' | translate }}
</button>
</vdr-dropdown-menu>

</vdr-dropdown>
</td>
</tr>
</table>
</div>
Loading

0 comments on commit fefe0ea

Please sign in to comment.