Browse Source

feat(core): Add boolean combination support on default CollectionFilters

This commit makes it possible to combine multiple of the default collection filters and decide
whether they combine with a logical AND or OR.
Michael Bromley 3 years ago
parent
commit
8889ac2c4d
1 changed files with 114 additions and 15 deletions
  1. 114 15
      packages/core/src/config/catalog/default-collection-filters.ts

+ 114 - 15
packages/core/src/config/catalog/default-collection-filters.ts

@@ -1,11 +1,42 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { customAlphabet } from 'nanoid';
 
+import { ConfigArgDef } from '../../common/configurable-operation';
 import { UserInputError } from '../../common/error/errors';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 
 import { CollectionFilter } from './collection-filter';
 
+/**
+ * @description
+ * Used to created unique key names for DB query parameters, to avoid conflicts if the
+ * same filter is applied multiple times.
+ */
+export function randomSuffix(prefix: string) {
+    const nanoid = customAlphabet('123456789abcdefghijklmnopqrstuvwxyz', 6);
+    return `${prefix}_${nanoid()}`;
+}
+
+/**
+ * @description
+ * Add this to your CollectionFilter `args` object to display the standard UI component
+ * for selecting the combination mode when working with multiple filters.
+ */
+export const combineWithAndArg: ConfigArgDef<'boolean'> = {
+    type: 'boolean',
+    label: [{ languageCode: LanguageCode.en, value: 'Combination mode' }],
+    description: [
+        {
+            languageCode: LanguageCode.en,
+            value: 'If this filter is being combined with other filters, do all conditions need to be satisfied (AND), or just one or the other (OR)?',
+        },
+    ],
+    defaultValue: true,
+    ui: {
+        component: 'combination-mode-form-input',
+    },
+};
+
 /**
  * Filters for ProductVariants having the given facetValueIds (including parent Product)
  */
@@ -29,6 +60,7 @@ export const facetValueCollectionFilter = new CollectionFilter({
                 },
             ],
         },
+        combineWithAnd: combineWithAndArg,
     },
     code: 'facet-value-filter',
     description: [{ languageCode: LanguageCode.en, value: 'Filter by facet values' }],
@@ -70,13 +102,21 @@ export const facetValueCollectionFilter = new CollectionFilter({
                 .select('variant_ids_table.variant_id')
                 .from(`(${union.getQuery()})`, 'variant_ids_table');
 
-            qb.andWhere(`productVariant.id IN (${variantIds.getQuery()})`).setParameters({
+            const clause = `productVariant.id IN (${variantIds.getQuery()})`;
+            const params = {
                 [idsName]: ids,
                 [countName]: args.containsAny ? 1 : ids.length,
-            });
+            };
+            if (args.combineWithAnd !== false) {
+                qb.andWhere(clause).setParameters(params);
+            } else {
+                qb.orWhere(clause).setParameters(params);
+            }
         } else {
             // If no facetValueIds are specified, no ProductVariants will be matched.
-            qb.andWhere('1 = 0');
+            if (args.combineWithAnd !== false) {
+                qb.andWhere('1 = 0');
+            }
         }
         return qb;
     },
@@ -97,13 +137,13 @@ export const variantNameCollectionFilter = new CollectionFilter({
             },
         },
         term: { type: 'string' },
