Prechádzať zdrojové kódy

feat(admin-ui): Create list/detail components for ProductCategory

Relates to #43
Michael Bromley 7 rokov pred
rodič
commit
ba88611017

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

@@ -16,6 +16,8 @@ import { FacetListComponent } from './components/facet-list/facet-list.component
 import { FacetValueSelectorComponent } from './components/facet-value-selector/facet-value-selector.component';
 import { GenerateProductVariantsComponent } from './components/generate-product-variants/generate-product-variants.component';
 import { ProductAssetsComponent } from './components/product-assets/product-assets.component';
+import { ProductCategoryDetailComponent } from './components/product-category-detail/product-category-detail.component';
+import { ProductCategoryListComponent } from './components/product-category-list/product-category-list.component';
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { ProductVariantsListComponent } from './components/product-variants-list/product-variants-list.component';
@@ -24,6 +26,7 @@ import { SelectOptionGroupDialogComponent } from './components/select-option-gro
 import { SelectOptionGroupComponent } from './components/select-option-group/select-option-group.component';
 import { VariantPriceDetailComponent } from './components/variant-price-detail/variant-price-detail.component';
 import { FacetResolver } from './providers/routing/facet-resolver';
+import { ProductCategoryResolver } from './providers/routing/product-category-resolver';
 import { ProductResolver } from './providers/routing/product-resolver';
 
 @NgModule({
@@ -49,6 +52,8 @@ import { ProductResolver } from './providers/routing/product-resolver';
         AssetPickerDialogComponent,
         AssetFileInputComponent,
         VariantPriceDetailComponent,
+        ProductCategoryListComponent,
+        ProductCategoryDetailComponent,
     ],
     entryComponents: [
         AssetPickerDialogComponent,
@@ -56,6 +61,6 @@ import { ProductResolver } from './providers/routing/product-resolver';
         SelectOptionGroupDialogComponent,
         ApplyFacetDialogComponent,
     ],
-    providers: [ProductResolver, FacetResolver],
+    providers: [ProductResolver, FacetResolver, ProductCategoryResolver],
 })
 export class CatalogModule {}

+ 28 - 0
admin-ui/src/app/catalog/catalog.routes.ts

@@ -8,9 +8,12 @@ import { _ } from '../core/providers/i18n/mark-for-extraction';
 import { AssetListComponent } from './components/asset-list/asset-list.component';
 import { FacetDetailComponent } from './components/facet-detail/facet-detail.component';
 import { FacetListComponent } from './components/facet-list/facet-list.component';
+import { ProductCategoryDetailComponent } from './components/product-category-detail/product-category-detail.component';
+import { ProductCategoryListComponent } from './components/product-category-list/product-category-list.component';
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { FacetResolver } from './providers/routing/facet-resolver';
+import { ProductCategoryResolver } from './providers/routing/product-category-resolver';
 import { ProductResolver } from './providers/routing/product-resolver';
 
 export const catalogRoutes: Route[] = [
@@ -44,6 +47,21 @@ export const catalogRoutes: Route[] = [
             breadcrumb: facetBreadcrumb,
         },
     },
+    {
+        path: 'categories',
+        component: ProductCategoryListComponent,
+        data: {
+            breadcrumb: _('breadcrumb.categories'),
+        },
+    },
+    {
+        path: 'categories/:id',
+        component: ProductCategoryDetailComponent,
+        resolve: createResolveData(ProductCategoryResolver),
+        data: {
+            breadcrumb: productCategoryBreadcrumb,
+        },
+    },
     {
         path: 'assets',
         component: AssetListComponent,
@@ -72,3 +90,13 @@ export function facetBreadcrumb(data: any, params: any) {
         route: 'facets',
     });
 }
+
+export function productCategoryBreadcrumb(data: any, params: any) {
+    return detailBreadcrumb<FacetWithValues.Fragment>({
+        entity: data.entity,
+        id: params.id,
+        breadcrumbKey: 'breadcrumb.categories',
+        getName: facet => facet.name,
+        route: 'categories',
+    });
+}

+ 63 - 0
admin-ui/src/app/catalog/components/product-category-detail/product-category-detail.component.html

@@ -0,0 +1,63 @@
+<vdr-action-bar>
+    <vdr-ab-left>
+        <vdr-language-selector
+            [availableLanguageCodes]="availableLanguages$ | async"
+            [currentLanguageCode]="languageCode$ | async"
+            (languageCodeChange)="setLanguage($event)"
+        ></vdr-language-selector>
+    </vdr-ab-left>
+
+    <vdr-ab-right>
+        <button
+            class="btn btn-primary"
+            *ngIf="(isNew$ | async); else: updateButton"
+            (click)="create()"
+            [disabled]="categoryForm.invalid || categoryForm.pristine"
+        >
+            {{ 'common.create' | translate }}
+        </button>
+        <ng-template #updateButton>
+            <button
+                class="btn btn-primary"
+                (click)="save()"
+                [disabled]="(categoryForm.invalid || categoryForm.pristine) && !assetsChanged()"
+            >
+                {{ 'common.update' | translate }}
+            </button>
+        </ng-template>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<form class="form" [formGroup]="categoryForm" *ngIf="(entity$ | async) as category">
+    <div class="clr-row">
+        <div class="clr-col">
+            <section class="form-block">
+                <vdr-form-field [label]="'common.name' | translate" for="name">
+                    <input id="name" type="text" formControlName="name" />
+                </vdr-form-field>
+                <vdr-rich-text-editor
+                    formControlName="description"
+                    [label]="'common.description' | translate"
+                ></vdr-rich-text-editor>
+
+                <section formGroupName="customFields" *ngIf="customFields.length">
+                    <label>{{ 'catalog.custom-fields' }}</label>
+                    <ng-container *ngFor="let customField of customFields">
+                        <vdr-custom-field-control
+                            *ngIf="customFieldIsSet(customField.name)"
+                            [customFieldsFormGroup]="categoryForm.get(['customFields'])"
+                            [customField]="customField"
+                        ></vdr-custom-field-control>
+                    </ng-container>
+                </section>
+            </section>
+        </div>
+        <div class="clr-col-md-auto">
+            <vdr-product-assets
+                [assets]="category.assets"
+                [featuredAsset]="category.featuredAsset"
+                (change)="assetChanges = $event"
+            ></vdr-product-assets>
+        </div>
+    </div>
+</form>

+ 0 - 0
admin-ui/src/app/catalog/components/product-category-detail/product-category-detail.component.scss


+ 187 - 0
admin-ui/src/app/catalog/components/product-category-detail/product-category-detail.component.ts

