Explorar el Código

feat: Implement cascading Collection filters

Relates to #71
Michael Bromley hace 6 años
padre
commit
665effee4e

+ 5 - 1
admin-ui/src/app/catalog/components/collection-detail/collection-detail.component.ts

@@ -121,7 +121,11 @@ export class CollectionDetailComponent extends BaseDetailComponent<Collection.Fr
             .pipe(
                 take(1),
                 mergeMap(([category, languageCode]) => {
-                    const input = this.getUpdatedCollection(category, this.detailForm, languageCode);
+                    const input = this.getUpdatedCollection(
+                        category,
+                        this.detailForm,
+                        languageCode,
+                    ) as CreateCollectionInput;
                     return this.dataService.product.createCollection(input);
                 }),
             )

+ 3 - 3
admin-ui/src/app/catalog/components/collection-tree/collection-tree-node.component.ts

@@ -51,7 +51,7 @@ export class CollectionTreeNodeComponent implements OnInit {
         this.root.onMove({
             index: 0,
             parentId,
-            categoryId: category.id,
+            collectionId: category.id,
         });
     }
 
@@ -59,7 +59,7 @@ export class CollectionTreeNodeComponent implements OnInit {
         this.root.onMove({
             index: currentIndex - 1,
             parentId: category.parent.id,
-            categoryId: category.id,
+            collectionId: category.id,
         });
     }
 
@@ -67,7 +67,7 @@ export class CollectionTreeNodeComponent implements OnInit {
         this.root.onMove({
             index: currentIndex + 1,
             parentId: category.parent.id,
-            categoryId: category.id,
+            collectionId: category.id,
         });
     }
 

+ 2 - 2
admin-ui/src/app/catalog/components/collection-tree/collection-tree.component.ts

@@ -12,7 +12,7 @@ import { Collection } from 'shared/generated-types';
 
 import { arrayToTree, HasParent, RootNode } from './array-to-tree';
 