+        combineWithAnd: combineWithAndArg,
     },
     code: 'variant-name-filter',
     description: [{ languageCode: LanguageCode.en, value: 'Filter by product variant name' }],
     apply: (qb, args) => {
         let translationAlias = `variant_name_filter_translation`;
-        const nanoid = customAlphabet('123456789abcdefghijklmnopqrstuvwxyz', 6);
-        const termName = `term_${nanoid()}`;
+        const termName = randomSuffix(`term`);
         const translationsJoin = qb.expressionMap.joinAttributes.find(
             ja => ja.entityOrProperty === 'productVariant.translations',
         );
@@ -113,26 +153,41 @@ export const variantNameCollectionFilter = new CollectionFilter({
             translationAlias = translationsJoin.alias.name;
         }
         const LIKE = qb.connection.options.type === 'postgres' ? 'ILIKE' : 'LIKE';
+        let clause: string;
+        let params: Record<string, string>;
         switch (args.operator) {
             case 'contains':
-                return qb.andWhere(`${translationAlias}.name ${LIKE} :${termName}`, {
+                clause = `${translationAlias}.name ${LIKE} :${termName}`;
+                params = {
                     [termName]: `%${args.term}%`,
-                });
+                };
+                break;
             case 'doesNotContain':
-                return qb.andWhere(`${translationAlias}.name NOT ${LIKE} :${termName}`, {
+                clause = `${translationAlias}.name NOT ${LIKE} :${termName}`;
+                params = {
                     [termName]: `%${args.term}%`,
-                });
+                };
+                break;
             case 'startsWith':
-                return qb.andWhere(`${translationAlias}.name ${LIKE} :${termName}`, {
+                clause = `${translationAlias}.name ${LIKE} :${termName}`;
+                params = {
                     [termName]: `${args.term}%`,
-                });
+                };
+                break;
             case 'endsWith':
-                return qb.andWhere(`${translationAlias}.name ${LIKE} :${termName}`, {
+                clause = `${translationAlias}.name ${LIKE} :${termName}`;
+                params = {
                     [termName]: `%${args.term}`,
-                });
+                };
+                break;
             default:
                 throw new UserInputError(`${args.operator} is not a valid operator`);
         }
+        if (args.combineWithAnd === false) {
+            return qb.orWhere(clause, params);
+        } else {
+            return qb.andWhere(clause, params);
+        }
     },
 });
 
@@ -141,15 +196,58 @@ export const variantIdCollectionFilter = new CollectionFilter({
         variantIds: {
             type: 'ID',
             list: true,
+            label: [{ languageCode: LanguageCode.en, value: 'Product variants' }],
             ui: {
-                component: 'product-selector-form-input',
+                component: 'product-multi-form-input',
+                selectionMode: 'variant',
             },
         },
+        combineWithAnd: combineWithAndArg,
     },
     code: 'variant-id-filter',
     description: [{ languageCode: LanguageCode.en, value: 'Manually select product variants' }],
     apply: (qb, args) => {
-        return qb.andWhere('productVariant.id IN (:...variantIds)', { variantIds: args.variantIds });
+        if (args.variantIds.length === 0) {
+            return qb;
+        }
+        const variantIdsKey = randomSuffix(`variantIds`);
+        const clause = `productVariant.id IN (:...${variantIdsKey})`;
+        const params = { [variantIdsKey]: args.variantIds };
+        if (args.combineWithAnd === false) {
+            return qb.orWhere(clause, params);
+        } else {
+            return qb.andWhere(clause, params);
+        }
+    },
+});
+
+export const productIdCollectionFilter = new CollectionFilter({
+    args: {
+        productIds: {
+            type: 'ID',
+            list: true,
+            label: [{ languageCode: LanguageCode.en, value: 'Products' }],
+            ui: {
+                component: 'product-multi-form-input',
+                selectionMode: 'product',
+            },
+        },
+        combineWithAnd: combineWithAndArg,
+    },
+    code: 'product-id-filter',
+    description: [{ languageCode: LanguageCode.en, value: 'Manually select products' }],
+    apply: (qb, args) => {
+        if (args.productIds.length === 0) {
+            return qb;
+        }
+        const productIdsKey = randomSuffix(`productIds`);
+        const clause = `productVariant.productId IN (:...${productIdsKey})`;
+        const params = { [productIdsKey]: args.productIds };
+        if (args.combineWithAnd === false) {
+            return qb.orWhere(clause, params);
+        } else {
+            return qb.andWhere(clause, params);
+        }
     },
 });
 
@@ -157,4 +255,5 @@ export const defaultCollectionFilters = [
     facetValueCollectionFilter,
     variantNameCollectionFilter,
     variantIdCollectionFilter,
+    productIdCollectionFilter,
 ];