@@ -0,0 +1,187 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { combineLatest, forkJoin, Observable } from 'rxjs';
+import { mergeMap, take } from 'rxjs/operators';
+import {
+    CreateProductCategoryInput,
+    CreateProductInput,
+    LanguageCode,
+    ProductCategory,
+    UpdateProductCategoryInput,
+    UpdateProductInput,
+} from 'shared/generated-types';
+import { CustomFieldConfig } from 'shared/shared-types';
+
+import { BaseDetailComponent } from '../../../common/base-detail.component';
+import { createUpdatedTranslatable } from '../../../common/utilities/create-updated-translatable';
+import { _ } from '../../../core/providers/i18n/mark-for-extraction';
+import { NotificationService } from '../../../core/providers/notification/notification.service';
+import { DataService } from '../../../data/providers/data.service';
+import { ServerConfigService } from '../../../data/server-config';
+
+@Component({
+    selector: 'vdr-product-category-detail',
+    templateUrl: './product-category-detail.component.html',
+    styleUrls: ['./product-category-detail.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ProductCategoryDetailComponent extends BaseDetailComponent<ProductCategory.Fragment>
+    implements OnInit, OnDestroy {
+    customFields: CustomFieldConfig[];
+    categoryForm: FormGroup;
+    assetChanges: { assetIds?: string[]; featuredAssetId?: string } = {};
+
+    constructor(
+        router: Router,
+        route: ActivatedRoute,
+        serverConfigService: ServerConfigService,
+        private changeDetector: ChangeDetectorRef,
+        private dataService: DataService,
+        private formBuilder: FormBuilder,
+        private notificationService: NotificationService,
+    ) {
+        super(route, router, serverConfigService);
+        this.customFields = this.getCustomFieldConfig('ProductCategory');
+        this.categoryForm = this.formBuilder.group({
+            name: ['', Validators.required],
+            description: '',
+            customFields: this.formBuilder.group(
+                this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
+            ),
+        });
+    }
+
+    ngOnInit() {
+        this.init();
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+    }
+
+    customFieldIsSet(name: string): boolean {
+        return !!this.categoryForm.get(['customFields', name]);
+    }
+
+    assetsChanged(): boolean {
+        return !!Object.values(this.assetChanges).length;
+    }
+
+    create() {
+        if (!this.categoryForm.dirty) {
+            return;
+        }
+        combineLatest(this.entity$, this.languageCode$)
+            .pipe(
+                take(1),
+                mergeMap(([category, languageCode]) => {
+                    const input = this.getUpdatedCategory(category, this.categoryForm, languageCode);
+                    return this.dataService.product.createProductCategory(input);
+                }),
+            )
+            .subscribe(
+                data => {
+                    this.notificationService.success(_('common.notify-create-success'), {
+                        entity: 'ProductCategory',
+                    });
+                    this.categoryForm.markAsPristine();
+                    this.changeDetector.markForCheck();
+                    this.router.navigate(['../', data.createProductCategory.id], { relativeTo: this.route });
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-create-error'), {
+                        entity: 'ProductCategory',
+                    });
+                },
+            );
+    }
+
+    save() {
+        combineLatest(this.entity$, this.languageCode$)
+            .pipe(
+                take(1),
+                mergeMap(([category, languageCode]) => {
+                    const updateOperations: Array<Observable<any>> = [];
+
+                    if (this.categoryForm.dirty || this.assetsChanged()) {
+                        const input = this.getUpdatedCategory(
+                            category,
+                            this.categoryForm,
+                            languageCode,
+                        ) as UpdateProductCategoryInput;
+                        if (input) {
+                            updateOperations.push(this.dataService.product.updateProductCategory(input));
+                        }
+                    }
+                    return forkJoin(updateOperations);
+                }),
+            )
+            .subscribe(
+                () => {
+                    this.categoryForm.markAsPristine();
+                    this.changeDetector.markForCheck();
+                    this.notificationService.success(_('common.notify-update-success'), {
+                        entity: 'ProductCategory',
+                    });
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-update-error'), {
+                        entity: 'ProductCategory',
+                    });
+                },
+            );
+    }
+
+    /**
+     * Sets the values of the form on changes to the category or current language.
+     */
+    protected setFormValues(category: ProductCategory.Fragment, languageCode: LanguageCode) {
+        const currentTranslation = category.translations.find(t => t.languageCode === languageCode);
+        if (currentTranslation) {
+            this.categoryForm.patchValue({
+                name: currentTranslation.name,
+                description: currentTranslation.description,
+            });
+
+            if (this.customFields.length) {
+                const customFieldsGroup = this.categoryForm.get(['customFields']) as FormGroup;
+
+                for (const fieldDef of this.customFields) {
+                    const key = fieldDef.name;
+                    const value =
+                        fieldDef.type === 'localeString'
+                            ? (currentTranslation as any).customFields[key]
+                            : (category as any).customFields[key];
+                    const control = customFieldsGroup.get(key);
+                    if (control) {
+                        control.patchValue(value);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Given a category and the value of the form, this method creates an updated copy of the category which
+     * can then be persisted to the API.
+     */
+    private getUpdatedCategory(
+        category: ProductCategory.Fragment,
+        form: FormGroup,
+        languageCode: LanguageCode,
+    ): CreateProductCategoryInput | UpdateProductCategoryInput {
+        const updatedCategory = createUpdatedTranslatable({
+            translatable: category,
+            updatedFields: form.value,
+            customFieldConfig: this.customFields,
+            languageCode,
+            defaultTranslation: {
+                languageCode,
+                name: category.name || '',
+                description: category.description || '',
+            },
+        });
+        return { ...updatedCategory, ...this.assetChanges };
+    }
+}

+ 34 - 0
admin-ui/src/app/catalog/components/product-category-list/product-category-list.component.html

@@ -0,0 +1,34 @@
+<vdr-action-bar>
+    <vdr-ab-right>
+        <a class="btn btn-primary" [routerLink]="['./create']">
+            <clr-icon shape="plus"></clr-icon>
+            {{ 'catalog.create-new-product-category' | translate }}
+        </a>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<vdr-data-table
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-dt-column>{{ 'common.ID' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'catalog.facet-values' | translate }}</vdr-dt-column>
+    <vdr-dt-column></vdr-dt-column>
+    <ng-template let-category="item">
+        <td class="left">{{ category.id }}</td>
+        <td class="left">{{ category.name }}</td>
+        <td class="left">{{ category.facetValues }}</td>
+        <td class="right">
+            <vdr-table-row-action
+                iconShape="edit"
+                [label]="'common.edit' | translate"
+                [linkTo]="['./', category.id]"
+            ></vdr-table-row-action>
+        </td>
+    </ng-template>
+</vdr-data-table>

+ 0 - 0
admin-ui/src/app/catalog/components/product-category-list/product-category-list.component.scss


+ 25 - 0
admin-ui/src/app/catalog/components/product-category-list/product-category-list.component.ts

@@ -0,0 +1,25 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { GetProductCategoryList } from 'shared/generated-types';
+
+import { BaseListComponent } from '../../../common/base-list.component';
+import { DataService } from '../../../data/providers/data.service';
+
+@Component({
+    selector: 'vdr-product-category-list',
+    templateUrl: './product-category-list.component.html',
+    styleUrls: ['./product-category-list.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ProductCategoryListComponent extends BaseListComponent<
+    GetProductCategoryList.Query,
+    GetProductCategoryList.Items
+> {
+    constructor(private dataService: DataService, router: Router, route: ActivatedRoute) {
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) => this.dataService.product.getProductCategories(...args),
+            data => data.productCategories,
+        );
+    }
+}

+ 0 - 25
admin-ui/src/app/catalog/components/product-detail/product-detail.component.spec.ts

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

+ 0 - 3
admin-ui/src/app/catalog/providers/routing/facet-resolver.ts

@@ -5,9 +5,6 @@ import { BaseEntityResolver } from '../../../common/base-entity-resolver';
 import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
 import { DataService } from '../../../data/providers/data.service';
 
-/**
- * Resolves the id from the path into a Customer entity.
- */
 @Injectable()
 export class FacetResolver extends BaseEntityResolver<FacetWithValues.Fragment> {
     constructor(private dataService: DataService) {

+ 26 - 0
admin-ui/src/app/catalog/providers/routing/product-category-resolver.ts

@@ -0,0 +1,26 @@
+import { Injectable } from '@angular/core';
+import { ProductCategory, ProductWithVariants } from 'shared/generated-types';
+
+import { BaseEntityResolver } from '../../../common/base-entity-resolver';
+import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
+import { DataService } from '../../../data/providers/data.service';
+
+@Injectable()
+export class ProductCategoryResolver extends BaseEntityResolver<ProductCategory.Fragment> {
+    constructor(private dataService: DataService) {
+        super(
+            {
+                __typename: 'ProductCategory' as 'ProductCategory',
+                id: '',
+                languageCode: getDefaultLanguage(),
+                name: '',
+                description: '',
+                featuredAsset: null,
+                assets: [],
+                translations: [],
+                facetValues: [],
+            },
+            id => this.dataService.product.getProductCategory(id).mapStream(data => data.productCategory),
+        );
+    }
+}

+ 0 - 4
admin-ui/src/app/catalog/providers/routing/product-resolver.ts

@@ -5,10 +5,6 @@ import { BaseEntityResolver } from '../../../common/base-entity-resolver';
 import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
 import { DataService } from '../../../data/providers/data.service';
 
-/**
- * Resolves the id from the path into a Customer entity.
- */
-
 @Injectable()
 export class ProductResolver extends BaseEntityResolver<ProductWithVariants.Fragment> {
     constructor(private dataService: DataService) {

+ 6 - 6
admin-ui/src/app/core/components/main-nav/main-nav.component.html

@@ -16,6 +16,12 @@
                         {{ 'nav.facets' | translate }}
                     </a>
                 </li>
+                <li>
+                    <a class="nav-link" [routerLink]="['/catalog', 'categories']" routerLinkActive="active">
+                        <clr-icon shape="folder-open" size="20"></clr-icon>
+                        {{ 'nav.categories' | translate }}
+                    </a>
+                </li>
                 <li>
                     <a
                         class="nav-link"
@@ -27,12 +33,6 @@
                         {{ 'nav.assets' | translate }}
                     </a>
                 </li>
-                <li>
-                    <a class="nav-link">
-                        <clr-icon shape="folder-open" size="20"></clr-icon>
-                        {{ 'nav.categories' | translate }}
-                    </a>
-                </li>
             </ul>
         </section>
         <section class="nav-group">

+ 84 - 0
admin-ui/src/app/data/definitions/product-definitions.ts

@@ -263,3 +263,87 @@ export const CREATE_ASSETS = gql`
     }
     ${ASSET_FRAGMENT}
 `;
+
+export const PRODUCT_CATEGORY_FRAGMENT = gql`
+    fragment ProductCategory on ProductCategory {
+        id
+        name
+        description
+        languageCode
+        featuredAsset {
+            ...Asset
+        }
+        assets {
+            ...Asset
+        }
+        facetValues {
+            id
+            name
+            code
+        }
+        translations {
+            id
+            languageCode
+            name
+            description
+        }
+        parent {
+            id
+            name
+        }
+        children {
+            id
+            name
+        }
+    }
+    ${ASSET_FRAGMENT}
+`;
+
+export const GET_PRODUCT_CATEGORY_LIST = gql`
+    query GetProductCategoryList($options: ProductCategoryListOptions, $languageCode: LanguageCode) {
+        productCategories(languageCode: $languageCode, options: $options) {
+            items {
+                id
+                name
+                description
+                featuredAsset {
+                    ...Asset
+                }
+                facetValues {
+                    id
+                    code
+                    name
+                }
+            }
+            totalItems
+        }
+    }
+    ${ASSET_FRAGMENT}
+`;
+
+export const GET_PRODUCT_CATEGORY = gql`
+    query GetProductCategory($id: ID!, $languageCode: LanguageCode) {
+        productCategory(id: $id, languageCode: $languageCode) {
+            ...ProductCategory
+        }
+    }
+    ${PRODUCT_CATEGORY_FRAGMENT}
+`;
+
+export const CREATE_PRODUCT_CATEGORY = gql`
+    mutation CreateProductCategory($input: CreateProductCategoryInput!) {
+        createProductCategory(input: $input) {
+            ...ProductCategory
+        }
+    }
+    ${PRODUCT_CATEGORY_FRAGMENT}
+`;
+
+export const UPDATE_PRODUCT_CATEGORY = gql`
+    mutation UpdateProductCategory($input: UpdateProductCategoryInput!) {
+        updateProductCategory(input: $input) {
+            ...ProductCategory
+        }
+    }
+    ${PRODUCT_CATEGORY_FRAGMENT}
+`;

+ 51 - 0
admin-ui/src/app/data/providers/product-data.service.ts

@@ -3,16 +3,22 @@ import {
     ApplyFacetValuesToProductVariants,
     CreateAssets,
     CreateProduct,
+    CreateProductCategory,
+    CreateProductCategoryInput,
     CreateProductInput,
     CreateProductOptionGroup,
     CreateProductOptionGroupInput,
     GenerateProductVariants,
     GetAssetList,
+    GetProductCategory,
+    GetProductCategoryList,
     GetProductList,
     GetProductOptionGroups,
     GetProductWithVariants,
     RemoveOptionGroupFromProduct,
     UpdateProduct,
+    UpdateProductCategory,
+    UpdateProductCategoryInput,
     UpdateProductInput,
     UpdateProductVariantInput,
     UpdateProductVariants,
@@ -25,14 +31,18 @@ import {
     APPLY_FACET_VALUE_TO_PRODUCT_VARIANTS,
     CREATE_ASSETS,
     CREATE_PRODUCT,
+    CREATE_PRODUCT_CATEGORY,
     CREATE_PRODUCT_OPTION_GROUP,
     GENERATE_PRODUCT_VARIANTS,
     GET_ASSET_LIST,
+    GET_PRODUCT_CATEGORY,
+    GET_PRODUCT_CATEGORY_LIST,
     GET_PRODUCT_LIST,
     GET_PRODUCT_OPTION_GROUPS,
     GET_PRODUCT_WITH_VARIANTS,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
     UPDATE_PRODUCT,
+    UPDATE_PRODUCT_CATEGORY,
     UPDATE_PRODUCT_VARIANTS,
 } from '../definitions/product-definitions';
 
@@ -156,4 +166,45 @@ export class ProductDataService {
             input: files.map(file => ({ file })),
         });
     }
+
+    getProductCategories(take: number = 10, skip: number = 0) {
+        return this.baseDataService.query<GetProductCategoryList.Query, GetProductCategoryList.Variables>(
+            GET_PRODUCT_CATEGORY_LIST,
+            {
+                options: {
+                    take,
+                    skip,
+                },
+                languageCode: getDefaultLanguage(),
+            },
+        );
+    }
+
+    getProductCategory(id: string) {
+        return this.baseDataService.query<GetProductCategory.Query, GetProductCategory.Variables>(
+            GET_PRODUCT_CATEGORY,
+            {
+                id,
+                languageCode: getDefaultLanguage(),
+            },
+        );
+    }
+
+    createProductCategory(input: CreateProductCategoryInput) {
+        return this.baseDataService.mutate<CreateProductCategory.Mutation, CreateProductCategory.Variables>(
+            CREATE_PRODUCT_CATEGORY,
+            {
+                input: pick(input, ['translations', 'assetIds', 'featuredAssetId', 'customFields']),
+            },
+        );
+    }
+
+    updateProductCategory(input: UpdateProductCategoryInput) {
+        return this.baseDataService.mutate<UpdateProductCategory.Mutation, UpdateProductCategory.Variables>(
+            UPDATE_PRODUCT_CATEGORY,
+            {
+                input: pick(input, ['id', 'translations', 'assetIds', 'featuredAssetId', 'customFields']),
+            },
+        );
+    }
 }

+ 3 - 1
admin-ui/src/i18n-messages/en.json

@@ -5,6 +5,7 @@
   "breadcrumb": {
     "administrators": "Administrators",
     "assets": "Assets",
+    "categories": "Categories",
     "channels": "Channels",
     "countries": "Countries",
     "customers": "Customers",
@@ -30,6 +31,7 @@
     "create-new-facet": "Create new facet",
     "create-new-option-group": "Create new option group",
     "create-new-product": "Create new product",
+    "create-new-product-category": "Create new product category",
     "drop-files-to-upload": "Drop files to upload",
     "facet": "Facet",
     "facet-values": "Facet values",
@@ -211,4 +213,4 @@
     "update": "Update",
     "zone": "Zone"
   }
-}
+}

+ 123 - 0
shared/generated-types.ts

@@ -5291,6 +5291,82 @@ export namespace CreateAssets {
     export type CreateAssets = Asset.Fragment;
 }
 
+export namespace GetProductCategoryList {
+    export type Variables = {
+        options?: ProductCategoryListOptions | null;
+        languageCode?: LanguageCode | null;
+    };
+
+    export type Query = {
+        __typename?: 'Query';
+        productCategories: ProductCategories;
+    };
+
+    export type ProductCategories = {
+        __typename?: 'ProductCategoryList';
+        items: Items[];
+        totalItems: number;
+    };
+
+    export type Items = {
+        __typename?: 'ProductCategory';
+        id: string;
+        name: string;
+        description: string;
+        featuredAsset?: FeaturedAsset | null;
+        facetValues: FacetValues[];
+    };
+
+    export type FeaturedAsset = Asset.Fragment;
+
+    export type FacetValues = {
+        __typename?: 'FacetValue';
+        id: string;
+        code: string;
+        name: string;
+    };
+}
+
+export namespace GetProductCategory {
+    export type Variables = {
+        id: string;
+        languageCode?: LanguageCode | null;
+    };
+
+    export type Query = {
+        __typename?: 'Query';
+        productCategory?: ProductCategory | null;
+    };
+
+    export type ProductCategory = ProductCategory.Fragment;
+}
+
+export namespace CreateProductCategory {
+    export type Variables = {
+        input: CreateProductCategoryInput;
+    };
+
+    export type Mutation = {
+        __typename?: 'Mutation';
+        createProductCategory: CreateProductCategory;
+    };
+
+    export type CreateProductCategory = ProductCategory.Fragment;
+}
+
+export namespace UpdateProductCategory {
+    export type Variables = {
+        input: UpdateProductCategoryInput;
+    };
+
+    export type Mutation = {
+        __typename?: 'Mutation';
+        updateProductCategory: UpdateProductCategory;
+    };
+
+    export type UpdateProductCategory = ProductCategory.Fragment;
+}
+
 export namespace GetPromotionList {
     export type Variables = {
         options?: PromotionListOptions | null;
@@ -6205,6 +6281,53 @@ export namespace ProductOptionGroup {
     };
 }
 
+export namespace ProductCategory {
+    export type Fragment = {
+        __typename?: 'ProductCategory';
+        id: string;
+        name: string;
+        description: string;
+        languageCode?: LanguageCode | null;
+        featuredAsset?: FeaturedAsset | null;
+        assets: Assets[];
+        facetValues: FacetValues[];
+        translations: Translations[];
+        parent?: Parent | null;
+        children?: Children[] | null;
+    };
+
+    export type FeaturedAsset = Asset.Fragment;
+
+    export type Assets = Asset.Fragment;
+
+    export type FacetValues = {
+        __typename?: 'FacetValue';
+        id: string;
+        name: string;
+        code: string;
+    };
+
+    export type Translations = {
+        __typename?: 'ProductCategoryTranslation';
+        id: string;
+        languageCode: LanguageCode;
+        name: string;
+        description: string;
+    };
+
+    export type Parent = {
+        __typename?: 'ProductCategory';
+        id: string;
+        name: string;
+    };
+
+    export type Children = {
+        __typename?: 'ProductCategory';
+        id: string;
+        name: string;
+    };
+}
+
 export namespace AdjustmentOperation {
     export type Fragment = {
         __typename?: 'AdjustmentOperation';