-export type RearrangeEvent = { categoryId: string; parentId: string; index: number };
+export type RearrangeEvent = { collectionId: string; parentId: string; index: number };
 
 @Component({
     selector: 'vdr-collection-tree',
@@ -39,7 +39,7 @@ export class CollectionTreeComponent implements OnChanges {
             throw new Error(`Could not determine the ID of the root Collection`);
         }
         this.rearrange.emit({
-            categoryId: item.id,
+            collectionId: item.id,
             parentId: newParentId,
             index: event.currentIndex,
         });

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
schema-admin.json


+ 6 - 10
schema.json

@@ -17434,19 +17434,15 @@
             "name": "filters",
             "description": null,
             "type": {
-              "kind": "NON_NULL",
+              "kind": "LIST",
               "name": null,
               "ofType": {
-                "kind": "LIST",
+                "kind": "NON_NULL",
                 "name": null,
                 "ofType": {
-                  "kind": "NON_NULL",
-                  "name": null,
-                  "ofType": {
-                    "kind": "INPUT_OBJECT",
-                    "name": "ConfigurableOperationInput",
-                    "ofType": null
-                  }
+                  "kind": "INPUT_OBJECT",
+                  "name": "ConfigurableOperationInput",
+                  "ofType": null
                 }
               }
             },
@@ -17496,7 +17492,7 @@
         "fields": null,
         "inputFields": [
           {
-            "name": "categoryId",
+            "name": "collectionId",
             "description": null,
             "type": {
               "kind": "NON_NULL",

+ 15 - 3
server/e2e/__snapshots__/collection.e2e-spec.ts.snap

@@ -88,10 +88,22 @@ Object {
     "source": "test-url/test-assets/derick-david-409858-unsplash.jpg",
     "type": "IMAGE",
   },
-  "filters": Array [],
+  "filters": Array [
+    Object {
+      "args": Array [
+        Object {
+          "name": "facetValueIds",
+          "type": "FACET_VALUE_IDS",
+          "value": "[\\"T_3\\"]",
+        },
+      ],
+      "code": "facet-value-filter",
+      "description": "Filter by FacetValues",
+    },
+  ],
   "id": "T_4",
   "languageCode": "en",
-  "name": "Apple",
+  "name": "Pear",
   "parent": Object {
     "id": "T_3",
     "name": "Computers",
@@ -101,7 +113,7 @@ Object {
       "description": "Apple stuff ",
       "id": "T_4",
       "languageCode": "en",
-      "name": "Apple",
+      "name": "Pear",
     },
   ],
 }

+ 301 - 71
server/e2e/collection.e2e-spec.ts

@@ -1,23 +1,33 @@
+/* tslint:disable:no-non-null-assertion */
 import gql from 'graphql-tag';
 import path from 'path';
 
+import { FACET_VALUE_FRAGMENT } from '../../admin-ui/src/app/data/definitions/facet-definitions';
 import {
     CREATE_COLLECTION,
     GET_ASSET_LIST,
     GET_COLLECTION,
     MOVE_COLLECTION,
     UPDATE_COLLECTION,
+    UPDATE_PRODUCT,
+    UPDATE_PRODUCT_VARIANTS,
 } from '../../admin-ui/src/app/data/definitions/product-definitions';
 import {
     Collection,
     ConfigArgType,
     CreateCollection,
+    CreateCollectionInput,
+    FacetValue,
     GetAssetList,
     GetCollection,
+    GetProductWithVariants,
     LanguageCode,
     MoveCollection,
+    ProductWithVariants,
     SortOrder,
     UpdateCollection,
+    UpdateProduct,
+    UpdateProductVariants,
 } from '../../shared/generated-types';
 import { ROOT_CATEGORY_NAME } from '../../shared/shared-constants';
 import { facetValueCollectionFilter } from '../src/config/collection/default-collection-filters';
@@ -27,19 +37,18 @@ import { TestAdminClient } from './test-client';
 import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './test-utils';
 
-// TODO: test collection without filters has no ProductVariants
-
 describe('Collection resolver', () => {
     const client = new TestAdminClient();
     const server = new TestServer();
     let assets: GetAssetList.Items[];
-    let electronicsCategory: Collection.Fragment;
-    let computersCategory: Collection.Fragment;
-    let appleCategory: Collection.Fragment;
+    let facetValues: FacetValue.Fragment[];
+    let electronicsCollection: Collection.Fragment;
+    let computersCollection: Collection.Fragment;
+    let pearCollection: Collection.Fragment;
 
     beforeAll(async () => {
         const token = await server.init({
-            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-collections.csv'),
             customerCount: 1,
         });
         await client.init();
@@ -51,6 +60,11 @@ describe('Collection resolver', () => {
             },
         });
         assets = assetsResult.assets.items;
+        const facetValuesResult = await client.query(GET_FACET_VALUES);
+        facetValues = facetValuesResult.facets.items.reduce(
+            (values: any, facet: any) => [...values, ...facet.values],
+            [],
+        );
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -71,7 +85,7 @@ describe('Collection resolver', () => {
                                 arguments: [
                                     {
                                         name: 'facetValueIds',
-                                        value: `["T_1"]`,
+                                        value: `["${getFacetValueId('electronics')}"]`,
                                         type: ConfigArgType.FACET_VALUE_IDS,
                                     },
                                 ],
@@ -84,9 +98,9 @@ describe('Collection resolver', () => {
                 },
             );
 
-            electronicsCategory = result.createCollection;
-            expect(electronicsCategory).toMatchSnapshot();
-            expect(electronicsCategory.parent.name).toBe(ROOT_CATEGORY_NAME);
+            electronicsCollection = result.createCollection;
+            expect(electronicsCollection).toMatchSnapshot();
+            expect(electronicsCollection.parent.name).toBe(ROOT_CATEGORY_NAME);
         });
 
         it('creates a nested category', async () => {
@@ -94,7 +108,7 @@ describe('Collection resolver', () => {
                 CREATE_COLLECTION,
                 {
                     input: {
-                        parentId: electronicsCategory.id,
+                        parentId: electronicsCollection.id,
                         translations: [{ languageCode: LanguageCode.en, name: 'Computers', description: '' }],
                         filters: [
                             {
@@ -102,7 +116,7 @@ describe('Collection resolver', () => {
                                 arguments: [
                                     {
                                         name: 'facetValueIds',
-                                        value: `["T_2"]`,
+                                        value: `["${getFacetValueId('computers')}"]`,
                                         type: ConfigArgType.FACET_VALUE_IDS,
                                     },
                                 ],
@@ -111,8 +125,8 @@ describe('Collection resolver', () => {
                     },
                 },
             );
-            computersCategory = result.createCollection;
-            expect(computersCategory.parent.name).toBe(electronicsCategory.name);
+            computersCollection = result.createCollection;
+            expect(computersCollection.parent.name).toBe(electronicsCollection.name);
         });
 
         it('creates a 2nd level nested category', async () => {
@@ -120,26 +134,37 @@ describe('Collection resolver', () => {
                 CREATE_COLLECTION,
                 {
                     input: {
-                        parentId: computersCategory.id,
-                        translations: [{ languageCode: LanguageCode.en, name: 'Apple', description: '' }],
-                        filters: [],
+                        parentId: computersCollection.id,
+                        translations: [{ languageCode: LanguageCode.en, name: 'Pear', description: '' }],
+                        filters: [
+                            {
+                                code: facetValueCollectionFilter.code,
+                                arguments: [
+                                    {
+                                        name: 'facetValueIds',
+                                        value: `["${getFacetValueId('pear')}"]`,
+                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                    },
+                                ],
+                            },
+                        ],
                     },
                 },
             );
-            appleCategory = result.createCollection;
-            expect(appleCategory.parent.name).toBe(computersCategory.name);
+            pearCollection = result.createCollection;
+            expect(pearCollection.parent.name).toBe(computersCollection.name);
         });
     });
 
     it('collection query', async () => {
         const result = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
-            id: computersCategory.id,
+            id: computersCollection.id,
         });
         if (!result.collection) {
             fail(`did not return the category`);
             return;
         }
-        expect(result.collection.id).toBe(computersCategory.id);
+        expect(result.collection.id).toBe(computersCollection.id);
     });
 
     it('updateCollection', async () => {
@@ -147,10 +172,9 @@ describe('Collection resolver', () => {
             UPDATE_COLLECTION,
             {
                 input: {
-                    id: appleCategory.id,
+                    id: pearCollection.id,
                     assetIds: [assets[1].id],
                     featuredAssetId: assets[1].id,
-                    filters: [],
                     translations: [{ languageCode: LanguageCode.en, description: 'Apple stuff ' }],
                 },
             },
@@ -165,56 +189,67 @@ describe('Collection resolver', () => {
                 MOVE_COLLECTION,
                 {
                     input: {
-                        categoryId: appleCategory.id,
-                        parentId: electronicsCategory.id,
+                        collectionId: pearCollection.id,
+                        parentId: electronicsCollection.id,
                         index: 0,
                     },
                 },
             );
 
-            expect(result.moveCollection.parent.id).toBe(electronicsCategory.id);
+            expect(result.moveCollection.parent.id).toBe(electronicsCollection.id);
+
+            const positions = await getChildrenOf(electronicsCollection.id);
+            expect(positions.map(i => i.id)).toEqual([pearCollection.id, computersCollection.id]);
+        });
 
-            const positions = await getChildrenOf(electronicsCategory.id);
-            expect(positions.map(i => i.id)).toEqual([appleCategory.id, computersCategory.id]);
+        it('re-evaluates Collection contents on move', async () => {
+            const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
+            expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
+                'Laptop 13 inch 8GB',
+                'Laptop 15 inch 8GB',
+                'Laptop 13 inch 16GB',
+                'Laptop 15 inch 16GB',
+                'Instant Camera',
+            ]);
         });
 
         it('alters the position in the current parent', async () => {
             await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                 input: {
-                    categoryId: appleCategory.id,
-                    parentId: electronicsCategory.id,
+                    collectionId: pearCollection.id,
+                    parentId: electronicsCollection.id,
                     index: 1,
                 },
             });
 
-            const afterResult = await getChildrenOf(electronicsCategory.id);
-            expect(afterResult.map(i => i.id)).toEqual([computersCategory.id, appleCategory.id]);
+            const afterResult = await getChildrenOf(electronicsCollection.id);
+            expect(afterResult.map(i => i.id)).toEqual([computersCollection.id, pearCollection.id]);
         });
 
         it('corrects an out-of-bounds negative index value', async () => {
             await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                 input: {
-                    categoryId: appleCategory.id,
-                    parentId: electronicsCategory.id,
+                    collectionId: pearCollection.id,
+                    parentId: electronicsCollection.id,
                     index: -3,
                 },
             });
 
-            const afterResult = await getChildrenOf(electronicsCategory.id);
-            expect(afterResult.map(i => i.id)).toEqual([appleCategory.id, computersCategory.id]);
+            const afterResult = await getChildrenOf(electronicsCollection.id);
+            expect(afterResult.map(i => i.id)).toEqual([pearCollection.id, computersCollection.id]);
         });
 
         it('corrects an out-of-bounds positive index value', async () => {
             await client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                 input: {
-                    categoryId: appleCategory.id,
-                    parentId: electronicsCategory.id,
+                    collectionId: pearCollection.id,
+                    parentId: electronicsCollection.id,
                     index: 10,
                 },
             });
 
-            const afterResult = await getChildrenOf(electronicsCategory.id);
-            expect(afterResult.map(i => i.id)).toEqual([computersCategory.id, appleCategory.id]);
+            const afterResult = await getChildrenOf(electronicsCollection.id);
+            expect(afterResult.map(i => i.id)).toEqual([computersCollection.id, pearCollection.id]);
         });
 
         it(
@@ -223,8 +258,8 @@ describe('Collection resolver', () => {
                 () =>
                     client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                         input: {
-                            categoryId: appleCategory.id,
-                            parentId: appleCategory.id,
+                            collectionId: pearCollection.id,
+                            parentId: pearCollection.id,
                             index: 0,
                         },
                     }),
@@ -238,8 +273,8 @@ describe('Collection resolver', () => {
                 () =>
                     client.query<MoveCollection.Mutation, MoveCollection.Variables>(MOVE_COLLECTION, {
                         input: {
-                            categoryId: appleCategory.id,
-                            parentId: appleCategory.id,
+                            collectionId: pearCollection.id,
+                            parentId: pearCollection.id,
                             index: 0,
                         },
                     }),
@@ -249,42 +284,224 @@ describe('Collection resolver', () => {
 
         async function getChildrenOf(parentId: string): Promise<Array<{ name: string; id: string }>> {
             const result = await client.query(GET_COLLECTIONS);
-            return result.collections.items.filter(i => i.parent.id === parentId);
+            return result.collections.items.filter((i: any) => i.parent.id === parentId);
         }
     });
 
     describe('filters', () => {
-        it('facetValue filter', async () => {
-            const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, {
-                id: electronicsCategory.id,
+        it('Collection with no filters has no productVariants', async () => {
+            const result = await client.query(CREATE_COLLECTION_SELECT_VARIANTS, {
+                input: {
+                    translations: [{ languageCode: LanguageCode.en, name: 'Empty', description: '' }],
+                    filters: [],
+                } as CreateCollectionInput,
+            });
+            expect(result.createCollection.productVariants.totalItems).toBe(0);
+        });
+
+        describe('facetValue filter', () => {
+            it('electronics', async () => {
+                const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, {
+                    id: electronicsCollection.id,
+                });
+                expect(result.collection.productVariants.items.map(i => i.name)).toEqual([
+                    'Laptop 13 inch 8GB',
+                    'Laptop 15 inch 8GB',
+                    'Laptop 13 inch 16GB',
+                    'Laptop 15 inch 16GB',
+                    'Curvy Monitor 24 inch',
+                    'Curvy Monitor 27 inch',
+                    'Gaming PC i7-8700 240GB SSD',
+                    'Gaming PC R7-2700 240GB SSD',
+                    'Gaming PC i7-8700 120GB SSD',
+                    'Gaming PC R7-2700 120GB SSD',
+                    'Hard Drive 1TB',
+                    'Hard Drive 2TB',
+                    'Hard Drive 3TB',
+                    'Hard Drive 4TB',
+                    'Hard Drive 6TB',
+                    'Clacky Keyboard',
+                    'USB Cable',
+                    'Instant Camera',
+                    'Camera Lens',
+                    'Tripod',
+                    'SLR Camera',
+                ]);
+            });
+
+            it('computers', async () => {
+                const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, {
+                    id: computersCollection.id,
+                });
+                expect(result.collection.productVariants.items.map(i => i.name)).toEqual([
+                    'Laptop 13 inch 8GB',
+                    'Laptop 15 inch 8GB',
+                    'Laptop 13 inch 16GB',
+                    'Laptop 15 inch 16GB',
+                    'Curvy Monitor 24 inch',
+                    'Curvy Monitor 27 inch',
+                    'Gaming PC i7-8700 240GB SSD',
+                    'Gaming PC R7-2700 240GB SSD',
+                    'Gaming PC i7-8700 120GB SSD',
+                    'Gaming PC R7-2700 120GB SSD',
+                    'Hard Drive 1TB',
+                    'Hard Drive 2TB',
+                    'Hard Drive 3TB',
+                    'Hard Drive 4TB',
+                    'Hard Drive 6TB',
+                    'Clacky Keyboard',
+                    'USB Cable',
+                ]);
+            });
+
+            it('photo and pear', async () => {
+                const result = await client.query(CREATE_COLLECTION_SELECT_VARIANTS, {
+                    input: {
+                        translations: [
+                            { languageCode: LanguageCode.en, name: 'Photo Pear', description: '' },
+                        ],
+                        filters: [
+                            {
+                                code: facetValueCollectionFilter.code,
+                                arguments: [
+                                    {
+                                        name: 'facetValueIds',
+                                        value: `["${getFacetValueId('pear')}", "${getFacetValueId(
+                                            'photo',
+                                        )}"]`,
+                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                    },
+                                ],
+                            },
+                        ],
+                    } as CreateCollectionInput,
+                });
+                expect(result.createCollection.productVariants.items.map(i => i.name)).toEqual([
+                    'Instant Camera',
+                ]);
+            });
+        });
+
+        describe('re-evaluation of contents on changes', () => {
+            let products: ProductWithVariants.Fragment[];
+
+            beforeAll(async () => {
+                const result = await client.query(gql`
+                    query {
+                        products {
+                            items {
+                                id
+                                name
+                                variants {
+                                    id
+                                }
+                            }
+                        }
+                    }
+                `);
+                products = result.products.items;
+            });
+
+            it('updates contents when Product is updated', async () => {
+                await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                    input: {
+                        id: products[1].id,
+                        facetValueIds: [
+                            getFacetValueId('electronics'),
+                            getFacetValueId('computers'),
+                            getFacetValueId('pear'),
+                        ],
+                    },
+                });
+
+                const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
+                expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
+                    'Laptop 13 inch 8GB',
+                    'Laptop 15 inch 8GB',
+                    'Laptop 13 inch 16GB',
+                    'Laptop 15 inch 16GB',
+                    'Curvy Monitor 24 inch',
+                    'Curvy Monitor 27 inch',
+                    'Instant Camera',
+                ]);
+            });
+
+            it('updates contents when ProductVariant is updated', async () => {
+                const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
+                await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                    UPDATE_PRODUCT_VARIANTS,
+                    {
+                        input: [
+                            {
+                                id: gamingPcFirstVariant.id,
+                                facetValueIds: [getFacetValueId('pear')],
+                            },
+                        ],
+                    },
+                );
+                const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
+                expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
+                    'Laptop 13 inch 8GB',
+                    'Laptop 15 inch 8GB',
+                    'Laptop 13 inch 16GB',
+                    'Laptop 15 inch 16GB',
+                    'Curvy Monitor 24 inch',
+                    'Curvy Monitor 27 inch',
+                    'Gaming PC i7-8700 240GB SSD',
+                    'Instant Camera',
+                ]);
+            });
+
+            it('correctly filters when ProductVariant and Product both have matching FacetValue', async () => {
+                const gamingPcFirstVariant = products.find(p => p.name === 'Gaming PC')!.variants[0];
+                await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                    UPDATE_PRODUCT_VARIANTS,
+                    {
+                        input: [
+                            {
+                                id: gamingPcFirstVariant.id,
+                                facetValueIds: [getFacetValueId('electronics'), getFacetValueId('pear')],
+                            },
+                        ],
+                    },
+                );
+                const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
+                expect(result.collection.productVariants.items.map((i: any) => i.name)).toEqual([
+                    'Laptop 13 inch 8GB',
+                    'Laptop 15 inch 8GB',
+                    'Laptop 13 inch 16GB',
+                    'Laptop 15 inch 16GB',
+                    'Curvy Monitor 24 inch',
+                    'Curvy Monitor 27 inch',
+                    'Gaming PC i7-8700 240GB SSD',
+                    'Instant Camera',
+                ]);
             });
-            expect(result.collection.productVariants.items.map(i => i.name)).toEqual([
-                'Laptop 13 inch 8GB',
-                'Laptop 15 inch 8GB',
-                'Laptop 13 inch 16GB',
-                'Laptop 15 inch 16GB',
-                'Curvy Monitor 24 inch',
-                'Curvy Monitor 27 inch',
-                'Gaming PC i7-8700 240GB SSD',
-                'Gaming PC R7-2700 240GB SSD',
-                'Gaming PC i7-8700 120GB SSD',
-                'Gaming PC R7-2700 120GB SSD',
-                'Hard Drive 1TB',
-                'Hard Drive 2TB',
-                'Hard Drive 3TB',
-                'Hard Drive 4TB',
-                'Hard Drive 6TB',
-                'Clacky Keyboard',
-                'USB Cable',
-                'Instant Camera',
-                'Camera Lens',
-                'Tripod',
-                'SLR Camera',
-            ]);
         });
     });
+
+    function getFacetValueId(code: string): string {
+        const match = facetValues.find(fv => fv.code === code);
+        if (!match) {
+            throw new Error(`Could not find a FacetValue with the code "${code}"`);
+        }
+        return match.id;
+    }
 });
 
