Browse Source

feat(server): Implement ProductCategory moving & ordering, e2e tests

Michael Bromley 7 years ago
parent
commit
3d4a8b9fe9

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

@@ -350,3 +350,12 @@ export const UPDATE_PRODUCT_CATEGORY = gql`
     }
     ${PRODUCT_CATEGORY_FRAGMENT}
 `;
+
+export const MOVE_PRODUCT_CATEGORY = gql`
+    mutation MoveProductCategory($input: MoveProductCategoryInput!) {
+        moveProductCategory(input: $input) {
+            ...ProductCategory
+        }
+    }
+    ${PRODUCT_CATEGORY_FRAGMENT}
+`;

File diff suppressed because it is too large
+ 0 - 0
schema.json


+ 96 - 0
server/e2e/__snapshots__/product-category.e2e-spec.ts.snap

@@ -0,0 +1,96 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProductCategory resolver createProductCategory creates a root category 1`] = `
+Object {
+  "assets": Array [
+    Object {
+      "fileSize": 4,
+      "id": "T_1",
+      "mimeType": "image/jpeg",
+      "name": "charles-deluvio-695736-unsplash.jpg",
+      "preview": "test-url/test-assets/charles-deluvio-695736-unsplash__preview.jpg",
+      "source": "test-url/test-assets/charles-deluvio-695736-unsplash.jpg",
+      "type": "IMAGE",
+    },
+    Object {
+      "fileSize": 4,
+      "id": "T_2",
+      "mimeType": "image/jpeg",
+      "name": "chuttersnap-584518-unsplash.jpg",
+      "preview": "test-url/test-assets/chuttersnap-584518-unsplash__preview.jpg",
+      "source": "test-url/test-assets/chuttersnap-584518-unsplash.jpg",
+      "type": "IMAGE",
+    },
+  ],
+  "children": null,
+  "description": "",
+  "facetValues": Array [],
+  "featuredAsset": Object {
+    "fileSize": 4,
+    "id": "T_2",
+    "mimeType": "image/jpeg",
+    "name": "chuttersnap-584518-unsplash.jpg",
+    "preview": "test-url/test-assets/chuttersnap-584518-unsplash__preview.jpg",
+    "source": "test-url/test-assets/chuttersnap-584518-unsplash.jpg",
+    "type": "IMAGE",
+  },
+  "id": "T_2",
+  "languageCode": "en",
+  "name": "Electronics",
+  "parent": Object {
+    "id": "T_1",
+    "name": "__root_category__",
+  },
+  "translations": Array [
+    Object {
+      "description": "",
+      "id": "T_1",
+      "languageCode": "en",
+      "name": "Electronics",
+    },
+  ],
+}
+`;
+
+exports[`ProductCategory resolver updateProductCategory updates the details 1`] = `
+Object {
+  "assets": Array [
+    Object {
+      "fileSize": 4,
+      "id": "T_2",
+      "mimeType": "image/jpeg",
+      "name": "chuttersnap-584518-unsplash.jpg",
+      "preview": "test-url/test-assets/chuttersnap-584518-unsplash__preview.jpg",
+      "source": "test-url/test-assets/chuttersnap-584518-unsplash.jpg",
+      "type": "IMAGE",
+    },
+  ],
+  "children": null,
+  "description": "Apple stuff ",
+  "facetValues": Array [],
+  "featuredAsset": Object {
+    "fileSize": 4,
+    "id": "T_2",
+    "mimeType": "image/jpeg",
+    "name": "chuttersnap-584518-unsplash.jpg",
+    "preview": "test-url/test-assets/chuttersnap-584518-unsplash__preview.jpg",
+    "source": "test-url/test-assets/chuttersnap-584518-unsplash.jpg",
+    "type": "IMAGE",
+  },
+  "id": "T_4",
+  "languageCode": "en",
+  "name": "Apple",
+  "parent": Object {
+    "id": "T_3",
+    "name": "Laptops",
+  },
+  "translations": Array [
+    Object {
+      "description": "Apple stuff ",
+      "id": "T_4",
+      "languageCode": "en",
+      "name": "Apple",
+    },
+  ],
+}
+`;

+ 238 - 0
server/e2e/product-category.e2e-spec.ts

@@ -0,0 +1,238 @@
+import gql from 'graphql-tag';
+import {
+    CreateProductCategory,
+    GetAssetList,
+    LanguageCode,
+    MoveProductCategory,
+    ProductCategory,
+    UpdateProductCategory,
+} from 'shared/generated-types';
+import { ROOT_CATEGORY_NAME } from 'shared/shared-constants';
+
+import {
+    CREATE_PRODUCT_CATEGORY,
+    GET_ASSET_LIST,
+    MOVE_PRODUCT_CATEGORY,
+    UPDATE_PRODUCT_CATEGORY,
+} from '../../admin-ui/src/app/data/definitions/product-definitions';
+
+import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { TestClient } from './test-client';
+import { TestServer } from './test-server';
+
+describe('ProductCategory resolver', () => {
+    const client = new TestClient();
+    const server = new TestServer();
+    let assets: GetAssetList.Items[];
+    let electronicsCategory: ProductCategory.Fragment;
+    let laptopsCategory: ProductCategory.Fragment;
+    let appleCategory: ProductCategory.Fragment;
+
+    beforeAll(async () => {
+        const token = await server.init({
+            productCount: 5,
+            customerCount: 1,
+        });
+        await client.init();
+        const assetsResult = await client.query<GetAssetList.Query, GetAssetList.Variables>(GET_ASSET_LIST);
+        assets = assetsResult.assets.items;
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    describe('createProductCategory', () => {
+        it('creates a root category', async () => {
+            const result = await client.query<
+                CreateProductCategory.Mutation,
+                CreateProductCategory.Variables
+            >(CREATE_PRODUCT_CATEGORY, {
+                input: {
+                    assetIds: [assets[0].id, assets[1].id],
+                    featuredAssetId: assets[1].id,
+                    translations: [{ languageCode: LanguageCode.en, name: 'Electronics', description: '' }],
+                },
+            });
+
+            electronicsCategory = result.createProductCategory;
+            expect(electronicsCategory).toMatchSnapshot();
+            expect(electronicsCategory.parent.name).toBe(ROOT_CATEGORY_NAME);
+        });
+
+        it('creates a nested category', async () => {
+            const result = await client.query<
+                CreateProductCategory.Mutation,
+                CreateProductCategory.Variables
+            >(CREATE_PRODUCT_CATEGORY, {
+                input: {
+                    parentId: electronicsCategory.id,
+                    translations: [{ languageCode: LanguageCode.en, name: 'Laptops', description: '' }],
+                },
+            });
+            laptopsCategory = result.createProductCategory;
+            expect(laptopsCategory.parent.name).toBe(electronicsCategory.name);
+        });
+
+        it('creates a 2nd level nested category', async () => {
+            const result = await client.query<
+                CreateProductCategory.Mutation,
+                CreateProductCategory.Variables
+            >(CREATE_PRODUCT_CATEGORY, {
+                input: {
+                    parentId: laptopsCategory.id,
+                    translations: [{ languageCode: LanguageCode.en, name: 'Apple', description: '' }],
+                },
+            });
+            appleCategory = result.createProductCategory;
+            expect(appleCategory.parent.name).toBe(laptopsCategory.name);
+        });
+    });
+
+    describe('updateProductCategory', () => {
+        it('updates the details', async () => {
+            const result = await client.query<
+                UpdateProductCategory.Mutation,
+                UpdateProductCategory.Variables
+            >(UPDATE_PRODUCT_CATEGORY, {
+                input: {
+                    id: appleCategory.id,
+                    assetIds: [assets[1].id],
+                    featuredAssetId: assets[1].id,
+                    translations: [{ languageCode: LanguageCode.en, description: 'Apple stuff ' }],
+                },
+            });
+
+            expect(result.updateProductCategory).toMatchSnapshot();
+        });
+    });
+
+    describe('moveProductCategory', () => {
+        it('moves a category to a new parent', async () => {
+            const result = await client.query<MoveProductCategory.Mutation, MoveProductCategory.Variables>(
+                MOVE_PRODUCT_CATEGORY,
+                {
+                    input: {
+                        categoryId: appleCategory.id,
+                        parentId: electronicsCategory.id,
+                        index: 0,
+                    },
+                },
+            );
+
+            expect(result.moveProductCategory.parent.id).toBe(electronicsCategory.id);
+
+            const positions = await getChildrenOf(electronicsCategory.id);
+            expect(positions.map(i => i.id)).toEqual([appleCategory.id, laptopsCategory.id]);
+        });
+
+        it('alters the position in the current parent', async () => {
+            await client.query<MoveProductCategory.Mutation, MoveProductCategory.Variables>(
+                MOVE_PRODUCT_CATEGORY,
+                {
+                    input: {
+                        categoryId: appleCategory.id,
+                        parentId: electronicsCategory.id,
+                        index: 1,
+                    },
+                },
+            );
+
+            const afterResult = await getChildrenOf(electronicsCategory.id);
+            expect(afterResult.map(i => i.id)).toEqual([laptopsCategory.id, appleCategory.id]);
+        });
+
+        it('corrects an out-of-bounds negative index value', async () => {
+            await client.query<MoveProductCategory.Mutation, MoveProductCategory.Variables>(
+                MOVE_PRODUCT_CATEGORY,
+                {
+                    input: {
+                        categoryId: appleCategory.id,
+                        parentId: electronicsCategory.id,
+                        index: -3,
+                    },
+                },
+            );
+
+            const afterResult = await getChildrenOf(electronicsCategory.id);
+            expect(afterResult.map(i => i.id)).toEqual([appleCategory.id, laptopsCategory.id]);
+        });
+
+        it('corrects an out-of-bounds positive index value', async () => {
+            await client.query<MoveProductCategory.Mutation, MoveProductCategory.Variables>(
+                MOVE_PRODUCT_CATEGORY,
+                {
+                    input: {
+                        categoryId: appleCategory.id,
+                        parentId: electronicsCategory.id,
+                        index: 10,
+                    },
+                },
+            );
+
+            const afterResult = await getChildrenOf(electronicsCategory.id);
+            expect(afterResult.map(i => i.id)).toEqual([laptopsCategory.id, appleCategory.id]);
+        });
+
+        it('throws if attempting to move into self', async () => {
+            try {
+                await client.query<MoveProductCategory.Mutation, MoveProductCategory.Variables>(
+                    MOVE_PRODUCT_CATEGORY,
+                    {
+                        input: {
+                            categoryId: appleCategory.id,
+                            parentId: appleCategory.id,
+                            index: 0,
+                        },
+                    },
+                );
+                fail('Should have thrown');
+            } catch (err) {
+                expect(err.message).toEqual(
+                    expect.stringContaining(`Cannot move a ProductCategory into itself`),
+                );
+            }
+        });
+
+        it('throws if attempting to move into a decendant of self', async () => {
+            try {
+                await client.query<MoveProductCategory.Mutation, MoveProductCategory.Variables>(
+                    MOVE_PRODUCT_CATEGORY,
+                    {
+                        input: {
+                            categoryId: appleCategory.id,
+                            parentId: appleCategory.id,
+                            index: 0,
+                        },
+                    },
+                );
+                fail('Should have thrown');
+            } catch (err) {
+                expect(err.message).toEqual(
+                    expect.stringContaining(`Cannot move a ProductCategory into itself`),
+                );
+            }
+        });
+
+        async function getChildrenOf(parentId: string): Promise<Array<{ name: string; id: string }>> {
+            const result = await client.query(GET_CATEGORIES);
+            return result.productCategories.items.filter(i => i.parent.id === parentId);
+        }
+    });
+});
+
+const GET_CATEGORIES = gql`
+    query GetCategories {
+        productCategories(languageCode: en) {
+            items {
+                id
+                name
+                position
+                parent {
+                    id
+                    name
+                }
+            }
+        }
+    }
+`;

+ 4 - 0
server/e2e/test-server.ts

@@ -2,6 +2,7 @@ import { INestApplication } from '@nestjs/common';
 import { NestFactory } from '@nestjs/core';
 import * as fs from 'fs';
 import * as path from 'path';
+import { ConnectionOptions } from 'typeorm';
 import { SqljsConnectionOptions } from 'typeorm/driver/sqljs/SqljsConnectionOptions';
 
 import { populate, PopulateOptions } from '../mock-data/populate';
@@ -29,6 +30,9 @@ export class TestServer {
     async init(options: PopulateOptions, customConfig: Partial<VendureConfig> = {}): Promise<void> {
         setTestEnvironment();
         const testingConfig = { ...testConfig, ...customConfig };
+        if (options.logging) {
+            (testingConfig.dbConnectionOptions as Mutable<ConnectionOptions>).logging = true;
+        }
         const dbFilePath = this.getDbFilePath();
         (testingConfig.dbConnectionOptions as Mutable<SqljsConnectionOptions>).location = dbFilePath;
         if (!fs.existsSync(dbFilePath)) {

+ 12 - 0
server/src/api/resolvers/product-category.resolver.ts

@@ -1,6 +1,7 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
     CreateProductCategoryMutationArgs,
+    MoveProductCategoryMutationArgs,
     Permission,
     ProductCategoriesQueryArgs,
     ProductCategoryQueryArgs,
@@ -59,4 +60,15 @@ export class ProductCategoryResolver {
         const { input } = args;
         return this.productCategoryService.update(ctx, input);
     }
+
+    @Mutation()
+    @Allow(Permission.UpdateCatalog)
+    @Decode('categoryId', 'parentId')
+    async moveProductCategory(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MoveProductCategoryMutationArgs,
+    ): Promise<Translated<ProductCategory>> {
+        const { input } = args;
+        return this.productCategoryService.move(ctx, input);
+    }
 }

+ 9 - 0
server/src/api/types/product-category.graphql

@@ -9,6 +9,9 @@ type Mutation {
 
     "Update an existing ProductCategory"
     updateProductCategory(input: UpdateProductCategoryInput!): ProductCategory!
+
+    "Move a ProductCategory to a different parent or index"
+    moveProductCategory(input: MoveProductCategoryInput!): ProductCategory!
 }
 
 type ProductCategoryList implements PaginatedList {
@@ -37,3 +40,9 @@ input ProductCategoryFilterParameter {
     createdAt: DateOperators
     updatedAt: DateOperators
 }
+
+input MoveProductCategoryInput {
+    categoryId: ID!
+    parentId: ID!
+    index: Int!
+}

+ 1 - 1
server/src/common/error/errors.ts

@@ -17,7 +17,7 @@ export class UserInputError extends I18nError {
 
 export class IllegalOperationError extends I18nError {
     constructor(message: string, variables: { [key: string]: string | number } = {}) {
-        super(message || 'error.cannot-transition-order-from-to', variables, 'ILLEGAL_OPERATION');
+        super(message, variables, 'ILLEGAL_OPERATION');
     }
 }
 

+ 4 - 1
server/src/entity/product-category/product-category.entity.ts

@@ -28,9 +28,12 @@ export class ProductCategory extends VendureEntity implements Translatable, HasC
         super(input);
     }
 
-    @Column()
+    @Column({ default: false })
     isRoot: boolean;
 
+    @Column()
+    position: number;
+
     name: LocaleString;
 
     description: LocaleString;

+ 1 - 0
server/src/entity/product-category/product-category.graphql

@@ -4,6 +4,7 @@ type ProductCategory implements Node {
     updatedAt: DateTime!
     languageCode: LanguageCode
     name: String!
+    position: Int!
     description: String!
     featuredAsset: Asset
     assets: [Asset!]!

+ 1 - 0
server/src/i18n/messages/en.json

@@ -1,6 +1,7 @@
 {
   "error": {
     "cannot-modify-role": "The role '{ roleCode }' cannot be modified",
+    "cannot-move-product-category-into-self": "Cannot move a ProductCategory into itself",
     "cannot-transition-order-from-to": "Cannot transition Order from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-to-shipping-when-order-is-empty": "Cannot transition Order to the \"ArrangingShipping\" state when it is empty",
     "cannot-transition-to-payment-without-customer": "Cannot transition Order to the \"ArrangingShipping\" state without Customer details",

+ 5 - 1
server/src/service/helpers/list-query-builder/list-query-builder.ts

@@ -35,7 +35,11 @@ export class ListQueryBuilder {
         if (options.skip !== undefined && options.take === undefined) {
             take = Number.MAX_SAFE_INTEGER;
         }
-        const sort = parseSortParams(this.connection, entity, options.sort);
+        const sort = parseSortParams(
+            this.connection,
+            entity,
+            Object.assign({}, options.sort, extendedOptions.orderBy),
+        );
         const filter = parseFilterParams(this.connection, entity, options.filter);
 
         const qb = this.connection.createQueryBuilder<T>(entity, entity.name.toLowerCase());

+ 75 - 4
server/src/service/services/product-category.service.ts

@@ -1,17 +1,24 @@
 import { InjectConnection } from '@nestjs/typeorm';
-import { CreateProductCategoryInput, UpdateProductCategoryInput } from 'shared/generated-types';
+import {
+    CreateProductCategoryInput,
+    MoveProductCategoryInput,
+    UpdateProductCategoryInput,
+} from 'shared/generated-types';
+import { ROOT_CATEGORY_NAME } from 'shared/shared-constants';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { IllegalOperationError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
-import { assertFound } from '../../common/utils';
+import { assertFound, idsAreEqual } from '../../common/utils';
 import { ProductCategoryTranslation } from '../../entity/product-category/product-category-translation.entity';
 import { ProductCategory } from '../../entity/product-category/product-category.entity';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { translateDeep, translateTree } from '../helpers/utils/translate-entity';
 
 import { AssetService } from './asset.service';
@@ -35,7 +42,12 @@ export class ProductCategoryService {
         const relations = ['featuredAsset', 'facetValues', 'parent', 'channels'];
 
         return this.listQueryBuilder
-            .build(ProductCategory, options, relations, ctx.channelId, { isRoot: false })
+            .build(ProductCategory, options, {
+                relations,
+                channelId: ctx.channelId,
+                where: { isRoot: false },
+                orderBy: { position: 'ASC' },
+            })
             .getManyAndCount()
             .then(async ([productCategories, totalItems]) => {
                 const items = productCategories.map(productCategory =>
@@ -81,6 +93,7 @@ export class ProductCategoryService {
                 if (parent) {
                     category.parent = parent;
                 }
+                category.position = await this.getNextPositionInParent(ctx, input.parentId || undefined);
             },
         });
         await this.saveAssetInputs(productCategory, input);
@@ -100,6 +113,48 @@ export class ProductCategoryService {
         return assertFound(this.findOne(ctx, productCategory.id));
     }
 
+    async move(ctx: RequestContext, input: MoveProductCategoryInput): Promise<Translated<ProductCategory>> {
+        const target = await getEntityOrThrow(this.connection, ProductCategory, input.categoryId, {
+            relations: ['parent'],
+        });
+        const descendants = await this.connection.getTreeRepository(ProductCategory).findDescendants(target);
+
+        if (
+            idsAreEqual(input.parentId, target.id) ||
+            descendants.some(cat => idsAreEqual(input.parentId, cat.id))
+        ) {
+            throw new IllegalOperationError(`error.cannot-move-product-category-into-self`);
+        }
+
+        const siblings = await this.connection
+            .getRepository(ProductCategory)
+            .createQueryBuilder('category')
+            .leftJoin('category.parent', 'parent')
+            .where('parent.id = :id', { id: input.parentId })
+            .orderBy('category.position', 'ASC')
+            .getMany();
+        const normalizedIndex = Math.max(Math.min(input.index, siblings.length), 0);
+
+        if (idsAreEqual(target.parent.id, input.parentId)) {
+            const currentIndex = siblings.findIndex(cat => idsAreEqual(cat.id, input.categoryId));
+            if (currentIndex !== normalizedIndex) {
+                siblings.splice(normalizedIndex, 0, siblings.splice(currentIndex, 1)[0]);
+                siblings.forEach((cat, index) => {
+                    cat.position = index;
+                });
+            }
+        } else {
+            target.parent = new ProductCategory({ id: input.parentId });
+            siblings.splice(normalizedIndex, 0, target);
+            siblings.forEach((cat, index) => {
+                cat.position = index;
+            });
+        }
+
+        await this.connection.getRepository(ProductCategory).save(siblings);
+        return assertFound(this.findOne(ctx, input.categoryId));
+    }
+
     private async saveAssetInputs(productCategory: ProductCategory, input: any) {
         if (input.assetIds || input.featuredAssetId) {
             if (input.assetIds) {
@@ -116,6 +171,21 @@ export class ProductCategoryService {
         }
     }
 
+    /**
+     * Returns the next position value in the given parent category.
+     */
+    async getNextPositionInParent(ctx: RequestContext, maybeParentId?: ID): Promise<number> {
+        const parentId = maybeParentId || (await this.getRootCategory(ctx)).id;
+        const result = await this.connection
+            .getRepository(ProductCategory)
+            .createQueryBuilder('category')
+            .leftJoin('category.parent', 'parent')
+            .select('MAX(category.position)', 'index')
+            .where('parent.id = :id', { id: parentId })
+            .getRawOne();
+        return (result.index || 0) + 1;
+    }
+
     private async getParentCategory(
         ctx: RequestContext,
         parentId?: ID | null,
@@ -156,13 +226,14 @@ export class ProductCategoryService {
         const rootTranslation = await this.connection.getRepository(ProductCategoryTranslation).save(
             new ProductCategoryTranslation({
                 languageCode: DEFAULT_LANGUAGE_CODE,
-                name: '__root_category__',
+                name: ROOT_CATEGORY_NAME,
                 description: 'The root of the ProductCategory tree.',
             }),
         );
 
         const newRoot = new ProductCategory({
             isRoot: true,
+            position: 0,
             translations: [rootTranslation],
             channels: [ctx.channel],
         });

+ 37 - 0
shared/generated-types.ts

@@ -483,6 +483,7 @@ export interface ProductCategory extends Node {
     updatedAt: DateTime;
     languageCode?: LanguageCode | null;
     name: string;
+    position: number;
     description: string;
     featuredAsset?: Asset | null;
     assets: Asset[];
@@ -639,6 +640,7 @@ export interface Mutation {
     updatePaymentMethod: PaymentMethod;
     createProductCategory: ProductCategory;
     updateProductCategory: ProductCategory;
+    moveProductCategory: ProductCategory;
     createProductOptionGroup: ProductOptionGroup;
     updateProductOptionGroup: ProductOptionGroup;
     createProduct: Product;
@@ -1203,6 +1205,12 @@ export interface UpdateProductCategoryInput {
     customFields?: Json | null;
 }
 
+export interface MoveProductCategoryInput {
+    categoryId: string;
+    parentId: string;
+    index: number;
+}
+
 export interface CreateProductOptionGroupInput {
     code: string;
     translations: ProductOptionGroupTranslationInput[];
@@ -1607,6 +1615,9 @@ export interface CreateProductCategoryMutationArgs {
 export interface UpdateProductCategoryMutationArgs {
     input: UpdateProductCategoryInput;
 }
+export interface MoveProductCategoryMutationArgs {
+    input: MoveProductCategoryInput;
+}
 export interface CreateProductOptionGroupMutationArgs {
     input: CreateProductOptionGroupInput;
 }
@@ -3488,6 +3499,7 @@ export namespace ProductCategoryResolvers {
         updatedAt?: UpdatedAtResolver<DateTime, any, Context>;
         languageCode?: LanguageCodeResolver<LanguageCode | null, any, Context>;
         name?: NameResolver<string, any, Context>;
+        position?: PositionResolver<number, any, Context>;
         description?: DescriptionResolver<string, any, Context>;
         featuredAsset?: FeaturedAssetResolver<Asset | null, any, Context>;
         assets?: AssetsResolver<Asset[], any, Context>;
@@ -3507,6 +3519,7 @@ export namespace ProductCategoryResolvers {
         Context
     >;
     export type NameResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type PositionResolver<R = number, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type DescriptionResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type FeaturedAssetResolver<R = Asset | null, Parent = any, Context = any> = Resolver<
         R,
@@ -3877,6 +3890,7 @@ export namespace MutationResolvers {
         updatePaymentMethod?: UpdatePaymentMethodResolver<PaymentMethod, any, Context>;
         createProductCategory?: CreateProductCategoryResolver<ProductCategory, any, Context>;
         updateProductCategory?: UpdateProductCategoryResolver<ProductCategory, any, Context>;
+        moveProductCategory?: MoveProductCategoryResolver<ProductCategory, any, Context>;
         createProductOptionGroup?: CreateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
         updateProductOptionGroup?: UpdateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
         createProduct?: CreateProductResolver<Product, any, Context>;
@@ -4272,6 +4286,16 @@ export namespace MutationResolvers {
         input: UpdateProductCategoryInput;
     }
 
+    export type MoveProductCategoryResolver<R = ProductCategory, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        MoveProductCategoryArgs
+    >;
+    export interface MoveProductCategoryArgs {
+        input: MoveProductCategoryInput;
+    }
+
     export type CreateProductOptionGroupResolver<
         R = ProductOptionGroup,
         Parent = any,
@@ -5375,6 +5399,19 @@ export namespace UpdateProductCategory {
     export type UpdateProductCategory = ProductCategory.Fragment;
 }
 
+export namespace MoveProductCategory {
+    export type Variables = {
+        input: MoveProductCategoryInput;
+    };
+
+    export type Mutation = {
+        __typename?: 'Mutation';
+        moveProductCategory: MoveProductCategory;
+    };
+
+    export type MoveProductCategory = ProductCategory.Fragment;
+}
+
 export namespace GetPromotionList {
     export type Variables = {
         options?: PromotionListOptions | null;

+ 1 - 0
shared/shared-constants.ts

@@ -11,3 +11,4 @@ export const SUPER_ADMIN_USER_IDENTIFIER = 'superadmin';
 export const SUPER_ADMIN_USER_PASSWORD = 'superadmin';
 export const CUSTOMER_ROLE_CODE = '__customer_role__';
 export const CUSTOMER_ROLE_DESCRIPTION = 'Customer';
+export const ROOT_CATEGORY_NAME = '__root_category__';

Some files were not shown because too many files changed in this diff