+const GET_FACET_VALUES = gql`
+    query {
+        facets {
+            items {
+                values {
+                    ...FacetValue
+                }
+            }
+        }
+    }
+    ${FACET_VALUE_FRAGMENT}
+`;
+
 const GET_COLLECTIONS = gql`
     query GetCollections {
         collections(languageCode: en) {
@@ -316,3 +533,16 @@ const GET_COLLECTION_PRODUCT_VARIANTS = gql`
         }
     }
 `;
+
+const CREATE_COLLECTION_SELECT_VARIANTS = gql`
+    mutation($input: CreateCollectionInput!) {
+        createCollection(input: $input) {
+            productVariants {
+                items {
+                    name
+                }
+                totalItems
+            }
+        }
+    }
+`;

+ 22 - 0
server/e2e/fixtures/e2e-products-collections.csv

@@ -0,0 +1,22 @@
+name            , slug            , description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   , assets                             , facets                                             , optionGroups      , optionValues        , sku          , price   , taxCategory , variantAssets , variantFacets
+Laptop          , laptop          , "Now equipped with seventh-generation Intel Core processors, Laptop is snappier than ever. From daily tasks like launching apps and opening files to more advanced computing, you can power through your day thanks to faster SSDs and Turbo Boost processing up to 3.6GHz."                                                                                                                                                                                                                  , derick-david-409858-unsplash.jpg   , category:electronics|category:computers|brand:pear , "screen size|RAM" , "13 inch|8GB"       , L2201308     , 1299.00 , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , "15 inch|8GB"       , L2201508     , 1399.00 , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , "13 inch|16GB"      , L2201316     , 2199.00 , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , "15 inch|16GB"      , L2201516     , 2299.00 , standard    ,               ,
+Curvy Monitor   , curvy-monitor   , "Discover a truly immersive viewing experience with this monitor curved more deeply than any other. Wrapping around your field of vision the 1,800 R screencreates a wider field of view, enhances depth perception, and minimises peripheral distractions to draw you deeper in to your content."                                                                                                                                                                                            , alexandru-acea-686569-unsplash.jpg , category:electronics|category:computers|brand:bell , monitor size      , 24 inch             , C24F390      , 143.74  , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , 27 inch             , C27F390      , 169.94  , standard    ,               ,
+Gaming PC       , gaming-pc       , "This pc is optimised for gaming, and is also VR ready. The Intel Core-i7 CPU and High Performance GPU give the computer the raw power it needs to function at a high level."                                                                                                                                                                                                                                                                                                                 , florian-olivo-1166419-unsplash.jpg , category:electronics|category:computers            , "cpu|HDD"         , "i7-8700|240GB SSD" , CGS480VR1063 , 1087.20 , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , "R7-2700|240GB SSD" , CGS480VR1064 , 1099.95 , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , "i7-8700|120GB SSD" , CGS480VR1065 , 931.20  , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , "R7-2700|120GB SSD" , CGS480VR1066 , 949.20  , standard    ,               ,
+Hard Drive      , hard-drive      , "Boost your PC storage with this internal hard drive, designed just for desktop and all-in-one PCs."                                                                                                                                                                                                                                                                                                                                                                                          , vincent-botta-736919-unsplash.jpg  , category:electronics|category:computers            , "HDD capacity"    , 1TB                 , IHD455T1     , 37.99   , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , 2TB                 , IHD455T2     , 53.74   , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , 3TB                 , IHD455T3     , 78.96   , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , 4TB                 , IHD455T4     , 92.99   , standard    ,               ,
+                ,                 ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                                    ,                   , 6TB                 , IHD455T6     , 134.35  , standard    ,               ,
+Clacky Keyboard , clacky-keyboard , "Let all your colleagues know that you are typing on this exclusive, colorful klicky-klacky keyboard. Huge travel on each keypress ensures maximum klack on each and every keystroke."                                                                                                                                                                                                                                                                                                        ,                                    , category:electronics|category:computers            ,                   ,                     , A4TKLA45535  , 74.89   , standard    ,               ,
+USB Cable       , usb-cable       , "Solid conductors eliminate strand-interaction distortion and reduce jitter. As the surface is made of high-purity silver, the performance is very close to that of a solid silver cable, but priced much closer to solid copper cable."                                                                                                                                                                                                                                                      ,                                    , category:electronics|category:computers            ,                   ,                     , USBCIN01.5MI , 69.00   , standard    ,               ,
+Instant Camera  , instant-camera  , "With its nostalgic design and simple point-and-shoot functionality, the Instant Camera is the perfect pick to get started with instant photography."                                                                                                                                                                                                                                                                                                                                         ,                                    , category:electronics|category:photo                ,                   ,                     , IC22MWDD     , 174.99  , standard    ,               , brand:pear
+Camera Lens     , camera-lens     , "This lens is a Di type lens using an optical system with improved multi-coating designed to function with digital SLR cameras as well as film cameras."                                                                                                                                                                                                                                                                                                                                      ,                                    , category:electronics|category:photo                ,                   ,                     , B0012UUP02   , 104.00  , standard    ,               ,
+Tripod          , tripod          , "Capture vivid, professional-style photographs with help from this lightweight tripod. The adjustable-height tripod makes it easy to achieve reliable stability and score just the right angle when going after that award-winning shot."                                                                                                                                                                                                                                                     ,                                    , category:electronics|category:photo|brand:bell     ,                   ,                     , B00XI87KV8   , 14.98   , standard    ,               ,
+SLR Camera      , slr-camera      , "Retro styled, portable in size and built around a powerful 24-megapixel APS-C CMOS sensor, this digital camera is the ideal companion for creative everyday photography. Packed full of high spec features such as an advanced hybrid autofocus system able to keep pace with even the most active subjects, a speedy 6fps continuous-shooting mode, high-resolution electronic viewfinder and intuitive swivelling touchscreen, it brings professional image making into everyone’s grasp." ,                                    , category:electronics|category:photo|brand:bell     ,                   ,                     , B07D75V44S   , 521.00  , standard    ,               ,

+ 2 - 2
server/src/api/resolvers/admin/collection.resolver.ts

@@ -78,13 +78,13 @@ export class CollectionResolver {
         @Args() args: UpdateCollectionMutationArgs,
     ): Promise<Translated<Collection>> {
         const { input } = args;
-        this.idCodecService.decodeConfigurableOperation(input.filters);
+        this.idCodecService.decodeConfigurableOperation(input.filters || []);
         return this.collectionService.update(ctx, input).then(this.encodeFilters);
     }
 
     @Mutation()
     @Allow(Permission.UpdateCatalog)
-    @Decode('categoryId', 'parentId')
+    @Decode('collectionId', 'parentId')
     async moveCollection(
         @Ctx() ctx: RequestContext,
         @Args() args: MoveCollectionMutationArgs,

+ 2 - 2
server/src/api/schema/admin-api/collection.api.graphql

@@ -19,7 +19,7 @@ type Mutation {
 input CollectionListOptions
 
 input MoveCollectionInput {
-    categoryId: ID!
+    collectionId: ID!
     parentId: ID!
     index: Int!
 }
@@ -44,6 +44,6 @@ input UpdateCollectionInput {
     featuredAssetId: ID
     parentId: ID
     assetIds: [ID!]
-    filters: [ConfigurableOperationInput!]!
+    filters: [ConfigurableOperationInput!]
     translations: [CollectionTranslationInput!]!
 }

+ 1 - 1
server/src/config/collection/default-collection-filters.ts

@@ -26,7 +26,7 @@ export const facetValueCollectionFilter = new CollectionFilter({
                 }),
             )
             .groupBy('productVariant.id')
-            .having(`COUNT(1) = :count`, { count: args.facetValueIds.length });
+            .having(`COUNT(1) >= :count`, { count: args.facetValueIds.length });
         return qb;
     },
 });

+ 87 - 41
server/src/service/services/collection.service.ts

@@ -1,3 +1,4 @@
+import { OnModuleInit } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import { Connection } from 'typeorm';
 
@@ -9,6 +10,7 @@ import {
 } from '../../../../shared/generated-types';
 import { ROOT_CATEGORY_NAME } from '../../../../shared/shared-constants';
 import { ID, PaginatedList } from '../../../../shared/shared-types';
+import { notNullOrUndefined } from '../../../../shared/shared-utils';
 import { RequestContext } from '../../api/common/request-context';
 import { configurableDefToOperation } from '../../common/configurable-operation';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
@@ -21,6 +23,8 @@ import { facetValueCollectionFilter } from '../../config/collection/default-coll
 import { CollectionTranslation } from '../../entity/collection/collection-translation.entity';
 import { Collection } from '../../entity/collection/collection.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { EventBus } from '../../event-bus/event-bus';
+import { CatalogModificationEvent } from '../../event-bus/events/catalog-modification-event';
 import { AssetUpdater } from '../helpers/asset-updater/asset-updater';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
@@ -30,7 +34,7 @@ import { translateDeep } from '../helpers/utils/translate-entity';
 import { ChannelService } from './channel.service';
 import { FacetValueService } from './facet-value.service';
 
-export class CollectionService {
+export class CollectionService implements OnModuleInit {
     private rootCategories: { [channelCode: string]: Collection } = {};
     private availableFilters: Array<CollectionFilter<any>> = [facetValueCollectionFilter];
 
@@ -41,8 +45,18 @@ export class CollectionService {
         private facetValueService: FacetValueService,
         private listQueryBuilder: ListQueryBuilder,
         private translatableSaver: TranslatableSaver,
+        private eventBus: EventBus,
     ) {}
 
+    onModuleInit() {
+        this.eventBus.subscribe(CatalogModificationEvent, async event => {
+            const collections = await this.connection.getRepository(Collection).find();
+            for (const collection of collections) {
+                await this.applyCollectionFilters(collection);
+            }
+        });
+    }
+
     async findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<Collection>,
@@ -103,14 +117,13 @@ export class CollectionService {
     }
 
     /**
-     * Gets the ancestors of a given category. Note that since ProductCategories are implemented as an adjacency list, this method
-     * will produce more queries the deeper the category is in the tree.
-     * @param categoryId
+     * Gets the ancestors of a given collection. Note that since ProductCategories are implemented as an adjacency list, this method
+     * will produce more queries the deeper the collection is in the tree.
      */
-    getAncestors(categoryId: ID): Promise<Collection[]>;
-    getAncestors(categoryId: ID, ctx: RequestContext): Promise<Array<Translated<Collection>>>;
+    getAncestors(collectionId: ID): Promise<Collection[]>;
+    getAncestors(collectionId: ID, ctx: RequestContext): Promise<Array<Translated<Collection>>>;
     async getAncestors(
-        categoryId: ID,
+        collectionId: ID,
         ctx?: RequestContext,
     ): Promise<Array<Translated<Collection> | Collection>> {
         const getParent = async (id, _ancestors: Collection[] = []): Promise<Collection[]> => {
@@ -128,13 +141,11 @@ export class CollectionService {
             }
             return _ancestors;
         };
-        const ancestors = await getParent(categoryId);
+        const ancestors = await getParent(collectionId);
 
         return this.connection
             .getRepository(Collection)
-            .findByIds(ancestors.map(c => c.id), {
-                relations: ['facetValues'],
-            })
+            .findByIds(ancestors.map(c => c.id))
             .then(categories => {
                 return ctx ? categories.map(c => translateDeep(c, ctx.languageCode)) : categories;
             });
@@ -147,16 +158,16 @@ export class CollectionService {
             translationType: CollectionTranslation,
             beforeSave: async coll => {
                 await this.channelService.assignToChannels(coll, ctx);
-                const parent = await this.getParentCategory(ctx, input.parentId);
+                const parent = await this.getParentCollection(ctx, input.parentId);
                 if (parent) {
                     coll.parent = parent;
                 }
                 coll.position = await this.getNextPositionInParent(ctx, input.parentId || undefined);
                 coll.filters = this.getCollectionFiltersFromInput(input);
-                coll.productVariants = await this.applyCollectionFilters(coll.filters);
                 await this.assetUpdater.updateEntityAssets(coll, input);
             },
         });
+        await this.applyCollectionFilters(collection);
         return assertFound(this.findOne(ctx, collection.id));
     }
 
@@ -166,19 +177,23 @@ export class CollectionService {
             entityType: Collection,
             translationType: CollectionTranslation,
             beforeSave: async coll => {
-                coll.filters = this.getCollectionFiltersFromInput(input);
-                coll.productVariants = await this.applyCollectionFilters(coll.filters);
+                if (input.filters) {
+                    coll.filters = this.getCollectionFiltersFromInput(input);
+                }
                 await this.assetUpdater.updateEntityAssets(coll, input);
             },
         });
+        if (input.filters) {
+            await this.applyCollectionFilters(collection);
+        }
         return assertFound(this.findOne(ctx, collection.id));
     }
 
     async move(ctx: RequestContext, input: MoveCollectionInput): Promise<Translated<Collection>> {
-        const target = await getEntityOrThrow(this.connection, Collection, input.categoryId, {
+        const target = await getEntityOrThrow(this.connection, Collection, input.collectionId, {
             relations: ['parent'],
         });
-        const descendants = await this.getDescendants(ctx, input.categoryId);
+        const descendants = await this.getDescendants(ctx, input.collectionId);
 
         if (
             idsAreEqual(input.parentId, target.id) ||
@@ -189,15 +204,15 @@ export class CollectionService {
 
         const siblings = await this.connection
             .getRepository(Collection)
-            .createQueryBuilder('category')
-            .leftJoin('category.parent', 'parent')
+            .createQueryBuilder('collection')
+            .leftJoin('collection.parent', 'parent')
             .where('parent.id = :id', { id: input.parentId })
-            .orderBy('category.position', 'ASC')
+            .orderBy('collection.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));
+            const currentIndex = siblings.findIndex(cat => idsAreEqual(cat.id, input.collectionId));
             if (currentIndex !== normalizedIndex) {
                 siblings.splice(normalizedIndex, 0, siblings.splice(currentIndex, 1)[0]);
                 siblings.forEach((cat, index) => {
@@ -213,7 +228,8 @@ export class CollectionService {
         }
 
         await this.connection.getRepository(Collection).save(siblings);
-        return assertFound(this.findOne(ctx, input.categoryId));
+        await this.applyCollectionFilters(target);
+        return assertFound(this.findOne(ctx, input.collectionId));
     }
 
     private getCollectionFiltersFromInput(
@@ -240,49 +256,79 @@ export class CollectionService {
         return filters;
     }
 
-    private async applyCollectionFilters(filters: ConfigurableOperation[]): Promise<ProductVariant[]> {
+    private async applyCollectionFilters(collection: Collection) {
+        const ancestorFilters = await this.getAncestors(collection.id).then(ancestors =>
+            ancestors.reduce(
+                (filters, c) => [...filters, ...(c.filters || [])],
+                [] as ConfigurableOperation[],
+            ),
+        );
+        collection.productVariants = await this.getFilteredProductVariants([
+            ...ancestorFilters,
+            ...(collection.filters || []),
+        ]);
+        await this.connection.getRepository(Collection).save(collection);
+    }
+
+    /**
+     * Applies the CollectionFilters and returns an array of ProductVariant entities which match.
+     */
+    private async getFilteredProductVariants(filters: ConfigurableOperation[]): Promise<ProductVariant[]> {
+        if (filters.length === 0) {
+            return [];
+        }
+        const facetFilters = filters.filter(f => f.code === facetValueCollectionFilter.code);
         let qb = this.connection.getRepository(ProductVariant).createQueryBuilder('productVariant');
-        for (const filter of filters) {
-            if (filter.code === facetValueCollectionFilter.code) {
-                qb = facetValueCollectionFilter.apply(qb, filter.args);
-            }
+        if (facetFilters) {
+            const mergedArgs = facetFilters
+                .map(f => f.args[0].value)
+                .filter(notNullOrUndefined)
+                .map(value => JSON.parse(value))
+                .reduce((all, ids) => [...all, ...ids]);
+            qb = facetValueCollectionFilter.apply(qb, [
+                {
+                    name: facetFilters[0].args[0].name,
+                    type: facetFilters[0].args[0].type,
+                    value: JSON.stringify(Array.from(new Set(mergedArgs))),
+                },
+            ]);
         }
         return qb.getMany();
     }
 
     /**
-     * Returns the next position value in the given parent category.
+     * Returns the next position value in the given parent collection.
      */
     private async getNextPositionInParent(ctx: RequestContext, maybeParentId?: ID): Promise<number> {
-        const parentId = maybeParentId || (await this.getRootCategory(ctx)).id;
+        const parentId = maybeParentId || (await this.getRootCollection(ctx)).id;
         const result = await this.connection
             .getRepository(Collection)
-            .createQueryBuilder('category')
-            .leftJoin('category.parent', 'parent')
-            .select('MAX(category.position)', 'index')
+            .createQueryBuilder('collection')
+            .leftJoin('collection.parent', 'parent')
+            .select('MAX(collection.position)', 'index')
             .where('parent.id = :id', { id: parentId })
             .getRawOne();
         return (result.index || 0) + 1;
     }
 
-    private async getParentCategory(
+    private async getParentCollection(
         ctx: RequestContext,
         parentId?: ID | null,
     ): Promise<Collection | undefined> {
         if (parentId) {
             return this.connection
                 .getRepository(Collection)
-                .createQueryBuilder('category')
-                .leftJoin('category.channels', 'channel')
-                .where('category.id = :id', { id: parentId })
+                .createQueryBuilder('collection')
+                .leftJoin('collection.channels', 'channel')
+                .where('collection.id = :id', { id: parentId })
                 .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
                 .getOne();
         } else {
-            return this.getRootCategory(ctx);
+            return this.getRootCollection(ctx);
         }
     }
 
-    private async getRootCategory(ctx: RequestContext): Promise<Collection> {
+    private async getRootCollection(ctx: RequestContext): Promise<Collection> {
         const cachedRoot = this.rootCategories[ctx.channel.code];
 
         if (cachedRoot) {
@@ -291,9 +337,9 @@ export class CollectionService {
 
         const existingRoot = await this.connection
             .getRepository(Collection)
-            .createQueryBuilder('category')
-            .leftJoin('category.channels', 'channel')
-            .where('category.isRoot = :isRoot', { isRoot: true })
+            .createQueryBuilder('collection')
+            .leftJoin('collection.channels', 'channel')
+            .where('collection.isRoot = :isRoot', { isRoot: true })
             .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
             .getOne();
 

+ 1 - 1
shared/generated-shop-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-03-06T10:36:47+01:00
+// Generated in 2019-03-06T21:22:24+01:00
 export type Maybe<T> = T | null;
 
 export interface OrderListOptions {

+ 3 - 3
shared/generated-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-03-06T10:36:48+01:00
+// Generated in 2019-03-06T21:22:26+01:00
 export type Maybe<T> = T | null;
 
 
@@ -758,7 +758,7 @@ export interface UpdateCollectionInput {
   
   assetIds?: Maybe<string[]>;
   
-  filters: ConfigurableOperationInput[];
+  filters?: Maybe<ConfigurableOperationInput[]>;
   
   translations: CollectionTranslationInput[];
   
@@ -767,7 +767,7 @@ export interface UpdateCollectionInput {
 
 export interface MoveCollectionInput {
   
-  categoryId: string;
+  collectionId: string;
   
   parentId: string;
   

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio