From 959a6f2894fa51feb595628ac08002218d7fd76e Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:34:24 +0700 Subject: [PATCH 01/12] Show error message when adding wrong shipping method --- .../saveShippingMethod.js | 7 +++- .../frontStore/checkout/ShippingMethods.jsx | 33 +++++++++++++------ .../services/cart/registerCartBaseFields.js | 1 + 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/evershop/src/modules/checkout/api/addCartShippingMethod/saveShippingMethod.js b/packages/evershop/src/modules/checkout/api/addCartShippingMethod/saveShippingMethod.js index 2b2360f6a..b6e42b754 100644 --- a/packages/evershop/src/modules/checkout/api/addCartShippingMethod/saveShippingMethod.js +++ b/packages/evershop/src/modules/checkout/api/addCartShippingMethod/saveShippingMethod.js @@ -6,6 +6,10 @@ const { } = require('@evershop/evershop/src/lib/util/httpStatus'); const { getCartByUUID } = require('../../services/getCartByUUID'); const { saveCart } = require('../../services/saveCart'); +const { error } = require('@evershop/evershop/src/lib/log/logger'); +const { + translate +} = require('@evershop/evershop/src/lib/locale/translate/translate'); module.exports = async (request, response, delegate, next) => { try { @@ -37,10 +41,11 @@ module.exports = async (request, response, delegate, next) => { next(); } } catch (e) { + error(e); response.status(INTERNAL_SERVER_ERROR); response.json({ error: { - message: e.message, + message: translate('Failed to set shipping method'), status: INTERNAL_SERVER_ERROR } }); diff --git a/packages/evershop/src/modules/checkout/pages/frontStore/checkout/ShippingMethods.jsx b/packages/evershop/src/modules/checkout/pages/frontStore/checkout/ShippingMethods.jsx index 211035155..ac6689b69 100644 --- a/packages/evershop/src/modules/checkout/pages/frontStore/checkout/ShippingMethods.jsx +++ b/packages/evershop/src/modules/checkout/pages/frontStore/checkout/ShippingMethods.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import axios from 'axios'; +import { toast } from 'react-toastify'; import { useClient } from 'urql'; import { useFormContext } from '@components/common/form/Form'; import { Field } from '@components/common/form/Field'; @@ -105,17 +106,29 @@ export default function ShippingMethods({ async function saveMethods() { // Get the selected method const selectedMethod = methods.find((m) => m.selected === true); - const response = await axios.post(addShippingMethodApi, { - method_code: selectedMethod.code, - method_name: selectedMethod.name - }); - if (!response.data.error) { - const result = await client.query(QUERY, { cartId }).toPromise(); - const address = result.data.cart.shippingAddress; - completeStep( - 'shipment', - `${address.address1}, ${address.city}, ${address.country.name}` + try { + const response = await axios.post( + addShippingMethodApi, + { + method_code: selectedMethod.code, + method_name: selectedMethod.name + }, + { + validateStatus: () => true + } ); + if (!response.data.error) { + const result = await client.query(QUERY, { cartId }).toPromise(); + const address = result.data.cart.shippingAddress; + completeStep( + 'shipment', + `${address.address1}, ${address.city}, ${address.country.name}` + ); + } else { + toast.error(response.data.error.message); + } + } catch (error) { + toast.error(error.message); } } if (formContext.state === 'submitSuccess') { diff --git a/packages/evershop/src/modules/checkout/services/cart/registerCartBaseFields.js b/packages/evershop/src/modules/checkout/services/cart/registerCartBaseFields.js index 0effd64e3..698b154b9 100644 --- a/packages/evershop/src/modules/checkout/services/cart/registerCartBaseFields.js +++ b/packages/evershop/src/modules/checkout/services/cart/registerCartBaseFields.js @@ -262,6 +262,7 @@ module.exports.registerCartBaseFields = function registerCartBaseFields() { ); shippingMethodQuery .where('uuid', '=', shippingMethod) + .and('is_enabled', '=', true) .and( 'shipping_zone_method.zone_id', '=', From b45cf1e5c2e01c7504361752a246fb44cef55395 Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:36:41 +0700 Subject: [PATCH 02/12] Show error message when adding wrong shipping method --- .../checkout/api/addCartShippingMethod/saveShippingMethod.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/evershop/src/modules/checkout/api/addCartShippingMethod/saveShippingMethod.js b/packages/evershop/src/modules/checkout/api/addCartShippingMethod/saveShippingMethod.js index b6e42b754..c276cdde6 100644 --- a/packages/evershop/src/modules/checkout/api/addCartShippingMethod/saveShippingMethod.js +++ b/packages/evershop/src/modules/checkout/api/addCartShippingMethod/saveShippingMethod.js @@ -4,12 +4,12 @@ const { INTERNAL_SERVER_ERROR, INVALID_PAYLOAD } = require('@evershop/evershop/src/lib/util/httpStatus'); -const { getCartByUUID } = require('../../services/getCartByUUID'); -const { saveCart } = require('../../services/saveCart'); const { error } = require('@evershop/evershop/src/lib/log/logger'); const { translate } = require('@evershop/evershop/src/lib/locale/translate/translate'); +const { getCartByUUID } = require('../../services/getCartByUUID'); +const { saveCart } = require('../../services/saveCart'); module.exports = async (request, response, delegate, next) => { try { From 1136d6cdfd74d14e87467f02640e59710eb062ae Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:33:34 +0700 Subject: [PATCH 03/12] Enable webpack source map --- packages/evershop/src/lib/webpack/dev/createConfigClient.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/evershop/src/lib/webpack/dev/createConfigClient.js b/packages/evershop/src/lib/webpack/dev/createConfigClient.js index 212d9fc11..8566aa160 100644 --- a/packages/evershop/src/lib/webpack/dev/createConfigClient.js +++ b/packages/evershop/src/lib/webpack/dev/createConfigClient.js @@ -97,5 +97,7 @@ module.exports.createConfigClient = function createConfigClient(route) { poll: 1000 }; + // Enable source maps + config.devtool = 'eval-cheap-module-source-map'; return config; }; From d063c67a818640e31fa14e3916cd2b5f9bba8b64 Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Fri, 12 Apr 2024 23:04:49 +0700 Subject: [PATCH 04/12] Support price and weight based shipping cost --- .../shippingSetting/Method.jsx | 30 +++++- .../shippingSetting/MethodForm.jsx | 46 +++++++- .../shippingSetting/Methods.jsx | 4 +- .../shippingSetting/PriceBasedPrice.jsx | 101 ++++++++++++++++++ .../shippingSetting/WeightBasedPrice.jsx | 101 ++++++++++++++++++ .../shippingSetting/Zone.jsx | 4 +- .../shippingSetting/ZoneForm.jsx | 0 .../shippingSetting/Zones.jsx | 2 +- .../src/components/common/form/Field.jsx | 2 +- .../src/components/common/form/validator.js | 8 +- .../api/getShippingMethods/sendMethods.js | 37 +++++++ .../payloadSchema.json | 44 +++++++- .../updateShippingZoneMethod.js | 20 +++- .../types/ShippingZone/ShippingZone.graphql | 18 ++++ .../ShippingZone/ShippingZone.resolvers.js | 6 ++ .../checkout/migration/Version-1.0.5.js | 28 +++++ .../pages/admin/all/ShippingSettingMenu.jsx | 0 .../admin/shippingSetting/ShippingSetting.jsx | 84 ++++++++++----- .../pages/admin/shippingSetting/index.js | 0 .../pages/admin/shippingSetting/route.json | 0 .../modules/checkout/services/cart/Cart.js | 2 +- .../checkout/services/cart/DataObject.js | 4 +- .../services/cart/registerCartBaseFields.js | 31 ++++++ 23 files changed, 525 insertions(+), 47 deletions(-) rename packages/evershop/src/components/admin/{oms => checkout}/shippingSetting/Method.jsx (69%) rename packages/evershop/src/components/admin/{oms => checkout}/shippingSetting/MethodForm.jsx (83%) rename packages/evershop/src/components/admin/{oms => checkout}/shippingSetting/Methods.jsx (92%) create mode 100644 packages/evershop/src/components/admin/checkout/shippingSetting/PriceBasedPrice.jsx create mode 100644 packages/evershop/src/components/admin/checkout/shippingSetting/WeightBasedPrice.jsx rename packages/evershop/src/components/admin/{oms => checkout}/shippingSetting/Zone.jsx (96%) rename packages/evershop/src/components/admin/{oms => checkout}/shippingSetting/ZoneForm.jsx (100%) rename packages/evershop/src/components/admin/{oms => checkout}/shippingSetting/Zones.jsx (94%) create mode 100644 packages/evershop/src/modules/checkout/migration/Version-1.0.5.js rename packages/evershop/src/modules/{oms => checkout}/pages/admin/all/ShippingSettingMenu.jsx (100%) rename packages/evershop/src/modules/{oms => checkout}/pages/admin/shippingSetting/ShippingSetting.jsx (63%) rename packages/evershop/src/modules/{oms => checkout}/pages/admin/shippingSetting/index.js (100%) rename packages/evershop/src/modules/{oms => checkout}/pages/admin/shippingSetting/route.json (100%) diff --git a/packages/evershop/src/components/admin/oms/shippingSetting/Method.jsx b/packages/evershop/src/components/admin/checkout/shippingSetting/Method.jsx similarity index 69% rename from packages/evershop/src/components/admin/oms/shippingSetting/Method.jsx rename to packages/evershop/src/components/admin/checkout/shippingSetting/Method.jsx index 20096569b..57666b104 100644 --- a/packages/evershop/src/components/admin/oms/shippingSetting/Method.jsx +++ b/packages/evershop/src/components/admin/checkout/shippingSetting/Method.jsx @@ -1,7 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import CogIcon from '@heroicons/react/outline/CogIcon'; import { useModal } from '@components/common/modal/useModal'; -import MethodForm from './MethodForm'; +import MethodForm from '@components/admin/checkout/shippingSetting/MethodForm'; function Method({ method, getZones }) { const modal = useModal(); @@ -12,7 +13,20 @@ function Method({ method, getZones }) { {method.isEnabled ? 'Enabled' : 'Disabled'} - {method.cost?.text} + + {method.cost?.text || ( + { + e.preventDefault(); + modal.openModal(); + }} + > + + + )} + {method.conditionType ? `${method.min || 0} <= ${method.conditionType} <= ${ @@ -62,6 +76,18 @@ Method.propTypes = { cost: PropTypes.shape({ text: PropTypes.string.isRequired }), + priceBasedCost: PropTypes.arrayOf( + PropTypes.shape({ + minPrice: PropTypes.number.isRequired, + cost: PropTypes.number.isRequired + }) + ), + weightBasedCost: PropTypes.arrayOf( + PropTypes.shape({ + minWeight: PropTypes.number.isRequired, + cost: PropTypes.number.isRequired + }) + ), conditionType: PropTypes.string.isRequired, min: PropTypes.number.isRequired, max: PropTypes.number.isRequired, diff --git a/packages/evershop/src/components/admin/oms/shippingSetting/MethodForm.jsx b/packages/evershop/src/components/admin/checkout/shippingSetting/MethodForm.jsx similarity index 83% rename from packages/evershop/src/components/admin/oms/shippingSetting/MethodForm.jsx rename to packages/evershop/src/components/admin/checkout/shippingSetting/MethodForm.jsx index 6be1b6b07..9ef98464c 100644 --- a/packages/evershop/src/components/admin/oms/shippingSetting/MethodForm.jsx +++ b/packages/evershop/src/components/admin/checkout/shippingSetting/MethodForm.jsx @@ -10,6 +10,8 @@ import CreatableSelect from 'react-select/creatable'; import Spinner from '@components/common/Spinner'; import { useQuery } from 'urql'; import { toast } from 'react-toastify'; +import PriceBasedPrice from '@components/admin/checkout/shippingSetting/PriceBasedPrice'; +import WeightBasedPrice from '@components/admin/checkout/shippingSetting/WeightBasedPrice'; const MethodsQuery = ` query Methods { @@ -83,9 +85,18 @@ Condition.defaultProps = { }; function MethodForm({ saveMethodApi, closeModal, getZones, method }) { - const [type, setType] = React.useState( - method?.calculateApi ? 'api' : 'flat_rate' - ); + const [type, setType] = React.useState(() => { + if (method?.calculateApi) { + return 'api'; + } + if (method?.priceBasedCost) { + return 'price_based_rate'; + } + if (method?.weightBasedCost) { + return 'weight_based_rate'; + } + return 'flat_rate'; + }); const [isLoading, setIsLoading] = React.useState(false); const [shippingMethod, setMethod] = React.useState( method @@ -138,6 +149,7 @@ function MethodForm({ saveMethodApi, closeModal, getZones, method }) { if (!response.error) { await getZones({ requestPolicy: 'network-only' }); closeModal(); + toast.success('Shipping method saved successfully'); } else { toast.error(response.error.message); } @@ -166,6 +178,8 @@ function MethodForm({ saveMethodApi, closeModal, getZones, method }) { name="calculation_type" options={[ { text: 'Flat rate', value: 'flat_rate' }, + { text: 'Price based rate', value: 'price_based_rate' }, + { text: 'Weight based rate', value: 'weight_based_rate' }, { text: 'API calculate', value: 'api' } ]} defaultValue={method?.calculateApi ? 'api' : 'flat_rate'} @@ -183,6 +197,12 @@ function MethodForm({ saveMethodApi, closeModal, getZones, method }) { value={method?.cost?.value} /> )} + {type === 'price_based_rate' && ( + + )} + {type === 'weight_based_rate' && ( + + )} {type === 'api' && ( ({ + ...line, + key: Math.random().toString(36).substring(7) + })) + ); + return ( +
+ + + + + + + + + + {rows.map((row, index) => ( + // Create a random key for each row + + + + + + ))} + + + + + + +
Min PriceShipping CostAction
+ + + + + { + setRows(rows.filter((r) => r.key !== row.key)); + }} + className="text-critical" + > + Delete + +
+ { + setRows([ + ...rows, + { + min_price: '', + shipping_cost: '', + key: Math.random().toString(36).substring(7) + } + ]); + }} + > + + Add Line + +
+
+ ); +} + +PriceBasedPrice.propTypes = { + lines: PropTypes.arrayOf( + PropTypes.shape({ + minPrice: PropTypes.shape({ + value: PropTypes.number.isRequired + }), + cost: PropTypes.shape({ + value: PropTypes.number.isRequired + }) + }) + ) +}; + +PriceBasedPrice.defaultProps = { + lines: [] +}; diff --git a/packages/evershop/src/components/admin/checkout/shippingSetting/WeightBasedPrice.jsx b/packages/evershop/src/components/admin/checkout/shippingSetting/WeightBasedPrice.jsx new file mode 100644 index 000000000..08bea86e7 --- /dev/null +++ b/packages/evershop/src/components/admin/checkout/shippingSetting/WeightBasedPrice.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field } from '@components/common/form/Field'; + +export default function WeightBasedPrice({ lines }) { + // This is a table with 3 columns: Min Price, Shipping Cost, and Action + const [rows, setRows] = React.useState( + lines.map((line) => ({ + ...line, + key: Math.random().toString(36).substring(7) + })) + ); + return ( +
+ + + + + + + + + + {rows.map((row, index) => ( + // Create a random key for each row + + + + + + ))} + + + + + + +
Min WeightShipping CostAction
+ + + + + { + setRows(rows.filter((r) => r.key !== row.key)); + }} + className="text-critical" + > + Delete + +
+ { + setRows([ + ...rows, + { + min_price: '', + shipping_cost: '', + key: Math.random().toString(36).substring(7) + } + ]); + }} + > + + Add Line + +
+
+ ); +} + +WeightBasedPrice.propTypes = { + lines: PropTypes.arrayOf( + PropTypes.shape({ + minWeight: PropTypes.shape({ + value: PropTypes.number.isRequired + }), + cost: PropTypes.shape({ + value: PropTypes.number.isRequired + }) + }) + ) +}; + +WeightBasedPrice.defaultProps = { + lines: [] +}; diff --git a/packages/evershop/src/components/admin/oms/shippingSetting/Zone.jsx b/packages/evershop/src/components/admin/checkout/shippingSetting/Zone.jsx similarity index 96% rename from packages/evershop/src/components/admin/oms/shippingSetting/Zone.jsx rename to packages/evershop/src/components/admin/checkout/shippingSetting/Zone.jsx index dcf7dedf7..26806b921 100644 --- a/packages/evershop/src/components/admin/oms/shippingSetting/Zone.jsx +++ b/packages/evershop/src/components/admin/checkout/shippingSetting/Zone.jsx @@ -5,8 +5,8 @@ import { toast } from 'react-toastify'; import { Card } from '@components/admin/cms/Card'; import MapIcon from '@heroicons/react/solid/esm/LocationMarkerIcon'; import { useModal } from '@components/common/modal/useModal'; -import ZoneForm from './ZoneForm'; -import { Methods } from './Methods'; +import ZoneForm from '@components/admin/checkout/shippingSetting/ZoneForm'; +import { Methods } from '@components/admin/checkout/shippingSetting/Methods'; function Zone({ zone, countries, getZones }) { const modal = useModal(); diff --git a/packages/evershop/src/components/admin/oms/shippingSetting/ZoneForm.jsx b/packages/evershop/src/components/admin/checkout/shippingSetting/ZoneForm.jsx similarity index 100% rename from packages/evershop/src/components/admin/oms/shippingSetting/ZoneForm.jsx rename to packages/evershop/src/components/admin/checkout/shippingSetting/ZoneForm.jsx diff --git a/packages/evershop/src/components/admin/oms/shippingSetting/Zones.jsx b/packages/evershop/src/components/admin/checkout/shippingSetting/Zones.jsx similarity index 94% rename from packages/evershop/src/components/admin/oms/shippingSetting/Zones.jsx rename to packages/evershop/src/components/admin/checkout/shippingSetting/Zones.jsx index 9740aab22..34deb623a 100644 --- a/packages/evershop/src/components/admin/oms/shippingSetting/Zones.jsx +++ b/packages/evershop/src/components/admin/checkout/shippingSetting/Zones.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Zone from './Zone'; +import Zone from '@components/admin/checkout/shippingSetting/Zone'; export function Zones({ countries, getZones, zones }) { return ( diff --git a/packages/evershop/src/components/common/form/Field.jsx b/packages/evershop/src/components/common/form/Field.jsx index 33bb3dfa3..af401f236 100644 --- a/packages/evershop/src/components/common/form/Field.jsx +++ b/packages/evershop/src/components/common/form/Field.jsx @@ -47,7 +47,7 @@ export function Field(props) { return () => { context.removeField(name); }; - }, []); + }, [name]); React.useEffect(() => { setFieldValue(value); diff --git a/packages/evershop/src/components/common/form/validator.js b/packages/evershop/src/components/common/form/validator.js index 1c85de12b..72d39c2da 100644 --- a/packages/evershop/src/components/common/form/validator.js +++ b/packages/evershop/src/components/common/form/validator.js @@ -13,8 +13,12 @@ const rules = { }, number: { handler(value) { - if (value === null || value === undefined || value === '') return true; - return /^-?[0-9]+$/.test(value); + if (value === null || value === undefined || value === '') { + return true; + } + + // Allowing integer and float + return !Number.isNaN(value); }, errorMessage: 'Invalid number' }, diff --git a/packages/evershop/src/modules/checkout/api/getShippingMethods/sendMethods.js b/packages/evershop/src/modules/checkout/api/getShippingMethods/sendMethods.js index 371cb1e35..f948ac550 100644 --- a/packages/evershop/src/modules/checkout/api/getShippingMethods/sendMethods.js +++ b/packages/evershop/src/modules/checkout/api/getShippingMethods/sendMethods.js @@ -131,6 +131,43 @@ module.exports = async (request, response, delegate, next) => { ...method, cost: toPrice(jsonResponse.data.data.cost, true) }; + } else if (method.weight_based_cost) { + const totalWeight = cart.total_weight; + const weightBasedCost = method.weight_based_cost + .map(({ min_weight, cost }) => ({ + min_weight: parseFloat(min_weight), + cost: toPrice(cost) + })) + .sort((a, b) => a.min_weight - b.min_weight); + + let cost = 0; + for (let i = 0; i < weightBasedCost.length; i += 1) { + if (totalWeight >= weightBasedCost[i].min_weight) { + cost = weightBasedCost[i].cost; + } + } + return { + ...method, + cost: toPrice(cost, true) + }; + } else if (method.price_based_cost) { + const subTotal = toPrice(cart.sub_total); + const priceBasedCost = method.price_based_cost + .map(({ min_price, cost }) => ({ + min_price: toPrice(min_price), + cost: toPrice(cost) + })) + .sort((a, b) => a.min_price - b.min_price); + let cost = 0; + for (let i = 0; i < priceBasedCost.length; i += 1) { + if (subTotal >= priceBasedCost[i].min_price) { + cost = priceBasedCost[i].cost; + } + } + return { + ...method, + cost: toPrice(cost, true) + }; } else { return { ...method, diff --git a/packages/evershop/src/modules/checkout/api/updateShippingZoneMethod/payloadSchema.json b/packages/evershop/src/modules/checkout/api/updateShippingZoneMethod/payloadSchema.json index dd3ed87c6..ee72c05bb 100644 --- a/packages/evershop/src/modules/checkout/api/updateShippingZoneMethod/payloadSchema.json +++ b/packages/evershop/src/modules/checkout/api/updateShippingZoneMethod/payloadSchema.json @@ -11,7 +11,49 @@ }, "calculation_type": { "type": "string", - "enum": ["flat_rate", "api"] + "enum": ["flat_rate", "price_based_rate", "weight_based_rate", "api"] + }, + "price_based_cost": { + "type": "array", + "items": { + "type": "object", + "properties": { + "min_price": { + "type": ["string", "number"], + "pattern": "^\\d+(\\.\\d{1,2})?$" + }, + "cost": { + "type": ["string", "number"], + "pattern": "^\\d+(\\.\\d{1,2})?$" + } + }, + "additionalProperties": false, + "required": [ + "min_price", + "cost" + ] + } + }, + "weight_based_cost": { + "type": "array", + "items": { + "type": "object", + "properties": { + "min_weight": { + "type": ["string", "number"], + "pattern": "^\\d+(\\.\\d{1,2})?$" + }, + "cost": { + "type": ["string", "number"], + "pattern": "^\\d+(\\.\\d{1,2})?$" + } + }, + "additionalProperties": false, + "required": [ + "min_weight", + "cost" + ] + } }, "condition_type": { "type": "string", diff --git a/packages/evershop/src/modules/checkout/api/updateShippingZoneMethod/updateShippingZoneMethod.js b/packages/evershop/src/modules/checkout/api/updateShippingZoneMethod/updateShippingZoneMethod.js index 9f2a48a53..0a6767c58 100644 --- a/packages/evershop/src/modules/checkout/api/updateShippingZoneMethod/updateShippingZoneMethod.js +++ b/packages/evershop/src/modules/checkout/api/updateShippingZoneMethod/updateShippingZoneMethod.js @@ -20,7 +20,15 @@ module.exports = async (request, response, deledate, next) => { const { method_id, zone_id } = request.params; const connection = await getConnection(); await startTransaction(connection); - let { cost, condition_type, calculate_api, min, max } = request.body; + let { + cost, + condition_type, + calculate_api, + price_based_cost, + weight_based_cost, + min, + max + } = request.body; const { is_enabled, calculation_type } = request.body; try { // Load the shipping zone @@ -65,9 +73,13 @@ module.exports = async (request, response, deledate, next) => { } if (calculation_type === 'api') { - cost = null; + cost = weight_based_cost = price_based_cost = null; + } else if (calculation_type === 'price_based_rate') { + calculate_api = cost = weight_based_cost = null; + } else if (calculation_type === 'weight_based_rate') { + calculate_api = cost = price_based_cost = null; } else { - calculate_api = null; + calculate_api = weight_based_cost = price_based_cost = null; } if (condition_type === 'none') { condition_type = null; @@ -80,6 +92,8 @@ module.exports = async (request, response, deledate, next) => { is_enabled, calculate_api, condition_type, + price_based_cost, + weight_based_cost, max, min }) diff --git a/packages/evershop/src/modules/checkout/graphql/types/ShippingZone/ShippingZone.graphql b/packages/evershop/src/modules/checkout/graphql/types/ShippingZone/ShippingZone.graphql index dfd7b927b..c8d0ddc39 100644 --- a/packages/evershop/src/modules/checkout/graphql/types/ShippingZone/ShippingZone.graphql +++ b/packages/evershop/src/modules/checkout/graphql/types/ShippingZone/ShippingZone.graphql @@ -1,3 +1,19 @@ +""" +Represents a price based cost +""" +type PriceBasedCostItem { + minPrice: Price! + cost: Price! +} + +""" +Represents a weight based cost +""" +type WeightBasedCostItem { + minWeight: Price! + cost: Price! +} + """ Represents a shipping method. """ @@ -7,6 +23,8 @@ type ShippingMethodByZone { uuid: String! name: String! cost: Price + priceBasedCost: [PriceBasedCostItem] + weightBasedCost: [WeightBasedCostItem] isEnabled: Boolean! calculateApi: String conditionType: String diff --git a/packages/evershop/src/modules/checkout/graphql/types/ShippingZone/ShippingZone.resolvers.js b/packages/evershop/src/modules/checkout/graphql/types/ShippingZone/ShippingZone.resolvers.js index 6ce38e780..875109967 100644 --- a/packages/evershop/src/modules/checkout/graphql/types/ShippingZone/ShippingZone.resolvers.js +++ b/packages/evershop/src/modules/checkout/graphql/types/ShippingZone/ShippingZone.resolvers.js @@ -74,5 +74,11 @@ module.exports = { method_id: uuid }); } + }, + WeightBasedCostItem: { + minWeight: ({ min_weight }) => min_weight + }, + PriceBasedCostItem: { + minPrice: ({ min_price }) => min_price } }; diff --git a/packages/evershop/src/modules/checkout/migration/Version-1.0.5.js b/packages/evershop/src/modules/checkout/migration/Version-1.0.5.js new file mode 100644 index 000000000..bbff86a64 --- /dev/null +++ b/packages/evershop/src/modules/checkout/migration/Version-1.0.5.js @@ -0,0 +1,28 @@ +const { execute } = require('@evershop/postgres-query-builder'); + +// eslint-disable-next-line no-multi-assign +module.exports = exports = async (connection) => { + // Add a column 'price_based_cost' to the method table if it does not exist + await execute( + connection, + `ALTER TABLE "shipping_zone_method" ADD COLUMN "price_based_cost" jsonb` + ); + + // Add a column 'price_based_cost' to the method table if it does not exist + await execute( + connection, + `ALTER TABLE "shipping_zone_method" ADD COLUMN "weight_based_cost" jsonb` + ); + + // Delete the constraint 'CONDITION_TYPE_MUST_BE_PRICE_OR_WEIGHT' from the method table + await execute( + connection, + `ALTER TABLE "shipping_zone_method" DROP CONSTRAINT "CONDITION_TYPE_MUST_BE_PRICE_OR_WEIGHT"` + ); + + // Delete the constraint 'CONDITION_TYPE_MUST_BE_PRICE_OR_WEIGHT' from the method table + await execute( + connection, + `ALTER TABLE "shipping_zone_method" DROP CONSTRAINT "CALCULATE API MUST BE PROVIDE IF COST IS NULL"` + ); +}; diff --git a/packages/evershop/src/modules/oms/pages/admin/all/ShippingSettingMenu.jsx b/packages/evershop/src/modules/checkout/pages/admin/all/ShippingSettingMenu.jsx similarity index 100% rename from packages/evershop/src/modules/oms/pages/admin/all/ShippingSettingMenu.jsx rename to packages/evershop/src/modules/checkout/pages/admin/all/ShippingSettingMenu.jsx diff --git a/packages/evershop/src/modules/oms/pages/admin/shippingSetting/ShippingSetting.jsx b/packages/evershop/src/modules/checkout/pages/admin/shippingSetting/ShippingSetting.jsx similarity index 63% rename from packages/evershop/src/modules/oms/pages/admin/shippingSetting/ShippingSetting.jsx rename to packages/evershop/src/modules/checkout/pages/admin/shippingSetting/ShippingSetting.jsx index 2d6fe1e08..9879066e3 100644 --- a/packages/evershop/src/modules/oms/pages/admin/shippingSetting/ShippingSetting.jsx +++ b/packages/evershop/src/modules/checkout/pages/admin/shippingSetting/ShippingSetting.jsx @@ -5,9 +5,9 @@ import { Card } from '@components/admin/cms/Card'; import SettingMenu from '@components/admin/setting/SettingMenu'; import Button from '@components/common/form/Button'; import { useModal } from '@components/common/modal/useModal'; -import ZoneForm from '@components/admin/oms/shippingSetting/ZoneForm'; +import ZoneForm from '@components/admin/checkout/shippingSetting/ZoneForm'; import Spinner from '@components/common/Spinner'; -import { Zones } from '@components/admin/oms/shippingSetting/Zones'; +import { Zones } from '@components/admin/checkout/shippingSetting/Zones'; const CountriesQuery = ` query Country($countries: [String]) { @@ -43,6 +43,26 @@ const ZonesQuery = ` text value } + priceBasedCost { + minPrice { + value + text + } + cost { + value + text + } + } + weightBasedCost { + minWeight { + value + text + } + cost { + value + text + } + } isEnabled conditionType calculateApi @@ -67,14 +87,6 @@ export default function ShippingSetting({ createShippingZoneApi }) { query: ZonesQuery }); - if (countriesQueryData.fetching || zonesQueryData.fetching) { - return ( -
- -
- ); - } - return (
@@ -82,27 +94,45 @@ export default function ShippingSetting({ createShippingZoneApi }) {
- + {countriesQueryData.fetching || zonesQueryData.fetching ? ( -
- Choose where you ship and how much you charge for shipping. +
+
- - -
-
-
- + )} + +
+
+
+ + )}
{modal.state.showing && ( diff --git a/packages/evershop/src/modules/oms/pages/admin/shippingSetting/index.js b/packages/evershop/src/modules/checkout/pages/admin/shippingSetting/index.js similarity index 100% rename from packages/evershop/src/modules/oms/pages/admin/shippingSetting/index.js rename to packages/evershop/src/modules/checkout/pages/admin/shippingSetting/index.js diff --git a/packages/evershop/src/modules/oms/pages/admin/shippingSetting/route.json b/packages/evershop/src/modules/checkout/pages/admin/shippingSetting/route.json similarity index 100% rename from packages/evershop/src/modules/oms/pages/admin/shippingSetting/route.json rename to packages/evershop/src/modules/checkout/pages/admin/shippingSetting/route.json diff --git a/packages/evershop/src/modules/checkout/services/cart/Cart.js b/packages/evershop/src/modules/checkout/services/cart/Cart.js index 651b48a24..7fe363a22 100644 --- a/packages/evershop/src/modules/checkout/services/cart/Cart.js +++ b/packages/evershop/src/modules/checkout/services/cart/Cart.js @@ -87,7 +87,7 @@ class Cart extends DataObject { if (!duplicateItem) { items = items.concat(item); } - await this.setData('items', items); + await this.setData('items', items, true); return duplicateItem || item; } } diff --git a/packages/evershop/src/modules/checkout/services/cart/DataObject.js b/packages/evershop/src/modules/checkout/services/cart/DataObject.js index 34261ce45..1abe71224 100644 --- a/packages/evershop/src/modules/checkout/services/cart/DataObject.js +++ b/packages/evershop/src/modules/checkout/services/cart/DataObject.js @@ -80,7 +80,7 @@ module.exports.DataObject = class DataObject { } } - async setData(key, value) { + async setData(key, value, force = false) { this.#triggeredField = key; this.#requestedValue = value; if (this.isBuilding === true) { @@ -91,7 +91,7 @@ module.exports.DataObject = class DataObject { throw new Error(`Field ${key} not existed`); } - if (isEqualWith(this.#data[key], value)) { + if (isEqualWith(this.#data[key], value) && !force) { return value; } diff --git a/packages/evershop/src/modules/checkout/services/cart/registerCartBaseFields.js b/packages/evershop/src/modules/checkout/services/cart/registerCartBaseFields.js index 698b154b9..063067674 100644 --- a/packages/evershop/src/modules/checkout/services/cart/registerCartBaseFields.js +++ b/packages/evershop/src/modules/checkout/services/cart/registerCartBaseFields.js @@ -384,6 +384,37 @@ module.exports.registerCartBaseFields = function registerCartBaseFields() { this.setError('shipping_fee_excl_tax', response.data.message); return 0; } + } else if (shippingMethod.weight_based_cost) { + const totalWeight = this.getData('total_weight'); + const weightBasedCost = shippingMethod.weight_based_cost + .map(({ min_weight, cost }) => ({ + min_weight: parseFloat(min_weight), + cost: toPrice(cost) + })) + .sort((a, b) => a.min_weight - b.min_weight); + + let cost = 0; + for (let i = 0; i < weightBasedCost.length; i += 1) { + if (totalWeight >= weightBasedCost[i].min_weight) { + cost = weightBasedCost[i].cost; + } + } + return toPrice(cost); + } else if (shippingMethod.price_based_cost) { + const subTotal = this.getData('sub_total'); + const priceBasedCost = shippingMethod.price_based_cost + .map(({ min_price, cost }) => ({ + min_price: toPrice(min_price), + cost: toPrice(cost) + })) + .sort((a, b) => a.min_price - b.min_price); + let cost = 0; + for (let i = 0; i < priceBasedCost.length; i += 1) { + if (subTotal >= priceBasedCost[i].min_price) { + cost = priceBasedCost[i].cost; + } + } + return toPrice(cost); } else { this.setError( 'shipping_fee_excl_tax', From 633829aac85c1968c4614d0826463b3694a9f889 Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:43:06 +0700 Subject: [PATCH 05/12] Fix can not update variant options --- .../admin/catalog/productEdit/variants/VariantModal.jsx | 9 +++++++-- .../admin/catalog/productEdit/variants/Variants.jsx | 2 +- .../modules/catalog/services/product/updateProduct.js | 6 ++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/evershop/src/components/admin/catalog/productEdit/variants/VariantModal.jsx b/packages/evershop/src/components/admin/catalog/productEdit/variants/VariantModal.jsx index 29ff36bea..de16eab3f 100644 --- a/packages/evershop/src/components/admin/catalog/productEdit/variants/VariantModal.jsx +++ b/packages/evershop/src/components/admin/catalog/productEdit/variants/VariantModal.jsx @@ -28,13 +28,18 @@ export function VariantModal({
- {variantAttributes.map((a) => ( + {variantAttributes.map((a, index) => (
+ Date: Sun, 14 Apr 2024 09:40:31 +0700 Subject: [PATCH 06/12] Fix too many logger instance issue --- packages/evershop/src/lib/log/logger.js | 19 +++++++++++++------ packages/evershop/src/lib/util/registry.js | 6 ++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/evershop/src/lib/log/logger.js b/packages/evershop/src/lib/log/logger.js index b5cbcbc5e..35701700e 100644 --- a/packages/evershop/src/lib/log/logger.js +++ b/packages/evershop/src/lib/log/logger.js @@ -5,7 +5,7 @@ const { errors } = winston.format; const customColorize = require('./CustomColorize'); const isDevelopmentMode = require('../util/isDevelopmentMode'); const { getEnv } = require('../util/getEnv'); -const { getValueSync } = require('../util/registry'); +const { getValueSync, addProcessor } = require('../util/registry'); const isDebugging = isDevelopmentMode() || process.argv.includes('--debug'); const format = winston.format.combine( @@ -108,11 +108,7 @@ const DEFAULT_CONFIG = { }; function createLogger() { - const config = getValueSync('logger_configuration', DEFAULT_CONFIG, { - isDebugging - }); - - return getValueSync('logger', winston.createLogger(config), { isDebugging }); + return getValueSync('logger', null, { isDebugging }); } // Define logger function @@ -141,6 +137,17 @@ function success(message) { logger.info(message); } +addProcessor( + 'logger', + () => { + const config = getValueSync('logger_configuration', DEFAULT_CONFIG, { + isDebugging + }); + return winston.createLogger(config); + }, + 0 +); + // eslint-disable-next-line no-multi-assign module.exports = exports = { success, diff --git a/packages/evershop/src/lib/util/registry.js b/packages/evershop/src/lib/util/registry.js index a253f87a0..48daab546 100644 --- a/packages/evershop/src/lib/util/registry.js +++ b/packages/evershop/src/lib/util/registry.js @@ -10,7 +10,8 @@ class Registry { // If the initValue and the context are identical, return the cached value. Skip the processors if ( isEqual(initValue, this.values[name].initValue) && - isEqual(this.values[name].context, context) + isEqual(this.values[name].context, context) && + Object.prototype.hasOwnProperty.call(this.values[name], 'value') ) { return this.values[name].value; } @@ -65,7 +66,8 @@ class Registry { // If the initValue and the context are identical, return the cached value. Skip the processors if ( isEqual(initValue, this.values[name].initValue) && - isEqual(this.values[name].context, context) + isEqual(this.values[name].context, context) && + Object.prototype.hasOwnProperty.call(this.values[name], 'value') ) { return this.values[name].value; } From 785662c736b263fffd33d6d3714ff3e24e823485 Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:24:37 +0700 Subject: [PATCH 07/12] Fix logger errors --- .../evershop/src/lib/event/callSubscibers.js | 25 +++++++++++-------- packages/evershop/src/lib/middleware/async.js | 13 ++++------ packages/evershop/src/lib/middleware/sync.js | 11 ++++---- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/evershop/src/lib/event/callSubscibers.js b/packages/evershop/src/lib/event/callSubscibers.js index 16d7dd989..9f2515480 100644 --- a/packages/evershop/src/lib/event/callSubscibers.js +++ b/packages/evershop/src/lib/event/callSubscibers.js @@ -1,19 +1,22 @@ -const logger = require('../log/logger'); +const { error } = require('../log/logger'); module.exports.callSubscribers = async function callSubscribers( subscribers, eventData ) { - const promises = subscribers.map((subscriber) => new Promise((resolve) => { - setTimeout(async () => { - try { - await subscriber(eventData); - } catch (error) { - logger.log('error', `Error executing subscriber function: ${error}`); - } - resolve(); - }, 0); - })); + const promises = subscribers.map( + (subscriber) => + new Promise((resolve) => { + setTimeout(async () => { + try { + await subscriber(eventData); + } catch (e) { + error(e); + } + resolve(); + }, 0); + }) + ); await Promise.all(promises); }; diff --git a/packages/evershop/src/lib/middleware/async.js b/packages/evershop/src/lib/middleware/async.js index eb0babf33..61c3e45a6 100644 --- a/packages/evershop/src/lib/middleware/async.js +++ b/packages/evershop/src/lib/middleware/async.js @@ -1,4 +1,4 @@ -const logger = require('@evershop/evershop/src/lib/log/logger'); +const { error } = require('@evershop/evershop/src/lib/log/logger'); const { setDelegate } = require('./delegate'); // eslint-disable-next-line no-multi-assign @@ -21,10 +21,10 @@ exports.asyncMiddlewareWrapper = async function asyncMiddlewareWrapper( // If the middleware function has the next function as a parameter let delegate; if (middlewareFunc.length === 4) { - delegate = middlewareFunc(request, response, delegates, (error) => { + delegate = middlewareFunc(request, response, delegates, (err) => { const endTime = process.hrtime(startTime); debuging.time = endTime[1] / 1000000; - next(error); + next(err); }); } else { delegate = middlewareFunc(request, response, delegates); @@ -35,11 +35,8 @@ exports.asyncMiddlewareWrapper = async function asyncMiddlewareWrapper( await delegate; } catch (e) { // Log the error - logger.error(`Exception in middleware ${id}`, { - message: e.message, - stack: e.stack - }); - logger.error(`Exception in middleware ${id}`); + e.message = `Exception in middleware ${id}: ${e.message}`; + error(e); // Call error handler middleware if it is not called yet next(e); } diff --git a/packages/evershop/src/lib/middleware/sync.js b/packages/evershop/src/lib/middleware/sync.js index 011b3e740..e73b693e1 100644 --- a/packages/evershop/src/lib/middleware/sync.js +++ b/packages/evershop/src/lib/middleware/sync.js @@ -1,4 +1,4 @@ -const logger = require('@evershop/evershop/src/lib/log/logger'); +const { error } = require('@evershop/evershop/src/lib/log/logger'); const { setDelegate } = require('./delegate'); // eslint-disable-next-line no-multi-assign @@ -21,10 +21,10 @@ exports.syncMiddlewareWrapper = function syncMiddlewareWrapper( response.debugMiddlewares.push(debuging); // If the middleware function has the next function as a parameter if (middlewareFunc.length === 4) { - delegate = middlewareFunc(request, response, delegates, (error) => { + delegate = middlewareFunc(request, response, delegates, (err) => { const endTime = process.hrtime(startTime); debuging.time = endTime[1] / 1000000; - next(error); + next(err); }); } else { delegate = middlewareFunc(request, response, delegates); @@ -34,9 +34,8 @@ exports.syncMiddlewareWrapper = function syncMiddlewareWrapper( setDelegate(id, delegate, request); } catch (e) { // Log the error - logger.error(`Exception in middleware ${id}`); - logger.error(e); - + e.message = `Exception in middleware ${id}: ${e.message}`; + error(e); // Call error handler middleware if it is not called yet next(e); } From 788e7659690e5bc559a48d3beafc218d66e939a9 Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Fri, 19 Apr 2024 23:18:37 +0700 Subject: [PATCH 08/12] Fix adding new component does not trigger re-build --- packages/evershop/bin/dev/index.js | 3 +- .../evershop/bin/lib/watch/watchComponents.js | 42 +++++++------------ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/packages/evershop/bin/dev/index.js b/packages/evershop/bin/dev/index.js index 9733d9f74..1e2291533 100644 --- a/packages/evershop/bin/dev/index.js +++ b/packages/evershop/bin/dev/index.js @@ -2,7 +2,8 @@ process.env.ALLOW_CONFIG_MUTATIONS = true; require('dotenv').config(); const { start } = require('@evershop/evershop/bin/lib/startUp'); +const { watchComponents } = require('../lib/watch/watchComponents'); (async () => { - await start(); + await start(watchComponents); })(); diff --git a/packages/evershop/bin/lib/watch/watchComponents.js b/packages/evershop/bin/lib/watch/watchComponents.js index 1bb072625..4f847a1f4 100644 --- a/packages/evershop/bin/lib/watch/watchComponents.js +++ b/packages/evershop/bin/lib/watch/watchComponents.js @@ -1,34 +1,24 @@ const chokidar = require('chokidar'); -const { resolve, sep, normalize } = require('path'); +const touch = require('touch'); +const { resolve } = require('path'); const { CONSTANTS } = require('@evershop/evershop/src/lib/helpers'); -const { Componee } = require('@evershop/evershop/src/lib/componee/Componee'); -const { - createComponents -} = require('@evershop/evershop/bin/lib/createComponents'); -const { getRoutes } = require('@evershop/evershop/src/lib/router/Router'); -const { - isBuildRequired -} = require('@evershop/evershop/src/lib/webpack/isBuildRequired'); function watchComponents() { chokidar - .watch('**/**/pages/*.js', { - ignored: /node_modules[\\/]/, - ignoreInitial: true, - persistent: true - }) - .on('all', (event, path) => { - const modulePath = resolve(CONSTANTS.ROOTPATH, path).split( - normalize('/views/') - )[0]; - Componee.updateModuleComponents({ - name: modulePath.split(sep).reverse()[0], - path: modulePath - }); - const routes = getRoutes(); - createComponents( - routes.filter((r) => isBuildRequired(r)), - true + .watch( + ['./packages/**/*.jsx', './extensions/**/*.jsx', './themes/**/*.jsx'], + { + ignored: /node_modules[\\/]/, + ignoreInitial: true, + persistent: true + } + ) + .on('add', () => { + touch( + resolve( + CONSTANTS.MOLDULESPATH, + '../components/common/react/client/Index.jsx' + ) ); }); } From 7a7fb5b089663bf86e47d8825ca64bb7fafce2b3 Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:48:30 +0700 Subject: [PATCH 09/12] Improve the collection filtering --- .../components/common/form/fields/Input.jsx | 3 +- .../components/common/grid/headers/Basic.jsx | 55 --- .../components/common/grid/headers/Dummy.jsx | 2 +- .../common/grid/headers/Sortable.jsx | 148 ++++++++ .../components/common/grid/rows/StatusRow.jsx | 2 +- packages/evershop/src/lib/helpers.js | 4 +- .../src/lib/util/buildFilterFromUrl.js | 95 ++--- .../src/lib/util/defaultPaginationFilters.js | 75 ++++ .../src/lib/util/filterOperationMapp.js | 16 + packages/evershop/src/lib/util/registry.js | 38 +- .../evershop/src/modules/catalog/bootstrap.js | 63 ++++ .../types/Attribute/Attribute.admin.graphql | 4 +- .../Attribute/Attribute.admin.resolvers.js | 324 +++--------------- .../graphql/types/Category/Category.graphql | 17 +- .../types/Category/Category.resolvers.js | 2 +- .../types/Collection/Collection.resolvers.js | 4 +- .../types/Product/Product.resolvers.js | 2 +- .../types/Product/Variant/Variant.graphql | 4 +- .../Product/Variant/Variant.resolvers.js | 161 ++++----- .../attributeEdit+attributeNew/General.jsx | 22 +- .../pages/admin/attributeGrid/Grid.jsx | 90 +++-- .../pages/admin/attributeGrid/index.js | 2 +- .../catalog/pages/admin/categoryGrid/Grid.jsx | 64 +++- .../catalog/pages/admin/categoryGrid/index.js | 2 +- .../pages/admin/collectionEdit/Products.jsx | 10 +- .../pages/admin/collectionGrid/Grid.jsx | 51 ++- .../pages/admin/collectionGrid/index.js | 2 +- .../productEdit+productNew/Attributes.jsx | 52 +-- .../catalog/pages/admin/productGrid/Grid.jsx | 83 +++-- .../catalog/pages/admin/productGrid/index.js | 3 +- .../frontStore/homepage/FeaturedProducts.jsx | 2 +- .../catalog/services/AttributeCollection.js | 59 ++++ .../services/AttributeGroupCollection.js | 100 ++++++ .../catalog/services/CategoryCollection.js | 111 ++---- .../catalog/services/CollectionCollection.js | 96 ++---- .../catalog/services/ProductCollection.js | 266 +++----------- .../services/getAttributeGroupsBaseQuery.js | 3 + .../services/getAttributesBaseQuery.js | 3 + .../services/getCollectionsBaseQuery.js | 1 - ...gisterDefaultAttributeCollectionFilters.js | 133 +++++++ ...egisterDefaultCategoryCollectionFilters.js | 85 +++++ ...isterDefaultCollectionCollectionFilters.js | 65 ++++ ...registerDefaultProductCollectionFilters.js | 198 +++++++++++ .../evershop/src/modules/cms/bootstrap.js | 17 + .../types/CmsPage/CmsPage.resolvers.js | 13 +- .../modules/cms/pages/admin/all/Layout.scss | 5 +- .../cms/pages/admin/cmsPageGrid/Grid.jsx | 71 +++- .../cms/pages/admin/cmsPageGrid/index.js | 3 +- .../modules/cms/services/CMSPageCollection.js | 93 ++--- .../registerDefaultPageCollectionFilters.js | 62 ++++ .../src/modules/customer/bootstrap.js | 16 + .../Customer/Customer.admin.resolvers.js | 6 +- .../pages/admin/customerGrid/Grid.jsx | 78 ++++- .../pages/admin/customerGrid/index.js | 3 +- .../customer/services/CustomerCollection.js | 127 ++----- ...egisterDefaultCustomerCollectionFilters.js | 90 +++++ .../graphql/services/graphqlMiddleware.js | 2 +- .../evershop/src/modules/oms/bootstrap.js | 17 + .../types/Order/Order.admin.resolvers.js | 2 +- .../graphql/types/Order/Order.resolvers.js | 3 +- .../oms/pages/admin/orderGrid/Grid.jsx | 118 ++++--- .../oms/pages/admin/orderGrid/index.js | 3 +- .../modules/oms/services/OrderCollection.js | 158 +-------- .../registerDefaultOrderCollectionFilters.js | 121 +++++++ .../src/modules/promotion/bootstrap.js | 16 + .../types/Coupon/Coupon.admin.resolvers.js | 9 +- .../promotion/pages/admin/couponGrid/Grid.jsx | 102 ++++-- .../promotion/pages/admin/couponGrid/index.js | 3 +- .../promotion/services/CouponCollection.js | 151 ++------ .../registerDefaultCouponCollectionFilters.js | 60 ++++ packages/postgres-query-builder/index.js | 10 + packages/product_review/bootstrap.js | 19 + .../graphql/types/Review/Review.resolvers.js | 2 +- packages/product_review/package.json | 2 +- .../pages/admin/reviewGrid/Grid.jsx | 96 ++++-- .../reviewGrid/header/IsApprovedHeader.jsx | 52 --- .../pages/admin/reviewGrid/index.js | 3 +- .../services/ReviewCollection.js | 109 ++---- .../registerDefaultReviewCollectionFilters.js | 66 ++++ 79 files changed, 2478 insertions(+), 1752 deletions(-) delete mode 100644 packages/evershop/src/components/common/grid/headers/Basic.jsx create mode 100644 packages/evershop/src/components/common/grid/headers/Sortable.jsx create mode 100644 packages/evershop/src/lib/util/defaultPaginationFilters.js create mode 100644 packages/evershop/src/lib/util/filterOperationMapp.js create mode 100644 packages/evershop/src/modules/catalog/services/AttributeCollection.js create mode 100644 packages/evershop/src/modules/catalog/services/AttributeGroupCollection.js create mode 100644 packages/evershop/src/modules/catalog/services/getAttributeGroupsBaseQuery.js create mode 100644 packages/evershop/src/modules/catalog/services/getAttributesBaseQuery.js create mode 100644 packages/evershop/src/modules/catalog/services/registerDefaultAttributeCollectionFilters.js create mode 100644 packages/evershop/src/modules/catalog/services/registerDefaultCategoryCollectionFilters.js create mode 100644 packages/evershop/src/modules/catalog/services/registerDefaultCollectionCollectionFilters.js create mode 100644 packages/evershop/src/modules/catalog/services/registerDefaultProductCollectionFilters.js create mode 100644 packages/evershop/src/modules/cms/services/registerDefaultPageCollectionFilters.js create mode 100644 packages/evershop/src/modules/customer/services/registerDefaultCustomerCollectionFilters.js create mode 100644 packages/evershop/src/modules/oms/services/registerDefaultOrderCollectionFilters.js create mode 100644 packages/evershop/src/modules/promotion/services/registerDefaultCouponCollectionFilters.js create mode 100644 packages/product_review/bootstrap.js delete mode 100644 packages/product_review/pages/admin/reviewGrid/header/IsApprovedHeader.jsx create mode 100644 packages/product_review/services/registerDefaultReviewCollectionFilters.js diff --git a/packages/evershop/src/components/common/form/fields/Input.jsx b/packages/evershop/src/components/common/form/fields/Input.jsx index 3dfa5ca93..ea591c301 100644 --- a/packages/evershop/src/components/common/form/fields/Input.jsx +++ b/packages/evershop/src/components/common/form/fields/Input.jsx @@ -24,7 +24,8 @@ const inputProps = function buidProps(props) { 'onKeyPress', 'onKeyDown', 'onKeyUp', - 'value' + 'value', + 'id' ].forEach((a) => { if (props[a]) obj[a] = props[a]; obj.defaultValue = props.value; diff --git a/packages/evershop/src/components/common/grid/headers/Basic.jsx b/packages/evershop/src/components/common/grid/headers/Basic.jsx deleted file mode 100644 index d50da3010..000000000 --- a/packages/evershop/src/components/common/grid/headers/Basic.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { Input } from '@components/common/form/fields/Input'; - -export default function BasicColumnHeader({ title, id, currentFilters = [] }) { - const filterInput = React.useRef(null); - - const onKeyPress = (e) => { - const url = new URL(document.location); - if (e.key === 'Enter') { - if (e.target.value === '') url.searchParams.delete(id); - else url.searchParams.set(id, e.target.value); - window.location.href = url.href; - } - }; - - React.useEffect(() => { - const filter = currentFilters.find((fillter) => fillter.key === id) || { - value: '' - }; - filterInput.current.value = filter.value; - }, []); - - return ( - -
-
- {title} -
-
- onKeyPress(e)} - placeholder={title} - /> -
-
- - ); -} - -BasicColumnHeader.propTypes = { - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - currentFilters: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string, - value: PropTypes.string - }) - ) -}; - -BasicColumnHeader.defaultProps = { - currentFilters: [] -}; diff --git a/packages/evershop/src/components/common/grid/headers/Dummy.jsx b/packages/evershop/src/components/common/grid/headers/Dummy.jsx index 9e3b9aebd..c8ddb1e75 100644 --- a/packages/evershop/src/components/common/grid/headers/Dummy.jsx +++ b/packages/evershop/src/components/common/grid/headers/Dummy.jsx @@ -5,7 +5,7 @@ export default function DummyColumnHeader({ title }) { return (
-
+
{title}
diff --git a/packages/evershop/src/components/common/grid/headers/Sortable.jsx b/packages/evershop/src/components/common/grid/headers/Sortable.jsx new file mode 100644 index 000000000..06c1d2983 --- /dev/null +++ b/packages/evershop/src/components/common/grid/headers/Sortable.jsx @@ -0,0 +1,148 @@ +/* eslint-disable no-nested-ternary */ +import React from 'react'; +import PropTypes from 'prop-types'; + +function Up() { + return ( + + + + + ); +} + +function Down() { + return ( + + + + + ); +} + +function None() { + return ( + + + + + ); +} + +export default function SortableHeader({ title, name, currentFilters }) { + const [currentDirection] = React.useState(() => { + const currentOrderBy = currentFilters.find((filter) => filter.key === 'ob'); + if (!currentOrderBy || currentOrderBy.value !== name) { + return null; + } else { + return ( + currentFilters.find((filter) => filter.key === 'od')?.value || 'asc' + ); + } + }); + const onChange = () => { + const url = new URL(window.location.href); + url.searchParams.set('ob', name); + // Get the current direction by checking the currentFilters + const currentDirection = currentFilters.find( + (filter) => filter.key === 'od' + ); + if (!currentDirection || currentDirection.value === 'asc') { + url.searchParams.set('od', 'desc'); + } else { + url.searchParams.set('od', 'asc'); + } + window.location.href = url; + }; + + return ( + +
+
+ {title} +
+
+ +
+
+ + ); +} + +SortableHeader.propTypes = { + title: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + currentFilters: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + operations: PropTypes.string.isRequired, + value: PropTypes.string.isRequired + }) + ) +}; + +SortableHeader.defaultProps = { + currentFilters: [] +}; diff --git a/packages/evershop/src/components/common/grid/rows/StatusRow.jsx b/packages/evershop/src/components/common/grid/rows/StatusRow.jsx index 3eec01116..b127f9279 100644 --- a/packages/evershop/src/components/common/grid/rows/StatusRow.jsx +++ b/packages/evershop/src/components/common/grid/rows/StatusRow.jsx @@ -5,7 +5,7 @@ import Dot from '@components/common/Dot'; export default function StatusRow({ id, areaProps }) { return ( -
+
{parseInt(areaProps.row[id], 10) === 0 && ( )} diff --git a/packages/evershop/src/lib/helpers.js b/packages/evershop/src/lib/helpers.js index 1dc4a24dd..742beb52f 100644 --- a/packages/evershop/src/lib/helpers.js +++ b/packages/evershop/src/lib/helpers.js @@ -1,4 +1,5 @@ const path = require('path'); +const { getConfig } = require('./util/getConfig'); const rootPath = process.cwd(); @@ -11,5 +12,6 @@ exports.CONSTANTS = Object.freeze({ NODEMODULEPATH: path.resolve(rootPath, 'node_modules'), THEMEPATH: path.resolve(rootPath, 'themes'), CACHEPATH: path.resolve(rootPath, '.evershop'), - BUILDPATH: path.resolve(rootPath, '.evershop', 'build') + BUILDPATH: path.resolve(rootPath, '.evershop', 'build'), + ADMIN_COLLECTION_SIZE: getConfig('admin_collection_size', 20) }); diff --git a/packages/evershop/src/lib/util/buildFilterFromUrl.js b/packages/evershop/src/lib/util/buildFilterFromUrl.js index 1d40cacb5..6ef227d09 100644 --- a/packages/evershop/src/lib/util/buildFilterFromUrl.js +++ b/packages/evershop/src/lib/util/buildFilterFromUrl.js @@ -1,79 +1,46 @@ -module.exports.buildFilterFromUrl = (query) => { +module.exports.buildFilterFromUrl = (request) => { + const { query } = request; if (!query) { return []; } else { const filtersFromUrl = []; - // Attribute filters Object.keys(query).forEach((key) => { - const filter = query[key]; - if (Array.isArray(filter)) { - const values = filter - .map((v) => parseInt(v, 10)) - .filter((v) => Number.isNaN(v) === false); - if (values.length > 0) { - filtersFromUrl.push({ - key, - operation: 'IN', - value: values.join(',') - }); - } - } else { - // Use regex to check if filter is either started or ended with a '%' - // If so, use LIKE operation - const regex = /^%|%$/; - if (!regex.test(filter)) { - filtersFromUrl.push({ - key, - operation: '=', - value: filter - }); - } else { + // Check if the value is a string + if (typeof query[key] === 'string') { + filtersFromUrl.push({ + key, + operation: 'eq', + value: query[key] + }); + } + // Check if the query is an object with value and operation + if (query[key].value && query[key].operation) { + // Convert key, value and operation to string + const { value, operation } = query[key]; + // Make sure operation is either eq, neq, gt, gteq, lt, lteq, like, nlike, in, nin + if ( + [ + 'eq', + 'neq', + 'gt', + 'gteq', + 'lt', + 'lteq', + 'like', + 'nlike', + 'in', + 'nin' + ].includes(operation) + ) { filtersFromUrl.push({ key, - operation: 'LIKE', - value: filter + operation: operation.toString(), + value: value.toString() }); } } }); - - const { sortBy } = query; - const sortOrder = - query.sortOrder && ['ASC', 'DESC'].includes(query.sortOrder.toUpperCase()) - ? query.sortOrder.toUpperCase() - : 'ASC'; - - if (sortBy) { - filtersFromUrl.push({ - key: 'sortBy', - operation: '=', - value: sortBy.toString() - }); - } - - if (sortOrder !== 'ASC') { - filtersFromUrl.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder - }); - } - // Paging - const page = Number.isNaN(parseInt(query.page, 10)) - ? '1' - : query.page.toString(); - if (page !== '1') { - filtersFromUrl.push({ key: 'page', operation: '=', value: page }); - } - // TODO: Get from config - const limit = Number.isNaN(parseInt(query.limit, 10)) - ? '20' - : query.limit.toString(); - if (limit !== '20') { - filtersFromUrl.push({ key: 'limit', operation: '=', value: limit }); - } - return filtersFromUrl; } }; diff --git a/packages/evershop/src/lib/util/defaultPaginationFilters.js b/packages/evershop/src/lib/util/defaultPaginationFilters.js new file mode 100644 index 000000000..a9f5a80a1 --- /dev/null +++ b/packages/evershop/src/lib/util/defaultPaginationFilters.js @@ -0,0 +1,75 @@ +const { CONSTANTS } = require('../helpers'); + +const defaultPaginationFilters = [ + { + key: 'od', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + if (['ASC', 'DESC', 'asc', 'desc'].includes(value)) { + query.orderDirection(value.toUpperCase()); + currentFilters.push({ + key: 'od', + operation, + value + }); + } + } + }, + { + key: 'page', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + if (parseInt(value, 10) > 0) { + query.limit( + (parseInt(value, 10) - 1) * CONSTANTS.ADMIN_COLLECTION_SIZE, + CONSTANTS.ADMIN_COLLECTION_SIZE + ); + currentFilters.push({ + key: 'page', + operation, + value + }); + } else { + query.limit(0, CONSTANTS.ADMIN_COLLECTION_SIZE); + currentFilters.push({ + key: 'page', + operation, + value: 1 + }); + } + } + }, + { + key: 'limit', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + if (parseInt(value, 10) > 0) { + // Get the current page from the current filters + const page = currentFilters.find((f) => f.key === 'page'); + if (page) { + query.limit( + (parseInt(page.value, 10) - 1) * parseInt(value, 10), + parseInt(value, 10) + ); + } else { + query.limit(0, parseInt(value, 10)); + } + currentFilters.push({ + key: 'limit', + operation, + value + }); + } else { + currentFilters.push({ + key: 'limit', + operation, + value: CONSTANTS.ADMIN_COLLECTION_SIZE + }); + } + } + } +]; + +module.exports = { + defaultPaginationFilters +}; diff --git a/packages/evershop/src/lib/util/filterOperationMapp.js b/packages/evershop/src/lib/util/filterOperationMapp.js new file mode 100644 index 000000000..28f4e5f81 --- /dev/null +++ b/packages/evershop/src/lib/util/filterOperationMapp.js @@ -0,0 +1,16 @@ +// Map the operation to the SQL operation +const OPERATION_MAP = { + eq: '=', + neq: '<>', + gt: '>', + gteq: '>=', + lt: '<', + lteq: '<=', + like: 'ILIKE', + nlike: 'NOT ILIKE', + in: 'IN', + nin: 'NOT IN' +}; + +module.exports = exports; +exports.OPERATION_MAP = OPERATION_MAP; diff --git a/packages/evershop/src/lib/util/registry.js b/packages/evershop/src/lib/util/registry.js index 48daab546..d5fe3a604 100644 --- a/packages/evershop/src/lib/util/registry.js +++ b/packages/evershop/src/lib/util/registry.js @@ -169,22 +169,52 @@ const registry = new Registry(); module.exports = { /** * @param {String} name - * @param {any} initValue + * @param {any} initialization * @param {Object} context * @param {Function} validator */ - async getValue(name, initValue, context, validator) { + async getValue(name, initialization, context, validator) { + let initValue; + // Check if the initValue is a function, then add this function to the processors as the first processor + if (typeof initialization === 'function') { + // Add this function to the processors, add this to the biginning of the processors + const processors = this.values[name] ? this.values[name].processors : []; + processors.unshift({ + callback: initialization, + priority: 0 + }); + this.values[name] = this.values[name] || {}; + this.values[name].processors = processors; + } else { + initValue = initialization; + } const val = await registry.get(name, initValue, context, validator); return val; }, /** * @param {String} name - * @param {any} initValue + * @param {any} initialization * @param {Object} context * @param {Function} validator */ - getValueSync(name, initValue, context, validator) { + getValueSync(name, initialization, context, validator) { + let initValue; + // Check if the initValue is a function, then add this function to the processors as the first processor + if (typeof initialization === 'function') { + // Add this function to the processors, add this to the biginning of the processors + const processors = registry.values[name] + ? registry.values[name]?.processors + : []; + processors.unshift({ + callback: initialization, + priority: 0 + }); + registry.values[name] = registry.values[name] || {}; + registry.values[name].processors = processors; + } else { + initValue = initialization; + } const val = registry.getSync(name, initValue, context, validator); return val; }, diff --git a/packages/evershop/src/modules/catalog/bootstrap.js b/packages/evershop/src/modules/catalog/bootstrap.js index 69ed15b23..e78c348ec 100644 --- a/packages/evershop/src/modules/catalog/bootstrap.js +++ b/packages/evershop/src/modules/catalog/bootstrap.js @@ -1,4 +1,12 @@ const config = require('config'); +const { addProcessor } = require('../../lib/util/registry'); +const registerDefaultProductCollectionFilters = require('./services/registerDefaultProductCollectionFilters'); +const registerDefaultCategoryCollectionFilters = require('./services/registerDefaultCategoryCollectionFilters'); +const registerDefaultCollectionCollectionFilters = require('./services/registerDefaultCollectionCollectionFilters'); +const registerDefaultAttributeCollectionFilters = require('./services/registerDefaultAttributeCollectionFilters'); +const { + defaultPaginationFilters +} = require('../../lib/util/defaultPaginationFilters'); module.exports = () => { const catalogConfig = { @@ -30,4 +38,59 @@ module.exports = () => { }; config.util.setModuleDefaults('pricing', pricingConfig); // Getting config value like this: config.get('catalog.product.image.thumbnail.width'); + + // Reigtering the default filters for product collection + addProcessor( + 'productCollectionFilters', + registerDefaultProductCollectionFilters, + 1 + ); + addProcessor( + 'productCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); + + // Reigtering the default filters for category collection + addProcessor( + 'categoryCollectionFilters', + registerDefaultCategoryCollectionFilters, + 1 + ); + addProcessor( + 'categoryCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); + + // Reigtering the default filters for collection collection + addProcessor( + 'collectionCollectionFilters', + registerDefaultCollectionCollectionFilters, + 1 + ); + addProcessor( + 'collectionCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); + + // Reigtering the default filters for attribute collection + addProcessor( + 'attributeCollectionFilters', + registerDefaultAttributeCollectionFilters, + 1 + ); + addProcessor( + 'attributeCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); + + // Reigtering the default filters for attribute group collection + addProcessor( + 'attributeGroupCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 1 + ); }; diff --git a/packages/evershop/src/modules/catalog/graphql/types/Attribute/Attribute.admin.graphql b/packages/evershop/src/modules/catalog/graphql/types/Attribute/Attribute.admin.graphql index e965014ff..0c2b407c0 100644 --- a/packages/evershop/src/modules/catalog/graphql/types/Attribute/Attribute.admin.graphql +++ b/packages/evershop/src/modules/catalog/graphql/types/Attribute/Attribute.admin.graphql @@ -6,11 +6,11 @@ type AttributeGroup { uuid: String! groupName: String! updateApi: String! - attributes: [Attribute] + attributes: AttributeCollection } extend type Attribute { - groups: [AttributeGroup] + groups: AttributeGroupCollection editUrl: String! updateApi: String! deleteApi: String! diff --git a/packages/evershop/src/modules/catalog/graphql/types/Attribute/Attribute.admin.resolvers.js b/packages/evershop/src/modules/catalog/graphql/types/Attribute/Attribute.admin.resolvers.js index f28d61fff..a9ff806b4 100644 --- a/packages/evershop/src/modules/catalog/graphql/types/Attribute/Attribute.admin.resolvers.js +++ b/packages/evershop/src/modules/catalog/graphql/types/Attribute/Attribute.admin.resolvers.js @@ -1,293 +1,65 @@ /* eslint-disable no-param-reassign */ -const { select } = require('@evershop/postgres-query-builder'); const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); -const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); +const { + getAttributesBaseQuery +} = require('../../../services/getAttributesBaseQuery'); +const { + getAttributeGroupsBaseQuery +} = require('../../../services/getAttributeGroupsBaseQuery'); +const { + AttributeCollection +} = require('../../../services/AttributeCollection'); +const { + AttributeGroupCollection +} = require('../../../services/AttributeGroupCollection'); module.exports = { Query: { - attributes: async (_, { filters: requestedFilters = [] }, { pool }) => { - const query = select().from('attribute'); - const currentFilters = []; - const filters = requestedFilters.map((filter) => { - if (filter.operation.toUpperCase() === 'LIKE') { - filter.valueRaw = filter.value.replace(/^%/, '').replace(/%$/, ''); - } else { - filter.valueRaw = filter.value; - } - if (filter.operation.toUpperCase() === 'IN') { - filter.value = filter.value.split(','); - } - return filter; - }); - - // Name filter - const nameFilter = filters.find((f) => f.key === 'name'); - if (nameFilter) { - query.andWhere( - 'attribute.attribute_name', - 'LIKE', - `%${nameFilter.value}%` - ); - currentFilters.push({ - key: 'name', - operation: '=', - value: nameFilter.value - }); - } - - // Code filter - const codeFilter = filters.find((f) => f.key === 'code'); - if (codeFilter) { - query.andWhere( - 'attribute.attribute_code', - codeFilter.operation, - codeFilter.value - ); - currentFilters.push({ - key: 'code', - operation: codeFilter.operation, - value: codeFilter.valueRaw - }); - } - - // Attribute group filter - const groupFilter = filters.find((f) => f.key === 'group'); - if (groupFilter) { - const attributes = await select() - .from('attribute_group_link') - .where('group_id', groupFilter.operation, groupFilter.value) - .execute(pool); - - query.andWhere( - 'attribute.attribute_id', - 'IN', - attributes.map((a) => a.attribute_id) - ); - currentFilters.push({ - key: 'group', - operation: groupFilter.operation, - value: groupFilter.valueRaw - }); - } - - // Type filter - const typeFilter = filters.find((f) => f.key === 'type'); - if (typeFilter) { - query.andWhere( - 'attribute.type', - typeFilter.operation, - typeFilter.value - ); - currentFilters.push({ - key: 'type', - operation: typeFilter.operation, - value: typeFilter.valueRaw - }); - } - - // isRequired filter - const isRequiredFilter = filters.find((f) => f.key === 'isRequired'); - if (isRequiredFilter) { - query.andWhere( - 'attribute.is_required', - isRequiredFilter.operation, - isRequiredFilter.value - ); - currentFilters.push({ - key: 'isRequired', - operation: isRequiredFilter.operation, - value: isRequiredFilter.valueRaw - }); - } - - // isFilterable filter - const isFilterableFilter = filters.find((f) => f.key === 'isFilterable'); - if (isFilterableFilter) { - query.andWhere( - 'attribute.is_filterable', - isFilterableFilter.operation, - isFilterableFilter.value - ); - currentFilters.push({ - key: 'isFilterable', - operation: isFilterableFilter.operation, - value: isFilterableFilter.valueRaw - }); - } - - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => f.key === 'sortOrder' && ['ASC', 'DESC'].includes(f.value) - ) || { value: 'ASC' }; - if (sortBy && sortBy.value === 'name') { - query.orderBy('attribute.attribute_name', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - query.orderBy('attribute.attribute_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } - // Clone the main query for getting total right before doing the paging - const cloneQuery = query.clone(); - cloneQuery.select('COUNT(*)', 'total'); - cloneQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - query.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); - return { - items: (await query.execute(pool)).map((row) => camelCase(row)), - total: (await cloneQuery.load(pool)).total, - currentFilters - }; + attributes: async (_, { filters = [] }) => { + const query = getAttributesBaseQuery(); + const root = new AttributeCollection(query); + await root.init(filters); + return root; }, - attributeGroups: async ( - _, - { filters: requestedFilters = [] }, - { pool } - ) => { - const query = select().from('attribute_group'); - - const currentFilters = []; - - const filters = requestedFilters.map((filter) => { - if (filter.operation.toUpperCase() === 'LIKE') { - filter.valueRaw = filter.value.replace(/^%/, '').replace(/%$/, ''); - } else { - filter.valueRaw = filter.value; - } - if (filter.operation.toUpperCase() === 'IN') { - filter.value = filter.value.split(','); - } - return filter; - }); - - // Name filter - const nameFilter = filters.find((f) => f.key === 'name'); - if (nameFilter) { - query.andWhere( - 'attribute_group.group_name', - 'LIKE', - `%${nameFilter.value}%` - ); - currentFilters.push({ - key: 'name', - operation: '=', - value: nameFilter.value - }); - } - - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => f.key === 'sortOrder' && ['ASC', 'DESC'].includes(f.value) - ) || { value: 'ASC' }; - if (sortBy && sortBy.value === 'name') { - query.orderBy('attribute_group.group_name', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - query.orderBy('attribute_group.attribute_group_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } - // Clone the main query for getting total right before doing the paging - const cloneQuery = query.clone(); - cloneQuery.select('COUNT(*)', 'total'); - cloneQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - query.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); - return { - items: (await query.execute(pool)).map((row) => camelCase(row)), - total: (await cloneQuery.load(pool)).total, - currentFilters - }; + attributeGroups: async (_, { filters = [] }) => { + const query = getAttributeGroupsBaseQuery(); + const root = new AttributeGroupCollection(query); + await root.init(filters); + return root; } }, AttributeGroup: { - attributes: async (group, _, { pool }) => { - const rows = await select() - .from('attribute') - .where( - 'attribute_id', - 'IN', - ( - await select('attribute_id') - .from('attribute_group_link') - .where('group_id', '=', group.attributeGroupId) - .execute(pool) - ).map((a) => a.attribute_id) - ) - .execute(pool); - return rows.map((row) => camelCase(row)); + attributes: async (group, { filters = [] }) => { + const query = getAttributesBaseQuery(); + query + .innerJoin('attribute_group_link') + .on('attribute.attribute_id', '=', 'attribute_group_link.attribute_id'); + query.where('attribute_group_link.group_id', '=', group.attributeGroupId); + const root = new AttributeCollection(query); + await root.init(filters); + return root; }, updateApi: (group) => buildUrl('updateAttributeGroup', { id: group.uuid }) }, Attribute: { - groups: async (attribute, _, { pool }) => { - const results = await select() - .from('attribute_group') - .where( - 'attribute_group_id', - 'IN', - ( - await select('group_id') - .from('attribute_group_link') - .where('attribute_id', '=', attribute.attributeId) - .execute(pool) - ).map((g) => g.group_id) - ) - .execute(pool); - return results.map((result) => camelCase(result)); + groups: async (attribute, { filters = [] }) => { + const query = getAttributeGroupsBaseQuery(); + query + .innerJoin('attribute_group_link') + .on( + 'attribute_group.attribute_group_id', + '=', + 'attribute_group_link.group_id' + ); + query.where( + 'attribute_group_link.attribute_id', + '=', + attribute.attributeId + ); + const root = new AttributeGroupCollection(query); + await root.init(filters); + return root; }, editUrl: ({ uuid }) => buildUrl('attributeEdit', { id: uuid }), updateApi: (attribute) => diff --git a/packages/evershop/src/modules/catalog/graphql/types/Category/Category.graphql b/packages/evershop/src/modules/catalog/graphql/types/Category/Category.graphql index efb41ff0b..7e94d42f5 100644 --- a/packages/evershop/src/modules/catalog/graphql/types/Category/Category.graphql +++ b/packages/evershop/src/modules/catalog/graphql/types/Category/Category.graphql @@ -31,11 +31,24 @@ type CategoryImage { } """ -The `FilterInput` type represents a filter input object. +The `FilterInput` type represents a filter input object. Operations must be one of the following: eq, neq, gt, gteq, lt, lteq, like, nlike, in, nin. """ +enum FilterOperation { + eq + neq + gt + gteq + lt + lteq + like + nlike + in + nin +} + input FilterInput { key: String! - operation: String! + operation: FilterOperation! value: String } diff --git a/packages/evershop/src/modules/catalog/graphql/types/Category/Category.resolvers.js b/packages/evershop/src/modules/catalog/graphql/types/Category/Category.resolvers.js index 1167b8ff5..a7da8733c 100644 --- a/packages/evershop/src/modules/catalog/graphql/types/Category/Category.resolvers.js +++ b/packages/evershop/src/modules/catalog/graphql/types/Category/Category.resolvers.js @@ -31,7 +31,7 @@ module.exports = { categories: async (_, { filters = [] }, { user }) => { const query = getCategoriesBaseQuery(); const root = new CategoryCollection(query); - await root.init({}, { filters }, { user }); + await root.init(filters, !!user); return root; } }, diff --git a/packages/evershop/src/modules/catalog/graphql/types/Collection/Collection.resolvers.js b/packages/evershop/src/modules/catalog/graphql/types/Collection/Collection.resolvers.js index e7877edc5..78666497e 100644 --- a/packages/evershop/src/modules/catalog/graphql/types/Collection/Collection.resolvers.js +++ b/packages/evershop/src/modules/catalog/graphql/types/Collection/Collection.resolvers.js @@ -22,7 +22,7 @@ module.exports = { collections: async (_, { filters = [] }) => { const query = getCollectionsBaseQuery(); const root = new CollectionCollection(query); - await root.init({}, { filters }); + await root.init(filters); return root; } }, @@ -30,7 +30,7 @@ module.exports = { products: async (collection, { filters = [] }, { user }) => { const query = getProductsByCollectionBaseQuery(collection.collectionId); const root = new ProductCollection(query); - await root.init(collection, { filters }, { user }); + await root.init(filters, !!user); return root; } }, diff --git a/packages/evershop/src/modules/catalog/graphql/types/Product/Product.resolvers.js b/packages/evershop/src/modules/catalog/graphql/types/Product/Product.resolvers.js index 5539684dc..5116727b9 100644 --- a/packages/evershop/src/modules/catalog/graphql/types/Product/Product.resolvers.js +++ b/packages/evershop/src/modules/catalog/graphql/types/Product/Product.resolvers.js @@ -36,7 +36,7 @@ module.exports = { products: async (_, { filters = [] }, { user }) => { const query = getProductsBaseQuery(); const root = new ProductCollection(query); - await root.init({}, { filters }, { user }); + await root.init(filters, !!user); return root; } } diff --git a/packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.graphql b/packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.graphql index f5e67e9df..5c067ff36 100644 --- a/packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.graphql +++ b/packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.graphql @@ -23,8 +23,8 @@ Represents a product variant attribute index type VariantAttributeIndex { attributeId: ID! attributeCode: String! - optionId: Int! - optionText: String! + optionId: Int + optionText: String } """ diff --git a/packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.resolvers.js b/packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.resolvers.js index ab525d182..c321b513f 100644 --- a/packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.resolvers.js +++ b/packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.resolvers.js @@ -24,7 +24,6 @@ module.exports = { .where('variant_group_id', '=', variantGroupId) .load(pool); - const variants = []; const query = select(); query .from('product') @@ -36,47 +35,26 @@ module.exports = { .select('product_attribute_value_index.option_text'); query - .innerJoin('product_attribute_value_index') + .leftJoin('product_attribute_value_index') .on( 'product.product_id', '=', 'product_attribute_value_index.product_id' ); query - .innerJoin('attribute') + .leftJoin('attribute') .on( 'product_attribute_value_index.attribute_id', '=', 'attribute.attribute_id' ); - query.where('variant_group_id', '=', variantGroupId).and( - 'attribute.attribute_id', - 'IN', - Object.values(group).filter((v) => Number.isInteger(v)) - ); + query.where('variant_group_id', '=', variantGroupId); if (!user) { query.andWhere('status', '=', 1); } const vs = await query.execute(pool); - // Filter the vs array, make sure that each product has all the attributes - // that are in the variant group. - let filteredVs; - if (!user) { - filteredVs = vs.filter((v) => { - const attributes = Object.values(group).filter((attr) => - Number.isInteger(attr) - ); - const productAttributes = vs - .filter((p) => p.product_id === v.product_id) - .map((p) => p.attribute_id); - return attributes.every((a) => productAttributes.includes(a)); - }); - } else { - filteredVs = vs; - } - - let attributes = await select() + const attributes = await select() .from('attribute') .where( 'attribute_id', @@ -85,77 +63,70 @@ module.exports = { ) .execute(pool); - attributes = attributes.map((a) => ({ - attributeId: a.attribute_id, - attributeCode: a.attribute_code, - attributeName: a.attribute_name - })); - - const promises = attributes.map(async (attribute) => { - const options = await select() - .from('attribute_option') - .where('attribute_id', '=', attribute.attributeId) - .execute(pool); - - // eslint-disable-next-line no-param-reassign - attribute.options = options.map((o) => { - // Check if the option is used in a variant - const used = filteredVs.find( - (v) => - parseInt(v.option_id, 10) === - parseInt(o.attribute_option_id, 10) - ); - if (!used) { - return { - optionId: o.attribute_option_id, - optionText: o.option_text - }; - } else { - return { - optionId: o.attribute_option_id, - optionText: o.option_text, - productId: used.product_id - }; - } - }); - return attribute; - }); - - attributes = await Promise.all(promises); - - for (let i = 0, len = filteredVs.length; i < len; i += 1) { - const ind = variants.findIndex( - (v) => v.productId === filteredVs[i].product_id - ); - if (ind !== -1) { - if (!variants[ind].attributes) { - variants[ind].attributes = []; - } - variants[ind].attributes.push({ - attributeCode: filteredVs[i].attribute_code, - attributeId: filteredVs[i].attribute_id, - optionId: filteredVs[i].option_id, - optionText: filteredVs[i].option_text - }); - } else { - variants.push({ - productId: filteredVs[i].product_id, - attributes: [ - { - attributeCode: filteredVs[i].attribute_code, - attributeId: filteredVs[i].attribute_id, - optionId: filteredVs[i].option_id, - optionText: filteredVs[i].option_text - } - ] - }); - } - } - return { variantGroupId, - variantAttributes: attributes, - items: variants.map((v) => ({ ...v, id: `id${uniqid()}` })), + variantAttributes: attributes.map((a) => { + // We need to get all the options available from the variants list + const options = vs + .filter((v) => v.attribute_id === a.attribute_id) + .map((v) => ({ + optionId: v.option_id, + optionText: v.option_text, + productId: v.product_id + })); + return { + attributeId: a.attribute_id, + attributeCode: a.attribute_code, + attributeName: a.attribute_name, + options + }; + }), + items: () => + vs + .reduce((acc, v) => { + const product = acc.find((p) => p.product_id === v.product_id); + if (!product) { + acc.push({ + product_id: v.product_id, + attributes: [ + { + attributeId: v.attribute_id, + attributeCode: v.attribute_code, + optionId: v.option_id, + optionText: v.option_text + } + ] + }); + } else { + product.attributes.push({ + attributeId: v.attribute_id, + attributeCode: v.attribute_code, + optionId: v.option_id, + optionText: v.option_text + }); + } + return acc; + }, []) + .map((p) => { + const productAttributes = p.attributes.map( + (a) => a.attributeCode + ); + const missingAttributes = attributes + .filter((a) => !productAttributes.includes(a.attribute_code)) + .map((a) => ({ + attributeId: a.attribute_id, + attributeCode: a.attribute_code, + optionId: null, + optionText: null + })); + return { + productId: p.product_id, + id: `id-${uniqid()}`, + attributes: [...p.attributes, ...missingAttributes].filter( + (a) => a.attributeCode + ) + }; + }), addItemApi: buildUrl('addVariantItem', { id: group.uuid }) }; } diff --git a/packages/evershop/src/modules/catalog/pages/admin/attributeEdit+attributeNew/General.jsx b/packages/evershop/src/modules/catalog/pages/admin/attributeEdit+attributeNew/General.jsx index c47ec3e9a..06e7c7a3b 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/attributeEdit+attributeNew/General.jsx +++ b/packages/evershop/src/modules/catalog/pages/admin/attributeEdit+attributeNew/General.jsx @@ -274,7 +274,7 @@ export default function General({ attribute, createGroupApi }) { )} @@ -294,12 +294,14 @@ General.propTypes = { optionText: PropTypes.string }) ), - groups: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.number, - label: PropTypes.string - }) - ) + groups: { + items: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.number, + label: PropTypes.string + }) + ) + } }), createGroupApi: PropTypes.string.isRequired }; @@ -328,8 +330,10 @@ export const query = ` optionText } groups { - value: attributeGroupId - label: groupName + items { + value: attributeGroupId + label: groupName + } } } createGroupApi: url(routeId: "createAttributeGroup") diff --git a/packages/evershop/src/modules/catalog/pages/admin/attributeGrid/Grid.jsx b/packages/evershop/src/modules/catalog/pages/admin/attributeGrid/Grid.jsx index fd49c3bb6..d037daa82 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/attributeGrid/Grid.jsx +++ b/packages/evershop/src/modules/catalog/pages/admin/attributeGrid/Grid.jsx @@ -12,9 +12,10 @@ import AttributeNameRow from '@components/admin/catalog/attributeGrid/rows/Attri import GroupRow from '@components/admin/catalog/attributeGrid/rows/GroupRow'; import BasicRow from '@components/common/grid/rows/BasicRow'; import YesNoRow from '@components/common/grid/rows/YesNoRow'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; -import GroupHeader from '@components/admin/catalog/attributeGrid/headers/GroupHeader'; -import DropdownColumnHeader from '@components/common/grid/headers/Dropdown'; +import SortableHeader from '@components/common/grid/headers/Sortable'; +import DummyColumnHeader from '@components/common/grid/headers/Dummy'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; function Actions({ attributes = [], selectedIds = [] }) { const { openAlert, closeAlert } = useAlertContext(); @@ -107,6 +108,45 @@ export default function AttributeGrid({ return ( + + f.key === 'name')?.value} + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const name = document.getElementById('name')?.value; + if (name) { + url.searchParams.set('name[operation]', 'like'); + url.searchParams.set('name[value]', name); + } else { + url.searchParams.delete('name[operation]'); + url.searchParams.delete('name[value]'); + } + window.location.href = url; + } + }} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => { + // Just get the url and remove all query params + const url = new URL(document.location); + url.search = ''; + window.location.href = url.href; + } + } + ]} + /> @@ -127,8 +167,8 @@ export default function AttributeGrid({ { component: { default: () => ( - @@ -138,25 +178,17 @@ export default function AttributeGrid({ }, { component: { - default: () => ( - - ) + default: () => }, sortOrder: 15 }, { component: { default: () => ( - ) }, @@ -165,14 +197,10 @@ export default function AttributeGrid({ { component: { default: () => ( - ) }, @@ -181,14 +209,10 @@ export default function AttributeGrid({ { component: { default: () => ( - ) }, @@ -238,7 +262,7 @@ export default function AttributeGrid({ }, { component: { - default: () => + default: () => }, sortOrder: 15 }, @@ -329,9 +353,11 @@ export const query = ` updateApi deleteApi groups { - attributeGroupId - groupName - updateApi + items { + attributeGroupId + groupName + updateApi + } } } total diff --git a/packages/evershop/src/modules/catalog/pages/admin/attributeGrid/index.js b/packages/evershop/src/modules/catalog/pages/admin/attributeGrid/index.js index 9e12edad0..8e476d162 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/attributeGrid/index.js +++ b/packages/evershop/src/modules/catalog/pages/admin/attributeGrid/index.js @@ -11,5 +11,5 @@ module.exports = (request, response) => { title: 'Attributes', description: 'Attributes' }); - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request.query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/evershop/src/modules/catalog/pages/admin/categoryGrid/Grid.jsx b/packages/evershop/src/modules/catalog/pages/admin/categoryGrid/Grid.jsx index d03258dc5..9550bac48 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/categoryGrid/Grid.jsx +++ b/packages/evershop/src/modules/catalog/pages/admin/categoryGrid/Grid.jsx @@ -10,10 +10,11 @@ import { useAlertContext } from '@components/common/modal/Alert'; import { Checkbox } from '@components/common/form/fields/Checkbox'; import { Card } from '@components/admin/cms/Card'; import CategoryNameRow from '@components/admin/catalog/categoryGrid/rows/CategoryName'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; -import DropdownColumnHeader from '@components/common/grid/headers/Dropdown'; import StatusRow from '@components/common/grid/rows/StatusRow'; import YesNoRow from '@components/common/grid/rows/YesNoRow'; +import SortableHeader from '@components/common/grid/headers/Sortable'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; function Actions({ categories = [], selectedIds = [] }) { const { openAlert, closeAlert } = useAlertContext(); @@ -106,6 +107,45 @@ export default function CategoryGrid({ return ( + + f.key === 'name')?.value} + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const name = document.getElementById('name')?.value; + if (name) { + url.searchParams.set('name[operation]', 'like'); + url.searchParams.set('name[value]', name); + } else { + url.searchParams.delete('name[operation]'); + url.searchParams.delete('name[value]'); + } + window.location.href = url; + } + }} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => { + // Just get the url and remove all query params + const url = new URL(document.location); + url.search = ''; + window.location.href = url.href; + } + } + ]} + />
@@ -128,9 +168,9 @@ export default function CategoryGrid({ { component: { default: () => ( - ) @@ -140,14 +180,10 @@ export default function CategoryGrid({ { component: { default: () => ( - ) }, @@ -156,14 +192,10 @@ export default function CategoryGrid({ { component: { default: () => ( - ) }, diff --git a/packages/evershop/src/modules/catalog/pages/admin/categoryGrid/index.js b/packages/evershop/src/modules/catalog/pages/admin/categoryGrid/index.js index 8a41f6227..d5d83b8ed 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/categoryGrid/index.js +++ b/packages/evershop/src/modules/catalog/pages/admin/categoryGrid/index.js @@ -11,5 +11,5 @@ module.exports = (request, response) => { title: 'Categories', description: 'Categories' }); - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request.query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/evershop/src/modules/catalog/pages/admin/collectionEdit/Products.jsx b/packages/evershop/src/modules/catalog/pages/admin/collectionEdit/Products.jsx index 08bf4cd60..bcbb00182 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/collectionEdit/Products.jsx +++ b/packages/evershop/src/modules/catalog/pages/admin/collectionEdit/Products.jsx @@ -46,13 +46,13 @@ export default function Products({ collection: { code, addProductApi } }) { code, filters: !keyword ? [ - { key: 'page', operation: '=', value: page.toString() }, - { key: 'limit', operation: '=', value: '10' } + { key: 'page', operation: 'eq', value: page.toString() }, + { key: 'limit', operation: 'eq', value: '10' } ] : [ - { key: 'page', operation: '=', value: page.toString() }, - { key: 'limit', operation: '=', value: '10' }, - { key: 'keyword', operation: '=', value: keyword } + { key: 'page', operation: 'eq', value: page.toString() }, + { key: 'limit', operation: 'eq', value: '10' }, + { key: 'keyword', operation: 'eq', value: keyword } ] }, pause: true diff --git a/packages/evershop/src/modules/catalog/pages/admin/collectionGrid/Grid.jsx b/packages/evershop/src/modules/catalog/pages/admin/collectionGrid/Grid.jsx index ad9c560cc..bdcd178bd 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/collectionGrid/Grid.jsx +++ b/packages/evershop/src/modules/catalog/pages/admin/collectionGrid/Grid.jsx @@ -10,9 +10,11 @@ import { useAlertContext } from '@components/common/modal/Alert'; import { Checkbox } from '@components/common/form/fields/Checkbox'; import { Card } from '@components/admin/cms/Card'; import CollectionNameRow from '@components/admin/catalog/collectionGrid/rows/CollectionNameRow'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; import TextRow from '@components/common/grid/rows/TextRow'; import DummyColumnHeader from '@components/common/grid/headers/Dummy'; +import SortableHeader from '@components/common/grid/headers/Sortable'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; function Actions({ collections = [], selectedIds = [] }) { const { openAlert, closeAlert } = useAlertContext(); @@ -106,6 +108,45 @@ export default function CollectionGrid({ return (
+ + f.key === 'name')?.value} + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const name = document.getElementById('name')?.value; + if (name) { + url.searchParams.set('name[operation]', 'like'); + url.searchParams.set('name[value]', name); + } else { + url.searchParams.delete('name[operation]'); + url.searchParams.delete('name[value]'); + } + window.location.href = url; + } + }} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => { + // Just get the url and remove all query params + const url = new URL(document.location); + url.search = ''; + window.location.href = url.href; + } + } + ]} + />
@@ -140,9 +181,9 @@ export default function CollectionGrid({ { component: { default: () => ( - ) @@ -152,9 +193,9 @@ export default function CollectionGrid({ { component: { default: () => ( - ) diff --git a/packages/evershop/src/modules/catalog/pages/admin/collectionGrid/index.js b/packages/evershop/src/modules/catalog/pages/admin/collectionGrid/index.js index 506eea526..997039018 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/collectionGrid/index.js +++ b/packages/evershop/src/modules/catalog/pages/admin/collectionGrid/index.js @@ -11,5 +11,5 @@ module.exports = (request, response) => { title: 'Collections', description: 'Collections' }); - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request.query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/evershop/src/modules/catalog/pages/admin/productEdit+productNew/Attributes.jsx b/packages/evershop/src/modules/catalog/pages/admin/productEdit+productNew/Attributes.jsx index ad3e91db5..a11ee3e85 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/productEdit+productNew/Attributes.jsx +++ b/packages/evershop/src/modules/catalog/pages/admin/productEdit+productNew/Attributes.jsx @@ -66,7 +66,7 @@ export default function Attributes({ product, groups: { items } }) {
- {currentGroup.attributes.map((attribute, index) => { + {currentGroup.attributes.items.map((attribute, index) => { const valueIndex = attributeIndex.find( (idx) => idx.attributeId === attribute.attributeId ); @@ -210,21 +210,23 @@ Attributes.propTypes = { PropTypes.shape({ groupId: PropTypes.number, groupName: PropTypes.string, - attributes: PropTypes.arrayOf( - PropTypes.shape({ - attributeId: PropTypes.number, - attributeName: PropTypes.string, - attributeCode: PropTypes.string, - type: PropTypes.string, - isRequired: PropTypes.number, - options: PropTypes.arrayOf( - PropTypes.shape({ - optionId: PropTypes.number, - optionText: PropTypes.string - }) - ) - }) - ) + attributes: { + items: PropTypes.arrayOf( + PropTypes.shape({ + attributeId: PropTypes.number, + attributeName: PropTypes.string, + attributeCode: PropTypes.string, + type: PropTypes.string, + isRequired: PropTypes.number, + options: PropTypes.arrayOf( + PropTypes.shape({ + optionId: PropTypes.number, + optionText: PropTypes.string + }) + ) + }) + ) + } }) ) }), @@ -267,14 +269,16 @@ export const query = ` groupId: attributeGroupId groupName attributes { - attributeId - attributeName - attributeCode - type - isRequired - options { - optionId: attributeOptionId - optionText + items { + attributeId + attributeName + attributeCode + type + isRequired + options { + optionId: attributeOptionId + optionText + } } } } diff --git a/packages/evershop/src/modules/catalog/pages/admin/productGrid/Grid.jsx b/packages/evershop/src/modules/catalog/pages/admin/productGrid/Grid.jsx index 5811c3d82..cce1157bf 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/productGrid/Grid.jsx +++ b/packages/evershop/src/modules/catalog/pages/admin/productGrid/Grid.jsx @@ -11,12 +11,12 @@ import StatusRow from '@components/common/grid/rows/StatusRow'; import ProductPriceRow from '@components/admin/catalog/productGrid/rows/PriceRow'; import BasicRow from '@components/common/grid/rows/BasicRow'; import ThumbnailRow from '@components/admin/catalog/productGrid/rows/ThumbnailRow'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; -import FromToColumnHeader from '@components/common/grid/headers/FromTo'; -import DropdownColumnHeader from '@components/common/grid/headers/Dropdown'; import { Card } from '@components/admin/cms/Card'; import DummyColumnHeader from '@components/common/grid/headers/Dummy'; import QtyRow from '@components/admin/catalog/productGrid/rows/QtyRow'; +import SortableHeader from '@components/common/grid/headers/Sortable'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; function Actions({ products = [], selectedIds = [] }) { const { openAlert, closeAlert } = useAlertContext(); @@ -170,6 +170,38 @@ export default function ProductGrid({ return ( + + f.key === 'keyword')?.value} + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const keyword = document.getElementById('keyword')?.value; + if (keyword) { + url.searchParams.set('keyword', keyword); + } else { + url.searchParams.delete('keyword'); + } + window.location.href = url; + } + }} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => {} + } + ]} + />
@@ -189,15 +221,25 @@ export default function ProductGrid({ noOuter coreComponents={[ { - component: { default: () => }, + component: { + default: () => ( + + ) + }, sortOrder: 5 }, { component: { default: () => ( - ) @@ -207,9 +249,9 @@ export default function ProductGrid({ { component: { default: () => ( - ) @@ -218,22 +260,16 @@ export default function ProductGrid({ }, { component: { - default: () => ( - - ) + default: () => }, sortOrder: 20 }, { component: { default: () => ( - ) @@ -243,14 +279,10 @@ export default function ProductGrid({ { component: { default: () => ( - ) }, @@ -432,6 +464,7 @@ export const query = ` value } } + newProductUrl: url(routeId: "productNew") } `; diff --git a/packages/evershop/src/modules/catalog/pages/admin/productGrid/index.js b/packages/evershop/src/modules/catalog/pages/admin/productGrid/index.js index cdf3746e6..1abe21a3b 100644 --- a/packages/evershop/src/modules/catalog/pages/admin/productGrid/index.js +++ b/packages/evershop/src/modules/catalog/pages/admin/productGrid/index.js @@ -11,6 +11,5 @@ module.exports = (request, response) => { title: 'Products', description: 'Products' }); - const { query } = request; - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/evershop/src/modules/catalog/pages/frontStore/homepage/FeaturedProducts.jsx b/packages/evershop/src/modules/catalog/pages/frontStore/homepage/FeaturedProducts.jsx index b86420da7..3366e8be4 100644 --- a/packages/evershop/src/modules/catalog/pages/frontStore/homepage/FeaturedProducts.jsx +++ b/packages/evershop/src/modules/catalog/pages/frontStore/homepage/FeaturedProducts.jsx @@ -59,7 +59,7 @@ export const query = ` collection (code: "homepage") { collectionId name - products (filters: [{key: "limit", operation: "=", value: "4"}]) { + products (filters: [{key: "limit", operation: eq, value: "4"}]) { items { productId name diff --git a/packages/evershop/src/modules/catalog/services/AttributeCollection.js b/packages/evershop/src/modules/catalog/services/AttributeCollection.js new file mode 100644 index 000000000..8415dc8b5 --- /dev/null +++ b/packages/evershop/src/modules/catalog/services/AttributeCollection.js @@ -0,0 +1,59 @@ +const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); + +class AttributeCollection { + constructor(baseQuery) { + this.baseQuery = baseQuery; + } + + async init(filters = []) { + const currentFilters = []; + + // Apply the filters + const attributeCollectionFilters = await getValue( + 'attributeCollectionFilters', + [] + ); + + attributeCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } + } + }); + + // Clone the main query for getting total right before doing the paging + const totalQuery = this.baseQuery.clone(); + totalQuery.select('COUNT(attribute.attribute_id)', 'total'); + totalQuery.removeOrderBy(); + totalQuery.removeLimit(); + + this.currentFilters = currentFilters; + this.totalQuery = totalQuery; + } + + async items() { + const items = await this.baseQuery.execute(pool); + return items.map((row) => camelCase(row)); + } + + async total() { + // Call items to get the total + const total = await this.totalQuery.execute(pool); + return total[0].total; + } + + currentFilters() { + return this.currentFilters; + } +} + +module.exports.AttributeCollection = AttributeCollection; diff --git a/packages/evershop/src/modules/catalog/services/AttributeGroupCollection.js b/packages/evershop/src/modules/catalog/services/AttributeGroupCollection.js new file mode 100644 index 000000000..ce890b39f --- /dev/null +++ b/packages/evershop/src/modules/catalog/services/AttributeGroupCollection.js @@ -0,0 +1,100 @@ +const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { + getValue, + getValueSync +} = require('@evershop/evershop/src/lib/util/registry'); +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); + +class AttributeGroupCollection { + constructor(baseQuery) { + this.baseQuery = baseQuery; + } + + async init(filters = []) { + const currentFilters = []; + const defaultFilters = [ + { + key: 'name', + operation: ['eq', 'like', 'nlike'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'attribute_group.group_name', + OPERATION_MAP[operation], + value + ); + currentFilters.push({ + key: 'name', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const attributeGroupsSortBy = getValueSync('attributeGroupsSortBy', { + name: (query) => query.orderBy('attribute_group.group_name') + }); + + if (attributeGroupsSortBy[value]) { + attributeGroupsSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } + } + } + ]; + // Apply the filters + const attributeGroupCollectionFilters = await getValue( + 'attributeGroupCollectionFilters', + defaultFilters + ); + + attributeGroupCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } + } + }); + + // Clone the main query for getting total right before doing the paging + const totalQuery = this.baseQuery.clone(); + totalQuery.select('COUNT(attribute_group.attribute_group_id)', 'total'); + totalQuery.removeOrderBy(); + totalQuery.removeLimit(); + + this.currentFilters = currentFilters; + this.totalQuery = totalQuery; + } + + async items() { + const items = await this.baseQuery.execute(pool); + return items.map((row) => camelCase(row)); + } + + async total() { + // Call items to get the total + const total = await this.totalQuery.execute(pool); + return total[0].total; + } + + currentFilters() { + return this.currentFilters; + } +} + +module.exports.AttributeGroupCollection = AttributeGroupCollection; diff --git a/packages/evershop/src/modules/catalog/services/CategoryCollection.js b/packages/evershop/src/modules/catalog/services/CategoryCollection.js index 552425e74..76185517f 100644 --- a/packages/evershop/src/modules/catalog/services/CategoryCollection.js +++ b/packages/evershop/src/modules/catalog/services/CategoryCollection.js @@ -1,105 +1,48 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); class CategoryCollection { constructor(baseQuery) { this.baseQuery = baseQuery; + this.baseQuery.orderBy('category.category_id', 'DESC'); } - async init(args, { filters = [] }, { user }) { - if (!user) { + async init(filters = [], isAdmin = false) { + if (!isAdmin) { this.baseQuery.andWhere('category.status', '=', 1); } const currentFilters = []; - // Name filter - const nameFilter = filters.find((f) => f.key === 'name'); - if (nameFilter) { - this.baseQuery.andWhere( - 'category_description.name', - 'ILIKE', - `%${nameFilter.value}%` - ); - currentFilters.push({ - key: 'name', - operation: '=', - value: nameFilter.value - }); - } - - // Status filter - const statusFilter = filters.find((f) => f.key === 'status'); - if (statusFilter) { - this.baseQuery.andWhere('category.status', '=', statusFilter.value); - currentFilters.push({ - key: 'status', - operation: '=', - value: statusFilter.value - }); - } - - // includeInNav filter - const includeInNav = filters.find((f) => f.key === 'includeInNav'); - if (includeInNav) { - this.baseQuery.andWhere( - 'category.include_in_nav', - '=', - includeInNav.value - ); - currentFilters.push({ - key: 'includeInNav', - operation: '=', - value: includeInNav.value - }); - } + // Apply the filters + const categoryCollectionFilters = await getValue( + 'categoryCollectionFilters', + [], + { + isAdmin + } + ); - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => - f.key === 'sortOrder' && - ['ASC', 'DESC', 'asc', 'desc'].includes(f.value) - ) || { value: 'DESC' }; - if (sortBy && sortBy.value === 'name') { - this.baseQuery.orderBy('category_description.name', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - this.baseQuery.orderBy('category.category_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } + categoryCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } + } + }); // Clone the main query for getting total right before doing the paging const totalQuery = this.baseQuery.clone(); totalQuery.select('COUNT(category.category_id)', 'total'); totalQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - this.baseQuery.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); + totalQuery.removeLimit(); + this.currentFilters = currentFilters; this.totalQuery = totalQuery; } diff --git a/packages/evershop/src/modules/catalog/services/CollectionCollection.js b/packages/evershop/src/modules/catalog/services/CollectionCollection.js index 57d8f1b9f..777f57eab 100644 --- a/packages/evershop/src/modules/catalog/services/CollectionCollection.js +++ b/packages/evershop/src/modules/catalog/services/CollectionCollection.js @@ -1,90 +1,42 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); class CollectionCollection { constructor(baseQuery) { this.baseQuery = baseQuery; + this.baseQuery.orderBy('collection.collection_id', 'DESC'); } - async init(args, { filters = [] }) { + async init(filters = []) { const currentFilters = []; - // Name filter - const nameFilter = filters.find((f) => f.key === 'name'); - if (nameFilter) { - this.baseQuery.andWhere( - 'collection.name', - 'ILIKE', - `%${nameFilter.value}%` - ); - currentFilters.push({ - key: 'name', - operation: '=', - value: nameFilter.value - }); - } - // Code filter - const codeFilter = filters.find((f) => f.key === 'code'); - if (codeFilter) { - this.baseQuery.andWhere( - 'collection.code', - 'ILIKE', - `%${codeFilter.value}%` - ); - currentFilters.push({ - key: 'code', - operation: '=', - value: codeFilter.value - }); - } + // Apply the filters + const collectionCollectionFilters = await getValue( + 'collectionCollectionFilters', + [] + ); + + collectionCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } + } + }); - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => - f.key === 'sortOrder' && - ['ASC', 'DESC', 'asc', 'desc'].includes(f.value) - ) || { value: 'DESC' }; - if (sortBy && sortBy.value === 'name') { - this.baseQuery.orderBy('collection.name', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - this.baseQuery.orderBy('collection.collection_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } // Clone the main query for getting total right before doing the paging const totalQuery = this.baseQuery.clone(); - totalQuery.removeOrderBy(); totalQuery.select('COUNT(collection.collection_id)', 'total'); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); + totalQuery.removeOrderBy(); + totalQuery.removeLimit(); - this.baseQuery.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); this.currentFilters = currentFilters; this.totalQuery = totalQuery; } diff --git a/packages/evershop/src/modules/catalog/services/ProductCollection.js b/packages/evershop/src/modules/catalog/services/ProductCollection.js index 5ef4ac63b..42910b721 100644 --- a/packages/evershop/src/modules/catalog/services/ProductCollection.js +++ b/packages/evershop/src/modules/catalog/services/ProductCollection.js @@ -1,16 +1,24 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); -const uniqid = require('uniqid'); -const { select, value, node } = require('@evershop/postgres-query-builder'); + +const { select, node, sql } = require('@evershop/postgres-query-builder'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); class ProductCollection { constructor(baseQuery) { this.baseQuery = baseQuery; + this.baseQuery.orderBy('product.product_id', 'DESC'); } - async init(args, { filters = [] }, { user }) { - if (!user) { + /** + * + * @param {{key: String, operation: String, value: String}[]} filters + * @param {boolean} isAdmin + */ + async init(filters = [], isAdmin = false) { + // If the user is not admin, we need to filter out the out of stock products and the disabled products + if (!isAdmin) { this.baseQuery.andWhere('product.status', '=', 1); if (getConfig('catalog.showOutOfStockProduct', false) === false) { this.baseQuery @@ -23,210 +31,37 @@ class ProductCollection { } } const currentFilters = []; - // Keyword filter - const keywordFilter = filters.find((f) => f.key === 'keyword'); - if (keywordFilter) { - const where = this.baseQuery.getWhere(); - const bindingKey = `keyword_${uniqid()}`; - where.addRaw( - 'AND', - `to_tsvector('simple', product_description.name || ' ' || product_description.description) @@ websearch_to_tsquery('simple', :${bindingKey})`, - { - [bindingKey]: keywordFilter.value - } - ); - currentFilters.push({ - key: 'keyword', - operation: '=', - value: keywordFilter.value - }); - } - - // Price filter - const minPrice = filters.find((f) => f.key === 'minPrice'); - const maxPrice = filters.find((f) => f.key === 'maxPrice'); - if (minPrice && Number.isNaN(parseFloat(minPrice.value)) === false) { - this.baseQuery.andWhere('product.price', '>=', minPrice.value); - currentFilters.push({ - key: 'minPrice', - operation: '=', - value: minPrice.value - }); - } - if (maxPrice && Number.isNaN(parseFloat(maxPrice.value)) === false) { - this.baseQuery.andWhere('product.price', '<=', maxPrice.value); - currentFilters.push({ - key: 'maxPrice', - operation: '=', - value: maxPrice.value - }); - } - - // Name filter - const nameFilter = filters.find((f) => f.key === 'name'); - if (nameFilter) { - this.baseQuery.andWhere( - 'product_description.name', - 'ILIKE', - `%${nameFilter.value}%` - ); - currentFilters.push({ - key: 'name', - operation: '=', - value: nameFilter.value - }); - } - - // Qty filter - const qtyFilter = filters.find((f) => f.key === 'qty'); - if (qtyFilter) { - const [min, max] = qtyFilter.value.split('-').map((v) => parseFloat(v)); - let currentQtyFilter; - if (Number.isNaN(min) === false) { - this.baseQuery.andWhere('product_inventory.qty', '>=', min); - currentQtyFilter = { key: 'qty', operation: '=', value: `${min}` }; - } - - if (Number.isNaN(max) === false) { - this.baseQuery.andWhere('product_inventory.qty', '<=', max); - currentQtyFilter = { - key: 'qty', - operation: '=', - value: `${currentQtyFilter.value}-${max}` - }; - } - if (currentQtyFilter) { - currentFilters.push(currentQtyFilter); - } - } - - // Sku filter - const skuFilter = filters.find((f) => f.key === 'sku'); - if (skuFilter) { - // Support like, equal and IN - if (['LIKE', 'like'].includes(skuFilter.operation)) { - this.baseQuery.andWhere('product.sku', 'ILIKE', `%${skuFilter.value}%`); - currentFilters.push({ - key: 'sku', - operation: 'like', - value: skuFilter.value - }); - } else if (['IN', 'in'].includes(skuFilter.operation)) { - const values = skuFilter.value - .split(',') - .map((v) => v.trim()) - .filter((v) => v.length > 0); - if (values.length > 0) { - this.baseQuery.andWhere('product.sku', 'IN', values); - currentFilters.push({ - key: 'sku', - operation: 'in', - value: values.join(',') - }); - } - } else { - this.baseQuery.andWhere('product.sku', '=', skuFilter.value); - currentFilters.push({ - key: 'sku', - operation: '=', - value: skuFilter.value - }); - } - } - - // Status filter - const statusFilter = filters.find((f) => f.key === 'status'); - if (statusFilter) { - this.baseQuery.andWhere('product.status', '=', statusFilter.value); - currentFilters.push({ - key: 'status', - operation: '=', - value: statusFilter.value - }); - } - - // Apply category filters - const categoryFilter = filters.find((f) => f.key === 'cat'); - if (categoryFilter) { - const values = categoryFilter.value - .split(',') - .map((v) => parseInt(v, 10)) - .filter((v) => Number.isNaN(v) === false); - this.baseQuery.andWhere('product.category_id', 'IN', values); - - currentFilters.push({ - key: 'cat', - operation: '=', - value: values.join(',') - }); - } - // Attribute filter const filterableAttributes = await select() .from('attribute') .where('type', '=', 'select') .and('is_filterable', '=', 1) .execute(pool); - // Attribute filters - filters.forEach((filter) => { - const attribute = filterableAttributes.find( - (a) => a.attribute_code === filter.key - ); - if (!attribute) { - return; + // Apply the filters + const productCollectionFilters = await getValue( + 'productCollectionFilters', + [], + { + isAdmin, + filterableAttributes } + ); - const values = filter.value - .split(',') - .map((v) => parseInt(v, 10)) - .filter((v) => Number.isNaN(v) === false); - if (values.length > 0) { - const alias = `attribute_${uniqid()}`; - this.baseQuery - .innerJoin('product_attribute_value_index', alias) - .on(`${alias}.product_id`, '=', 'product.product_id') - .and(`${alias}.attribute_id`, '=', value(attribute.attribute_id)) - .and(`${alias}.option_id`, 'IN', value(values)); + productCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } } - currentFilters.push({ - key: filter.key, - operation: filter.operation, - value: values.join(',') - }); }); - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => - f.key === 'sortOrder' && - ['ASC', 'DESC', 'asc', 'desc'].includes(f.value) - ) || { value: 'DESC' }; - if (sortBy && sortBy.value === 'price') { - this.baseQuery.orderBy('product.price', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else if (sortBy && sortBy.value === 'name') { - this.baseQuery.orderBy('product_description.name`', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - this.baseQuery.orderBy('product.product_id', sortOrder.value); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } - - if (!user) { + if (!isAdmin) { // Visibility. For variant group const copy = this.baseQuery.clone(); // Get all group that have at lease 1 item visibile @@ -261,31 +96,30 @@ class ProductCollection { } else { this.baseQuery.andWhere('product.visibility', '=', 't'); } + } else { + const onePerVariantGroupQuery = this.baseQuery.clone(); + onePerVariantGroupQuery.removeLimit(); + onePerVariantGroupQuery.select( + sql( + 'DISTINCT ON (COALESCE(product.variant_group_id, random())) product.product_id', + 'product_id' + ) + ); + onePerVariantGroupQuery.removeOrderBy(); + const onePerGroup = await onePerVariantGroupQuery.execute(pool); + this.baseQuery.andWhere( + 'product.product_id', + 'IN', + onePerGroup.map((v) => v.product_id) + ); } // Clone the main query for getting total right before doing the paging const totalQuery = this.baseQuery.clone(); totalQuery.select('COUNT(product.product_id)', 'total'); totalQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - this.baseQuery.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); + totalQuery.removeLimit(); + this.currentFilters = currentFilters; this.totalQuery = totalQuery; } diff --git a/packages/evershop/src/modules/catalog/services/getAttributeGroupsBaseQuery.js b/packages/evershop/src/modules/catalog/services/getAttributeGroupsBaseQuery.js new file mode 100644 index 000000000..8c12adff0 --- /dev/null +++ b/packages/evershop/src/modules/catalog/services/getAttributeGroupsBaseQuery.js @@ -0,0 +1,3 @@ +const { select } = require('@evershop/postgres-query-builder'); + +module.exports.getAttributeGroupsBaseQuery = () => select().from('attribute_group'); diff --git a/packages/evershop/src/modules/catalog/services/getAttributesBaseQuery.js b/packages/evershop/src/modules/catalog/services/getAttributesBaseQuery.js new file mode 100644 index 000000000..653b29a87 --- /dev/null +++ b/packages/evershop/src/modules/catalog/services/getAttributesBaseQuery.js @@ -0,0 +1,3 @@ +const { select } = require('@evershop/postgres-query-builder'); + +module.exports.getAttributesBaseQuery = () => select().from('attribute'); diff --git a/packages/evershop/src/modules/catalog/services/getCollectionsBaseQuery.js b/packages/evershop/src/modules/catalog/services/getCollectionsBaseQuery.js index bd9761a5b..57fba52ea 100644 --- a/packages/evershop/src/modules/catalog/services/getCollectionsBaseQuery.js +++ b/packages/evershop/src/modules/catalog/services/getCollectionsBaseQuery.js @@ -2,6 +2,5 @@ const { select } = require('@evershop/postgres-query-builder'); module.exports.getCollectionsBaseQuery = () => { const query = select().from('collection'); - return query; }; diff --git a/packages/evershop/src/modules/catalog/services/registerDefaultAttributeCollectionFilters.js b/packages/evershop/src/modules/catalog/services/registerDefaultAttributeCollectionFilters.js new file mode 100644 index 000000000..9860b9117 --- /dev/null +++ b/packages/evershop/src/modules/catalog/services/registerDefaultAttributeCollectionFilters.js @@ -0,0 +1,133 @@ +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultAttributeCollectionFilters() { + // List of default supported filters + const defaultFilters = [ + { + key: 'name', + operation: ['like', 'nlike'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'attribute.attribute_name', + OPERATION_MAP[operation], + `%${value}%` + ); + currentFilters.push({ + key: 'name', + operation, + value + }); + } + }, + { + key: 'code', + operation: ['eq', 'like', 'nlike'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'attribute.attribute_code', + OPERATION_MAP[operation], + value + ); + currentFilters.push({ + key: 'code', + operation, + value + }); + } + }, + { + key: 'group', + operation: ['in', 'eq'], + callback: (query, operation, value, currentFilters) => { + query + .innerJoin('attribute_group_link') + .on( + 'attribute.attribute_id', + '=', + 'attribute_group_link.attribute_id' + ); + query.andWhere( + 'attribute_group_link.group_id', + OPERATION_MAP[operation], + value + ); + currentFilters.push({ + key: 'code', + operation, + value + }); + } + }, + { + key: 'type', + operation: ['eq', 'neq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere('attribute.type', OPERATION_MAP[operation], value); + currentFilters.push({ + key: 'code', + operation, + value + }); + } + }, + { + key: 'is_required', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'attribute.is_required', + OPERATION_MAP[operation], + value + ); + currentFilters.push({ + key: 'is_required', + operation, + value + }); + } + }, + { + key: 'is_filterable', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'attribute.is_filterable', + OPERATION_MAP[operation], + value + ); + currentFilters.push({ + key: 'is_filterable', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const attributeCollectionSortBy = getValueSync( + 'attributeCollectionSortBy', + { + name: (query) => query.orderBy('attribute.name'), + type: (query) => query.orderBy('attribute.type') + } + ); + + if (attributeCollectionSortBy[value]) { + attributeCollectionSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } + } + } + ]; + + return defaultFilters; +}; diff --git a/packages/evershop/src/modules/catalog/services/registerDefaultCategoryCollectionFilters.js b/packages/evershop/src/modules/catalog/services/registerDefaultCategoryCollectionFilters.js new file mode 100644 index 000000000..b59c84b2e --- /dev/null +++ b/packages/evershop/src/modules/catalog/services/registerDefaultCategoryCollectionFilters.js @@ -0,0 +1,85 @@ +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultCategoryCollectionFilters() { + const { isAdmin } = this; + // List of default supported filters + const defaultFilters = [ + { + key: 'name', + operation: ['like'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'category_description.name', + OPERATION_MAP[operation], + `%${value}%` + ); + currentFilters.push({ + key: 'name', + operation, + value + }); + } + }, + { + key: 'status', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere('category.status', OPERATION_MAP[operation], value); + currentFilters.push({ + key: 'status', + operation, + value + }); + } + }, + { + key: 'include_in_nav', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'category.include_in_nav', + OPERATION_MAP[operation], + value + ); + currentFilters.push({ + key: 'include_in_nav', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const categorySortBy = getValueSync( + 'categoryCollectionSortBy', + { + name: (query) => query.orderBy('category_description.name'), + include_in_nav: (query) => query.orderBy('category.include_in_nav'), + status: (query) => query.orderBy('category.status') + }, + { + isAdmin + } + ); + + if (categorySortBy[value]) { + categorySortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } else { + query.orderBy('category.category_id', 'DESC'); + } + } + } + ]; + + return defaultFilters; +}; diff --git a/packages/evershop/src/modules/catalog/services/registerDefaultCollectionCollectionFilters.js b/packages/evershop/src/modules/catalog/services/registerDefaultCollectionCollectionFilters.js new file mode 100644 index 000000000..f2e7c757d --- /dev/null +++ b/packages/evershop/src/modules/catalog/services/registerDefaultCollectionCollectionFilters.js @@ -0,0 +1,65 @@ +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultCollectionCollectionFilters() { + // List of default supported filters + const defaultFilters = [ + { + key: 'name', + operation: ['like'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'collection.name', + OPERATION_MAP[operation], + `%${value}%` + ); + currentFilters.push({ + key: 'name', + operation, + value + }); + } + }, + { + key: 'code', + operation: ['like', 'eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'collection.code', + OPERATION_MAP[operation], + `%${value}%` + ); + currentFilters.push({ + key: 'code', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const collectionSortBy = getValueSync('collectionCollectionSortBy', { + name: (query) => query.orderBy('collection.name'), + code: (query) => query.orderBy('collection.code') + }); + + if (collectionSortBy[value]) { + collectionSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } else { + query.orderBy('collection.collection_id', 'DESC'); + } + } + } + ]; + + return defaultFilters; +}; diff --git a/packages/evershop/src/modules/catalog/services/registerDefaultProductCollectionFilters.js b/packages/evershop/src/modules/catalog/services/registerDefaultProductCollectionFilters.js new file mode 100644 index 000000000..5281cd21c --- /dev/null +++ b/packages/evershop/src/modules/catalog/services/registerDefaultProductCollectionFilters.js @@ -0,0 +1,198 @@ +const uniqid = require('uniqid'); +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultProductCollectionFilters() { + // List of default supported filters + const defaultFilters = [ + { + key: 'keyword', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const where = query.getWhere(); + const bindingKey = `keyword_${uniqid()}`; + where.addRaw( + 'AND', + `to_tsvector('simple', product_description.name || ' ' || product_description.description) @@ websearch_to_tsquery('simple', :${bindingKey})`, + { + [bindingKey]: value + } + ); + currentFilters.push({ + key: 'keyword', + operation, + value + }); + } + }, + { + key: 'minPrice', + operation: ['gteq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'product.price', + OPERATION_MAP[operation], + parseFloat(value) || 0 + ); + currentFilters.push({ + key: 'minPrice', + operation, + value + }); + } + }, + { + key: 'maxPrice', + operation: ['lteq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'product.price', + OPERATION_MAP[operation], + parseFloat(value) || 9999999999 + ); + currentFilters.push({ + key: 'maxPrice', + operation, + value + }); + } + }, + { + key: 'name', + operation: ['like'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'product_description.name', + OPERATION_MAP[operation], + `%${value}%` + ); + currentFilters.push({ + key: 'name', + operation, + value + }); + } + }, + { + key: 'qty', + operation: ['eq', 'gteq', 'lteq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'product_inventory.qty', + OPERATION_MAP[operation], + parseFloat(value) || 0 + ); + currentFilters.push({ + key: 'qty', + operation, + value + }); + } + }, + { + key: 'sku', + operation: ['like', 'in'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'product.sku', + OPERATION_MAP[operation], + value.split(',') + ); + currentFilters.push({ + key: 'sku', + operation, + value + }); + } + }, + { + key: 'status', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere('product.status', OPERATION_MAP[operation], value); + currentFilters.push({ + key: 'status', + operation, + value + }); + } + }, + { + key: 'cat', + operation: ['eq', 'in', 'nin'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'product.category_id', + OPERATION_MAP[operation], + ['in', 'nin'].includes(operation) ? value.split(',') : value + ); + currentFilters.push({ + key: 'cat', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const productSortBy = getValueSync( + 'productCollectionSortBy', + { + price: (query) => query.orderBy('product.price'), + name: (query) => query.orderBy('product_description.name'), + qty: (query) => query.orderBy('product_inventory.qty'), + status: (query) => query.orderBy('product.status') + }, + { + isAdmin + } + ); + + if (productSortBy[value]) { + productSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } else { + query.orderBy('product.product_id', 'DESC'); + } + } + } + ]; + + const {filterableAttributes} = this; + const {isAdmin} = this; + // Attribute filters + filterableAttributes.forEach((attribute) => { + defaultFilters.push({ + key: attribute.attribute_code, + operation: ['in'], + callback: (query, operation, value, currentFilters) => { + const alias = `attribute_${uniqid()}`; + // Split the value by comma and only get the positive integer + const values = value + .split(',') + .map((v) => parseInt(v, 10)) + .filter((v) => v > 0); + query + .innerJoin('product_attribute_value_index', alias) + .on(`${alias}.product_id`, '=', 'product.product_id') + .and(`${alias}.attribute_id`, '=', value(attribute.attribute_id)) + .and(`${alias}.option_id`, 'IN', values); + currentFilters.push({ + key: attribute.attribute_code, + operation, + values + }); + } + }); + }); + + return defaultFilters; +}; diff --git a/packages/evershop/src/modules/cms/bootstrap.js b/packages/evershop/src/modules/cms/bootstrap.js index ae9c0a4d2..c819f7b74 100644 --- a/packages/evershop/src/modules/cms/bootstrap.js +++ b/packages/evershop/src/modules/cms/bootstrap.js @@ -1,4 +1,9 @@ const config = require('config'); +const registerDefaultPageCollectionFilters = require('./services/registerDefaultPageCollectionFilters'); +const { + defaultPaginationFilters +} = require('../../lib/util/defaultPaginationFilters'); +const { addProcessor } = require('../../lib/util/registry'); module.exports = () => { const themeConfig = { @@ -20,4 +25,16 @@ module.exports = () => { config.util.setModuleDefaults('system', { file_storage: 'local' }); + + // Reigtering the default filters for attribute collection + addProcessor( + 'cmsPageCollectionFilters', + registerDefaultPageCollectionFilters, + 1 + ); + addProcessor( + 'cmsPageCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); }; diff --git a/packages/evershop/src/modules/cms/graphql/types/CmsPage/CmsPage.resolvers.js b/packages/evershop/src/modules/cms/graphql/types/CmsPage/CmsPage.resolvers.js index aec0fabbe..7bbcec932 100644 --- a/packages/evershop/src/modules/cms/graphql/types/CmsPage/CmsPage.resolvers.js +++ b/packages/evershop/src/modules/cms/graphql/types/CmsPage/CmsPage.resolvers.js @@ -1,4 +1,3 @@ -const { select } = require('@evershop/postgres-query-builder'); const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { @@ -9,23 +8,15 @@ const { CMSPageCollection } = require('../../../services/CMSPageCollection'); module.exports = { Query: { cmsPage: async (root, { id }, { pool }) => { - const query = select().from('cms_page'); - query - .leftJoin('cms_page_description') - .on( - 'cms_page.cms_page_id', - '=', - 'cms_page_description.cms_page_description_cms_page_id' - ); + const query = getCmsPagesBaseQuery(); query.where('cms_page_id', '=', id); - const page = await query.load(pool); return page ? camelCase(page) : null; }, cmsPages: async (_, { filters = [] }, { user }) => { const query = getCmsPagesBaseQuery(); const root = new CMSPageCollection(query); - await root.init({}, { filters }, { user }); + await root.init(filters, !!user); return root; } }, diff --git a/packages/evershop/src/modules/cms/pages/admin/all/Layout.scss b/packages/evershop/src/modules/cms/pages/admin/all/Layout.scss index 60bc89645..867c2e192 100644 --- a/packages/evershop/src/modules/cms/pages/admin/all/Layout.scss +++ b/packages/evershop/src/modules/cms/pages/admin/all/Layout.scss @@ -47,8 +47,9 @@ table { } th { border: 0; - padding-top: 2rem; - padding-bottom: 2rem; + padding-top: 1.5rem; + padding-bottom: 1.5rem; + align-content: center; } } } diff --git a/packages/evershop/src/modules/cms/pages/admin/cmsPageGrid/Grid.jsx b/packages/evershop/src/modules/cms/pages/admin/cmsPageGrid/Grid.jsx index 9bef6a819..1eb6cc85b 100644 --- a/packages/evershop/src/modules/cms/pages/admin/cmsPageGrid/Grid.jsx +++ b/packages/evershop/src/modules/cms/pages/admin/cmsPageGrid/Grid.jsx @@ -7,10 +7,11 @@ import { useAlertContext } from '@components/common/modal/Alert'; import { Checkbox } from '@components/common/form/fields/Checkbox'; import { Card } from '@components/admin/cms/Card'; import Area from '@components/common/Area'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; -import StatusColumnHeader from '@components/common/grid/headers/Status'; import StatusRow from '@components/common/grid/rows/StatusRow'; import PageName from '@components/admin/cms/cmsPageGrid/rows/PageName'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; +import SortableHeader from '@components/common/grid/headers/Sortable'; function Actions({ pages = [], selectedIds = [] }) { const { openAlert, closeAlert } = useAlertContext(); @@ -164,6 +165,60 @@ export default function CMSPageGrid({ return ( + + ( + f.key === 'name')?.value + } + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const name = document.getElementById('name')?.value; + if (name) { + url.searchParams.set('name[operation]', 'like'); + url.searchParams.set('name[value]', name); + } else { + url.searchParams.delete('name[operation]'); + url.searchParams.delete('name[value]'); + } + window.location.href = url; + } + }} + /> + ) + }, + sortOrder: 10 + } + ]} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => { + // Just get the url and remove all query params + const url = new URL(document.location); + url.search = ''; + window.location.href = url.href; + } + } + ]} + />
+
+
+ Thumbnail +
+
+
@@ -186,9 +241,9 @@ export default function CMSPageGrid({ { component: { default: () => ( - ) @@ -198,12 +253,10 @@ export default function CMSPageGrid({ { component: { default: () => ( - f.key === 'status' - )} + name="status" + currentFilters={currentFilters} /> ) }, diff --git a/packages/evershop/src/modules/cms/pages/admin/cmsPageGrid/index.js b/packages/evershop/src/modules/cms/pages/admin/cmsPageGrid/index.js index 8ded173d2..a385f0efa 100644 --- a/packages/evershop/src/modules/cms/pages/admin/cmsPageGrid/index.js +++ b/packages/evershop/src/modules/cms/pages/admin/cmsPageGrid/index.js @@ -11,6 +11,5 @@ module.exports = (request, response) => { title: 'Cms pages', description: 'Cms pages' }); - const { query } = request; - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/evershop/src/modules/cms/services/CMSPageCollection.js b/packages/evershop/src/modules/cms/services/CMSPageCollection.js index ff7c9cac8..99a90ef27 100644 --- a/packages/evershop/src/modules/cms/services/CMSPageCollection.js +++ b/packages/evershop/src/modules/cms/services/CMSPageCollection.js @@ -1,91 +1,44 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); class CMSPageCollection { constructor(baseQuery) { this.baseQuery = baseQuery; } - async init(args, { filters = [] }, { user }) { - if (!user) { + async init(filters = [], isAdmin = false) { + if (!isAdmin) { this.baseQuery.andWhere('cms_page.status', '=', 't'); } const currentFilters = []; - // Name filter - const nameFilter = filters.find((f) => f.key === 'name'); - if (nameFilter) { - this.baseQuery.andWhere( - 'cms_page_description.name', - 'ILIKE', - `%${nameFilter.value}%` - ); - currentFilters.push({ - key: 'name', - operation: '=', - value: nameFilter.value - }); - } - - // Status filter - const statusFilter = filters.find((f) => f.key === 'status'); - if (statusFilter) { - this.baseQuery.andWhere('cms_page.status', '=', statusFilter.value); - currentFilters.push({ - key: 'status', - operation: '=', - value: statusFilter.value - }); - } - - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => - f.key === 'sortOrder' && - ['ASC', 'DESC', 'asc', 'desc'].includes(f.value) - ) || { value: 'DESC' }; + // Apply the filters + const cmsPageCollectionFilters = await getValue( + 'cmsPageCollectionFilters', + [] + ); - if (sortBy && sortBy.value === 'name') { - this.baseQuery.orderBy('cms_page_description.name', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - this.baseQuery.orderBy('cms_page.cms_page_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } + cmsPageCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } + } + }); // Clone the main query for getting total right before doing the paging const totalQuery = this.baseQuery.clone(); totalQuery.select('COUNT(cms_page.cms_page_id)', 'total'); totalQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - this.baseQuery.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); + totalQuery.removeLimit(); + this.currentFilters = currentFilters; this.totalQuery = totalQuery; } diff --git a/packages/evershop/src/modules/cms/services/registerDefaultPageCollectionFilters.js b/packages/evershop/src/modules/cms/services/registerDefaultPageCollectionFilters.js new file mode 100644 index 000000000..bf163c305 --- /dev/null +++ b/packages/evershop/src/modules/cms/services/registerDefaultPageCollectionFilters.js @@ -0,0 +1,62 @@ +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultPageCollectionFilters() { + // List of default supported filters + const defaultFilters = [ + { + key: 'name', + operation: ['eq', 'like'], + callback: (query, operation, value, currentFilters) => { + if (operation === 'eq') { + query.andWhere('cms_page_description.name', '=', value); + } else { + query.andWhere('cms_page_description.name', 'ilike', `%${value}%`); + } + currentFilters.push({ + key: 'name', + operation, + value + }); + } + }, + { + key: 'status', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere('cms_page.status', OPERATION_MAP[operation], value); + currentFilters.push({ + key: 'status', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const cmsPageCollectionSortBy = getValueSync( + 'cmsPageCollectionSortBy', + { + name: (query) => query.orderBy('cms_page_description.name'), + status: (query) => query.orderBy('cms_page.status') + } + ); + + if (cmsPageCollectionSortBy[value]) { + cmsPageCollectionSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } + } + } + ]; + + return defaultFilters; +}; diff --git a/packages/evershop/src/modules/customer/bootstrap.js b/packages/evershop/src/modules/customer/bootstrap.js index 8fd5f3e9a..c0acd36df 100644 --- a/packages/evershop/src/modules/customer/bootstrap.js +++ b/packages/evershop/src/modules/customer/bootstrap.js @@ -5,6 +5,10 @@ const { select } = require('@evershop/postgres-query-builder'); const { comparePassword } = require('../../lib/util/passwordHelper'); const { translate } = require('../../lib/locale/translate/translate'); const { addProcessor } = require('../../lib/util/registry'); +const registerDefaultCustomerCollectionFilters = require('./services/registerDefaultCustomerCollectionFilters'); +const { + defaultPaginationFilters +} = require('../../lib/util/defaultPaginationFilters'); module.exports = () => { addProcessor('cartFields', (fields) => { @@ -155,4 +159,16 @@ module.exports = () => { } }; config.util.setModuleDefaults('customer', customerConfig); + + // Reigtering the default filters for attribute collection + addProcessor( + 'customerCollectionFilters', + registerDefaultCustomerCollectionFilters, + 1 + ); + addProcessor( + 'customerCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); }; diff --git a/packages/evershop/src/modules/customer/graphql/types/Customer/Customer.admin.resolvers.js b/packages/evershop/src/modules/customer/graphql/types/Customer/Customer.admin.resolvers.js index 231a99203..1132de6b2 100644 --- a/packages/evershop/src/modules/customer/graphql/types/Customer/Customer.admin.resolvers.js +++ b/packages/evershop/src/modules/customer/graphql/types/Customer/Customer.admin.resolvers.js @@ -1,4 +1,3 @@ -const { select } = require('@evershop/postgres-query-builder'); const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { @@ -9,16 +8,15 @@ const { CustomerCollection } = require('../../../services/CustomerCollection'); module.exports = { Query: { customer: async (root, { id }, { pool }) => { - const query = select().from('customer'); + const query = getCustomersBaseQuery(); query.where('uuid', '=', id); - const customer = await query.load(pool); return customer ? camelCase(customer) : null; }, customers: async (_, { filters = [] }) => { const query = getCustomersBaseQuery(); const root = new CustomerCollection(query); - await root.init({}, { filters }); + await root.init(filters); return root; } }, diff --git a/packages/evershop/src/modules/customer/pages/admin/customerGrid/Grid.jsx b/packages/evershop/src/modules/customer/pages/admin/customerGrid/Grid.jsx index 82b60a60d..3169bea39 100644 --- a/packages/evershop/src/modules/customer/pages/admin/customerGrid/Grid.jsx +++ b/packages/evershop/src/modules/customer/pages/admin/customerGrid/Grid.jsx @@ -7,11 +7,12 @@ import { Checkbox } from '@components/common/form/fields/Checkbox'; import { useAlertContext } from '@components/common/modal/Alert'; import StatusRow from '@components/common/grid/rows/StatusRow'; import BasicRow from '@components/common/grid/rows/BasicRow'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; -import DropdownColumnHeader from '@components/common/grid/headers/Dropdown'; import { Card } from '@components/admin/cms/Card'; import CustomerNameRow from '@components/admin/customer/customerGrid/rows/CustomerName'; import CreateAt from '@components/admin/customer/customerGrid/rows/CreateAt'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; +import SortableHeader from '@components/common/grid/headers/Sortable'; function Actions({ customers = [], selectedIds = [] }) { const { openAlert, closeAlert } = useAlertContext(); @@ -127,6 +128,59 @@ export default function CustomerGrid({ return ( + + ( + f.key === 'keyword')?.value + } + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const keyword = + document.getElementById('keyword')?.value; + if (keyword) { + url.searchParams.set('keyword', keyword); + } else { + url.searchParams.delete('keyword'); + } + window.location.href = url; + } + }} + /> + ) + }, + sortOrder: 10 + } + ]} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => { + // Just get the url and remove all query params + const url = new URL(document.location); + url.search = ''; + window.location.href = url.href; + } + } + ]} + />
@@ -147,9 +201,9 @@ export default function CustomerGrid({ // eslint-disable-next-line react/no-unstable-nested-components component: { default: () => ( - ) @@ -160,9 +214,9 @@ export default function CustomerGrid({ // eslint-disable-next-line react/no-unstable-nested-components component: { default: () => ( - ) @@ -173,14 +227,10 @@ export default function CustomerGrid({ // eslint-disable-next-line react/no-unstable-nested-components component: { default: () => ( - ) }, @@ -190,9 +240,9 @@ export default function CustomerGrid({ // eslint-disable-next-line react/no-unstable-nested-components component: { default: () => ( - ) diff --git a/packages/evershop/src/modules/customer/pages/admin/customerGrid/index.js b/packages/evershop/src/modules/customer/pages/admin/customerGrid/index.js index c8d85ec8f..5f3ec599f 100644 --- a/packages/evershop/src/modules/customer/pages/admin/customerGrid/index.js +++ b/packages/evershop/src/modules/customer/pages/admin/customerGrid/index.js @@ -11,6 +11,5 @@ module.exports = (request, response) => { title: 'Customers', description: 'Customers' }); - const { query } = request; - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/evershop/src/modules/customer/services/CustomerCollection.js b/packages/evershop/src/modules/customer/services/CustomerCollection.js index 17266cc44..7d8ed6190 100644 --- a/packages/evershop/src/modules/customer/services/CustomerCollection.js +++ b/packages/evershop/src/modules/customer/services/CustomerCollection.js @@ -1,123 +1,42 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); class CustomerCollection { constructor(baseQuery) { this.baseQuery = baseQuery; + this.baseQuery.orderBy('customer.customer_id', 'DESC'); } - async init(args, { filters = [] }) { + async init(filters = []) { const currentFilters = []; - // Name filter - const nameFilter = filters.find((f) => f.key === 'full_name'); - if (nameFilter) { - this.baseQuery.andWhere( - 'customer.full_name', - 'ILIKE', - `%${nameFilter.value}%` - ); - currentFilters.push({ - key: 'full_name', - operation: '=', - value: nameFilter.value - }); - } - - // Email filter - const emailFilter = filters.find((f) => f.key === 'email'); - if (emailFilter) { - this.baseQuery.andWhere( - 'customer.email', - 'ILIKE', - `%${emailFilter.value}%` - ); - currentFilters.push({ - key: 'email', - operation: '=', - value: emailFilter.value - }); - } - - // Keyword search - const keywordFilter = filters.find((f) => f.key === 'keyword'); - if (keywordFilter) { - this.baseQuery - .andWhere('customer.full_name', 'ILIKE', `%${keywordFilter.value}%`) - .or('customer.email', 'ILIKE', `%${keywordFilter.value}%`); - currentFilters.push({ - key: 'keyword', - operation: '=', - value: keywordFilter.value - }); - } - - // Status filter - const statusFilter = filters.find((f) => f.key === 'status'); - if (statusFilter) { - this.baseQuery.andWhere('customer.status', '=', statusFilter.value); - currentFilters.push({ - key: 'status', - operation: '=', - value: statusFilter.value - }); - } - - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => - f.key === 'sortOrder' && - ['ASC', 'DESC', 'asc', 'desc'].includes(f.value) - ) || { value: 'DESC' }; + // Apply the filters + const customerCollectionFilters = await getValue( + 'customerCollectionFilters', + [] + ); - if (sortBy && sortBy.value === 'full_name') { - this.baseQuery.orderBy('customer.full_name', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else if (sortBy && sortBy.value === 'email') { - this.baseQuery.orderBy('customer.email', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - this.baseQuery.orderBy('customer.customer_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } + customerCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } + } + }); // Clone the main query for getting total right before doing the paging const totalQuery = this.baseQuery.clone(); totalQuery.select('COUNT(customer.customer_id)', 'total'); totalQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - this.baseQuery.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); + totalQuery.removeLimit(); + this.currentFilters = currentFilters; this.totalQuery = totalQuery; } diff --git a/packages/evershop/src/modules/customer/services/registerDefaultCustomerCollectionFilters.js b/packages/evershop/src/modules/customer/services/registerDefaultCustomerCollectionFilters.js new file mode 100644 index 000000000..e928003f4 --- /dev/null +++ b/packages/evershop/src/modules/customer/services/registerDefaultCustomerCollectionFilters.js @@ -0,0 +1,90 @@ +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultCustomerCollectionFilters() { + // List of default supported filters + const defaultFilters = [ + { + key: 'keyword', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query + .andWhere('customer.full_name', 'ILIKE', `%${value}%`) + .or('customer.email', 'ILIKE', `%${value}%`); + currentFilters.push({ + key: 'keyword', + operation, + value + }); + } + }, + { + key: 'full_name', + operation: ['like', 'nlike'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'customer.full_name', + OPERATION_MAP[operation], + `%${value}%` + ); + currentFilters.push({ + key: 'full_name', + operation, + value + }); + } + }, + { + key: 'email', + operation: ['eq', 'like', 'nlike'], + callback: (query, operation, value, currentFilters) => { + query.andWhere('customer.email', OPERATION_MAP[operation], value); + currentFilters.push({ + key: 'email', + operation, + value + }); + } + }, + { + key: 'status', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere('customer.status', OPERATION_MAP[operation], value); + currentFilters.push({ + key: 'status', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const customerCollectionSortBy = getValueSync( + 'customerCollectionSortBy', + { + email: (query) => query.orderBy('customer.email'), + name: (query) => query.orderBy('customer.full_name'), + status: (query) => query.orderBy('customer.status'), + created_at: (query) => query.orderBy('customer.created_at') + } + ); + + if (customerCollectionSortBy[value]) { + customerCollectionSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } + } + } + ]; + + return defaultFilters; +}; diff --git a/packages/evershop/src/modules/graphql/services/graphqlMiddleware.js b/packages/evershop/src/modules/graphql/services/graphqlMiddleware.js index 9cde760ba..efb3a7258 100644 --- a/packages/evershop/src/modules/graphql/services/graphqlMiddleware.js +++ b/packages/evershop/src/modules/graphql/services/graphqlMiddleware.js @@ -38,7 +38,7 @@ module.exports.graphqlMiddleware = (schema) => }); if (data.errors) { // Create an Error instance with message and stack trace - next(new Error(data.errors[0].message)); + next(data.errors[0]); } else { response.status(OK).json({ data: data.data diff --git a/packages/evershop/src/modules/oms/bootstrap.js b/packages/evershop/src/modules/oms/bootstrap.js index a45e9587c..9704e4a42 100644 --- a/packages/evershop/src/modules/oms/bootstrap.js +++ b/packages/evershop/src/modules/oms/bootstrap.js @@ -1,4 +1,9 @@ const config = require('config'); +const registerDefaultOrderCollectionFilters = require('./services/registerDefaultOrderCollectionFilters'); +const { + defaultPaginationFilters +} = require('../../lib/util/defaultPaginationFilters'); +const { addProcessor } = require('../../lib/util/registry'); module.exports = () => { // Default order status and carriers configuration @@ -57,4 +62,16 @@ module.exports = () => { } }; config.util.setModuleDefaults('oms', orderStatusConfig); + + // Reigtering the default filters for attribute collection + addProcessor( + 'orderCollectionFilters', + registerDefaultOrderCollectionFilters, + 1 + ); + addProcessor( + 'orderCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); }; diff --git a/packages/evershop/src/modules/oms/graphql/types/Order/Order.admin.resolvers.js b/packages/evershop/src/modules/oms/graphql/types/Order/Order.admin.resolvers.js index d5c1351f0..1f66fe80b 100644 --- a/packages/evershop/src/modules/oms/graphql/types/Order/Order.admin.resolvers.js +++ b/packages/evershop/src/modules/oms/graphql/types/Order/Order.admin.resolvers.js @@ -8,7 +8,7 @@ module.exports = { orders: async (_, { filters = [] }) => { const query = getOrdersBaseQuery(); const root = new OrderCollection(query); - await root.init({}, { filters }); + await root.init(filters); return root; } }, diff --git a/packages/evershop/src/modules/oms/graphql/types/Order/Order.resolvers.js b/packages/evershop/src/modules/oms/graphql/types/Order/Order.resolvers.js index dc87f4084..97ff6aa50 100644 --- a/packages/evershop/src/modules/oms/graphql/types/Order/Order.resolvers.js +++ b/packages/evershop/src/modules/oms/graphql/types/Order/Order.resolvers.js @@ -2,11 +2,12 @@ const { select } = require('@evershop/postgres-query-builder'); const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); +const { getOrdersBaseQuery } = require('../../../services/getOrdersBaseQuery'); module.exports = { Query: { order: async (_, { uuid }, { pool }) => { - const query = select().from('order'); + const query = getOrdersBaseQuery(); query.where('uuid', '=', uuid); const order = await query.load(pool); if (!order) { diff --git a/packages/evershop/src/modules/oms/pages/admin/orderGrid/Grid.jsx b/packages/evershop/src/modules/oms/pages/admin/orderGrid/Grid.jsx index 6d56ffb55..9a7fdc9e7 100644 --- a/packages/evershop/src/modules/oms/pages/admin/orderGrid/Grid.jsx +++ b/packages/evershop/src/modules/oms/pages/admin/orderGrid/Grid.jsx @@ -7,16 +7,15 @@ import Pagination from '@components/common/grid/Pagination'; import { Checkbox } from '@components/common/form/fields/Checkbox'; import { useAlertContext } from '@components/common/modal/Alert'; import { Card } from '@components/admin/cms/Card'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; -import FromToColumnHeader from '@components/common/grid/headers/FromTo'; -import ShipmentStatusColumnHeader from '@components/admin/oms/orderGrid/headers/ShipmentStatusColumnHeader'; -import PaymentStatusColumnHeader from '@components/admin/oms/orderGrid/headers/PaymentStatusColumnHeader'; import OrderNumberRow from '@components/admin/oms/orderGrid/rows/OrderNumberRow'; import BasicRow from '@components/common/grid/rows/BasicRow'; import ShipmentStatusRow from '@components/admin/oms/orderGrid/rows/ShipmentStatus'; import PaymentStatusRow from '@components/admin/oms/orderGrid/rows/PaymentStatus'; import TotalRow from '@components/admin/oms/orderGrid/rows/TotalRow'; import CreateAt from '@components/admin/customer/customerGrid/rows/CreateAt'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; +import SortableHeader from '@components/common/grid/headers/Sortable'; function Actions({ orders = [], selectedIds = [] }) { const { openAlert, closeAlert } = useAlertContext(); @@ -103,9 +102,7 @@ Actions.propTypes = { }; export default function OrderGrid({ - orders: { items: orders, total, currentFilters = [] }, - shipmentStatusList, - paymentStatusList + orders: { items: orders, total, currentFilters = [] } }) { const page = currentFilters.find((filter) => filter.key === 'page') ? currentFilters.find((filter) => filter.key === 'page').value @@ -119,6 +116,59 @@ export default function OrderGrid({ return ( + + ( + f.key === 'keyword')?.value + } + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const keyword = + document.getElementById('keyword')?.value; + if (keyword) { + url.searchParams.set('keyword', keyword); + } else { + url.searchParams.delete('keyword'); + } + window.location.href = url; + } + }} + /> + ) + }, + sortOrder: 10 + } + ]} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => { + // Just get the url and remove all query params + const url = new URL(document.location); + url.search = ''; + window.location.href = url.href; + } + } + ]} + />
@@ -141,9 +191,9 @@ export default function OrderGrid({ { component: { default: () => ( - ) @@ -153,9 +203,9 @@ export default function OrderGrid({ { component: { default: () => ( - ) @@ -165,9 +215,9 @@ export default function OrderGrid({ { component: { default: () => ( - ) @@ -177,10 +227,9 @@ export default function OrderGrid({ { component: { default: () => ( - ) @@ -190,10 +239,9 @@ export default function OrderGrid({ { component: { default: () => ( - ) @@ -203,9 +251,9 @@ export default function OrderGrid({ { component: { default: () => ( - ) @@ -308,22 +356,6 @@ export default function OrderGrid({ } OrderGrid.propTypes = { - paymentStatusList: PropTypes.arrayOf( - PropTypes.shape({ - text: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - badge: PropTypes.string.isRequired, - progress: PropTypes.number.isRequired - }) - ).isRequired, - shipmentStatusList: PropTypes.arrayOf( - PropTypes.shape({ - text: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - badge: PropTypes.string.isRequired, - progress: PropTypes.number.isRequired - }) - ).isRequired, orders: PropTypes.shape({ items: PropTypes.arrayOf( PropTypes.shape({ @@ -417,18 +449,6 @@ export const query = ` value } } - shipmentStatusList { - text: name - value: code - badge - progress - } - paymentStatusList { - text: name - value: code - badge - progress - } } `; diff --git a/packages/evershop/src/modules/oms/pages/admin/orderGrid/index.js b/packages/evershop/src/modules/oms/pages/admin/orderGrid/index.js index 123d56523..e6c7fa72c 100644 --- a/packages/evershop/src/modules/oms/pages/admin/orderGrid/index.js +++ b/packages/evershop/src/modules/oms/pages/admin/orderGrid/index.js @@ -11,6 +11,5 @@ module.exports = (request, response) => { title: 'Orders', description: 'Orders' }); - const { query } = request; - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/evershop/src/modules/oms/services/OrderCollection.js b/packages/evershop/src/modules/oms/services/OrderCollection.js index 991b2ac51..9565d64a5 100644 --- a/packages/evershop/src/modules/oms/services/OrderCollection.js +++ b/packages/evershop/src/modules/oms/services/OrderCollection.js @@ -1,158 +1,38 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); class OrderCollection { constructor(baseQuery) { this.baseQuery = baseQuery; + this.baseQuery.orderBy('order.order_id', 'DESC'); } - async init(args, { filters = [] }) { + async init(filters = []) { const currentFilters = []; - // Number filter - const numberFilter = filters.find((f) => f.key === 'orderNumber'); - if (numberFilter) { - this.baseQuery.andWhere( - 'order.order_number', - 'ILIKE', - `%${numberFilter.value}%` - ); - currentFilters.push({ - key: 'orderNumber', - operation: '=', - value: numberFilter.value - }); - } - - // Email filter - const customerEmailFilter = filters.find((f) => f.key === 'customerEmail'); - if (customerEmailFilter) { - this.baseQuery.andWhere( - 'order.customer_email', - 'ILIKE', - `%${customerEmailFilter.value}%` - ); - currentFilters.push({ - key: 'customerEmail', - operation: '=', - value: customerEmailFilter.value - }); - } - - // Keyword search - const keywordFilter = filters.find((f) => f.key === 'keyword'); - if (keywordFilter) { - this.baseQuery - .andWhere('order.customer_email', 'ILIKE', `%${keywordFilter.value}%`) - .or('order.order_number', 'ILIKE', `%${keywordFilter.value}%`) - .or('order.customer_full_name', 'ILIKE', `%${keywordFilter.value}%`); - currentFilters.push({ - key: 'keyword', - operation: '=', - value: keywordFilter.value - }); - } - - // Status filter - const shipmentStatusFilter = filters.find( - (f) => f.key === 'shipmentStatus' - ); - if (shipmentStatusFilter) { - this.baseQuery.andWhere( - 'order.shipment_status', - '=', - shipmentStatusFilter.value - ); - currentFilters.push({ - key: 'shipmentStatus', - operation: '=', - value: shipmentStatusFilter.value - }); - } - - // Status filter - const paymentStatusFilter = filters.find((f) => f.key === 'paymentStatus'); - if (paymentStatusFilter) { - this.baseQuery.andWhere( - 'order.payment_status', - '=', - paymentStatusFilter.value - ); - currentFilters.push({ - key: 'paymentStatus', - operation: '=', - value: paymentStatusFilter.value - }); - } - - // Order Total filter - const totalFilter = filters.find((f) => f.key === 'total'); - if (totalFilter) { - const [min, max] = totalFilter.value.split('-').map((v) => parseFloat(v)); - let currentTotalFilter; - if (Number.isNaN(min) === false) { - this.baseQuery.andWhere('order.grand_total', '>=', min); - currentTotalFilter = { key: 'total', value: `${min}` }; - } - - if (Number.isNaN(max) === false) { - this.baseQuery.andWhere('order.grand_total', '<=', max); - currentTotalFilter = { - key: 'total', - value: `${currentTotalFilter.value}-${max}` - }; + // Apply the filters + const orderCollectionFilters = await getValue('orderCollectionFilters', []); + orderCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } } - if (currentTotalFilter) { - currentFilters.push(currentTotalFilter); - } - } - - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => f.key === 'sortOrder' && ['ASC', 'DESC'].includes(f.value) - ) || { value: 'ASC' }; - - if (sortBy && sortBy.value === 'orderNumber') { - this.baseQuery.orderBy('order.order_number', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - this.baseQuery.orderBy('order.order_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } + }); // Clone the main query for getting total right before doing the paging const totalQuery = this.baseQuery.clone(); totalQuery.select('COUNT("order".order_id)', 'total'); totalQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - this.baseQuery.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); + totalQuery.removeLimit(); + this.currentFilters = currentFilters; this.totalQuery = totalQuery; } diff --git a/packages/evershop/src/modules/oms/services/registerDefaultOrderCollectionFilters.js b/packages/evershop/src/modules/oms/services/registerDefaultOrderCollectionFilters.js new file mode 100644 index 000000000..83dd20319 --- /dev/null +++ b/packages/evershop/src/modules/oms/services/registerDefaultOrderCollectionFilters.js @@ -0,0 +1,121 @@ +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultOrderCollectionFilters() { + // List of default supported filters + const defaultFilters = [ + { + key: 'keyword', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query + .andWhere('order.customer_email', 'ILIKE', `%${value}%`) + .or('order.order_number', 'ILIKE', `%${value}%`) + .or('order.customer_full_name', 'ILIKE', `%${value}%`); + currentFilters.push({ + key: 'keyword', + operation, + value + }); + } + }, + { + key: 'number', + operation: ['eq', 'like'], + callback: (query, operation, value, currentFilters) => { + if (operation === 'eq') { + query.andWhere('order.order_number', OPERATION_MAP[operation], value); + } else { + query.andWhere( + 'order.order_number', + OPERATION_MAP[operation], + `%${value}%` + ); + } + currentFilters.push({ + key: 'order_number', + operation, + value + }); + } + }, + { + key: 'email', + operation: ['eq', 'like'], + callback: (query, operation, value, currentFilters) => { + if (operation === 'eq') { + query.andWhere( + 'order.customer_email', + OPERATION_MAP[operation], + value + ); + } else { + query.andWhere( + 'order.customer_email', + OPERATION_MAP[operation], + `%${value}%` + ); + } + currentFilters.push({ + key: 'email', + operation, + value + }); + } + }, + { + key: 'shipment_status', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'order.shipment_status', + OPERATION_MAP[operation], + value + ); + currentFilters.push({ + key: 'shipment_status', + operation, + value + }); + } + }, + { + key: 'payment_status', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere('order.payment_status', OPERATION_MAP[operation], value); + currentFilters.push({ + key: 'payment_status', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const orderCollectionSortBy = getValueSync('orderCollectionSortBy', { + number: (query) => query.orderBy('order.order_number'), + payment_status: (query) => query.orderBy('order.payment_status'), + shipment_status: (query) => query.orderBy('order.shipment_status'), + total: (query) => query.orderBy('order.grand_total'), + created_at: (query) => query.orderBy('order.created_at') + }); + + if (orderCollectionSortBy[value]) { + orderCollectionSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } + } + } + ]; + + return defaultFilters; +}; diff --git a/packages/evershop/src/modules/promotion/bootstrap.js b/packages/evershop/src/modules/promotion/bootstrap.js index b8137c9e1..0b88723d8 100644 --- a/packages/evershop/src/modules/promotion/bootstrap.js +++ b/packages/evershop/src/modules/promotion/bootstrap.js @@ -1,3 +1,6 @@ +const { + defaultPaginationFilters +} = require('../../lib/util/defaultPaginationFilters'); const { addProcessor } = require('../../lib/util/registry'); const { toPrice } = require('../checkout/services/toPrice'); const { validateCoupon } = require('./services/couponValidator'); @@ -5,6 +8,7 @@ const { calculateDiscount } = require('./services/discountCalculator'); const { registerDefaultCalculators } = require('./services/registerDefaultCalculators'); +const registerDefaultCouponCollectionFilters = require('./services/registerDefaultCouponCollectionFilters'); const { registerDefaultValidators } = require('./services/registerDefaultValidators'); @@ -112,4 +116,16 @@ module.exports = () => { addProcessor('couponValidatorFunctions', registerDefaultValidators); addProcessor('discountCalculatorFunctions', registerDefaultCalculators); + + // Reigtering the default filters for attribute collection + addProcessor( + 'couponCollectionFilters', + registerDefaultCouponCollectionFilters, + 1 + ); + addProcessor( + 'couponCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); }; diff --git a/packages/evershop/src/modules/promotion/graphql/types/Coupon/Coupon.admin.resolvers.js b/packages/evershop/src/modules/promotion/graphql/types/Coupon/Coupon.admin.resolvers.js index 50716b248..019889237 100644 --- a/packages/evershop/src/modules/promotion/graphql/types/Coupon/Coupon.admin.resolvers.js +++ b/packages/evershop/src/modules/promotion/graphql/types/Coupon/Coupon.admin.resolvers.js @@ -1,5 +1,4 @@ const { GraphQLJSON } = require('graphql-type-json'); -const { select } = require('@evershop/postgres-query-builder'); const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { @@ -11,12 +10,8 @@ module.exports = { JSON: GraphQLJSON, Query: { coupon: async (root, { id }, { pool }) => { - const query = select().from('coupon'); + const query = getCouponsBaseQuery(); query.where('coupon_id', '=', id); - // if (admin !== true) { - // query.where('cms_page.status', '=', 1); - // } - const coupon = await query.load(pool); return coupon ? camelCase(coupon) : null; }, @@ -27,7 +22,7 @@ module.exports = { } const query = getCouponsBaseQuery(); const root = new CouponCollection(query); - await root.init({}, { filters }); + await root.init(filters); return root; } }, diff --git a/packages/evershop/src/modules/promotion/pages/admin/couponGrid/Grid.jsx b/packages/evershop/src/modules/promotion/pages/admin/couponGrid/Grid.jsx index 2fa0e16a3..e1333f9ee 100644 --- a/packages/evershop/src/modules/promotion/pages/admin/couponGrid/Grid.jsx +++ b/packages/evershop/src/modules/promotion/pages/admin/couponGrid/Grid.jsx @@ -5,14 +5,15 @@ import Area from '@components/common/Area'; import Pagination from '@components/common/grid/Pagination'; import { Checkbox } from '@components/common/form/fields/Checkbox'; import { useAlertContext } from '@components/common/modal/Alert'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; -import FromToColumnHeader from '@components/common/grid/headers/FromTo'; -import StatusColumnHeader from '@components/common/grid/headers/Status'; import CouponName from '@components/admin/promotion/couponGrid/rows/CouponName'; import BasicRow from '@components/common/grid/rows/BasicRow'; import StatusRow from '@components/common/grid/rows/StatusRow'; import { Card } from '@components/admin/cms/Card'; import TextRow from '@components/common/grid/rows/TextRow'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; +import SortableHeader from '@components/common/grid/headers/Sortable'; +import DummyColumnHeader from '@components/common/grid/headers/Dummy'; function Actions({ coupons = [], selectedIds = [] }) { const { openAlert, closeAlert } = useAlertContext(); @@ -167,6 +168,61 @@ export default function CouponGrid({ return ( + + ( + f.key === 'coupon')?.value + } + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const coupon = + document.getElementById('coupon')?.value; + if (coupon) { + url.searchParams.set('coupon[operation]', 'like'); + url.searchParams.set('coupon[value]', coupon); + } else { + url.searchParams.delete('coupon[operation]'); + url.searchParams.delete('coupon[value]'); + } + window.location.href = url; + } + }} + /> + ) + }, + sortOrder: 10 + } + ]} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => { + // Just get the url and remove all query params + const url = new URL(document.location); + url.search = ''; + window.location.href = url.href; + } + } + ]} + />
@@ -187,9 +243,9 @@ export default function CouponGrid({ // eslint-disable-next-line react/no-unstable-nested-components component: { default: () => ( - ) @@ -199,30 +255,14 @@ export default function CouponGrid({ { // eslint-disable-next-line react/no-unstable-nested-components component: { - default: () => ( - f.key === 'startDate' - )} - /> - ) + default: () => }, sortOrder: 20 }, { // eslint-disable-next-line react/no-unstable-nested-components component: { - default: () => ( - f.key === 'endDate' - )} - /> - ) + default: () => }, sortOrder: 30 }, @@ -230,12 +270,10 @@ export default function CouponGrid({ // eslint-disable-next-line react/no-unstable-nested-components component: { default: () => ( - f.key === 'status' - )} + name="status" + currentFilters={currentFilters} /> ) }, @@ -245,12 +283,10 @@ export default function CouponGrid({ // eslint-disable-next-line react/no-unstable-nested-components component: { default: () => ( - f.key === 'usedTime' - )} + name="used_time" + currentFilters={currentFilters} /> ) }, diff --git a/packages/evershop/src/modules/promotion/pages/admin/couponGrid/index.js b/packages/evershop/src/modules/promotion/pages/admin/couponGrid/index.js index aa1656c62..eae86b3ff 100644 --- a/packages/evershop/src/modules/promotion/pages/admin/couponGrid/index.js +++ b/packages/evershop/src/modules/promotion/pages/admin/couponGrid/index.js @@ -10,6 +10,5 @@ module.exports = (request) => { title: 'Coupons', description: 'Coupons' }); - const { query } = request; - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/evershop/src/modules/promotion/services/CouponCollection.js b/packages/evershop/src/modules/promotion/services/CouponCollection.js index 08d0f44bb..97041b1ff 100644 --- a/packages/evershop/src/modules/promotion/services/CouponCollection.js +++ b/packages/evershop/src/modules/promotion/services/CouponCollection.js @@ -1,150 +1,41 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); class CouponCollection { constructor(baseQuery) { this.baseQuery = baseQuery; } - async init(args, { filters = [] }) { + async init(filters = []) { const currentFilters = []; - // Code filter - const nameFilter = filters.find((f) => f.key === 'coupon'); - if (nameFilter) { - this.baseQuery.andWhere( - 'coupon.coupon', - 'ILIKE', - `%${nameFilter.value}%` - ); - currentFilters.push({ - key: 'coupon', - operation: '=', - value: nameFilter.value - }); - } - - // Status filter - const statusFilter = filters.find((f) => f.key === 'status'); - if (statusFilter) { - this.baseQuery.andWhere('coupon.status', '=', statusFilter.value); - currentFilters.push({ - key: 'status', - operation: '=', - value: statusFilter.value - }); - } - - const startDate = filters.find((f) => f.key === 'startDate'); - if (startDate) { - const [min, max] = startDate.value.split('-').map((v) => parseFloat(v)); - let currentStartDateFilter; - if (Number.isNaN(min) === false) { - this.baseQuery.andWhere('coupon.start_date', '>=', min); - currentStartDateFilter = { key: 'startDate', value: `${min}` }; - } - - if (Number.isNaN(max) === false) { - this.baseQuery.andWhere('coupon.start_date', '<=', max); - currentStartDateFilter = { - key: 'startDate', - value: `${currentStartDateFilter.value}-${max}` - }; - } - if (currentStartDateFilter) { - currentFilters.push(currentStartDateFilter); - } - } - // Start date filter - const endDate = filters.find((f) => f.key === 'endDate'); - if (endDate) { - const [min, max] = endDate.value.split('-').map((v) => parseFloat(v)); - let currentEndtDateFilter; - if (Number.isNaN(min) === false) { - this.baseQuery.andWhere('coupon.end_date', '>=', min); - currentEndtDateFilter = { key: 'endDate', value: `${min}` }; - } - - if (Number.isNaN(max) === false) { - this.baseQuery.andWhere('coupon.end_date', '<=', max); - currentEndtDateFilter = { - key: 'endDate', - value: `${currentEndtDateFilter.value}-${max}` - }; - } - if (currentEndtDateFilter) { - currentFilters.push(currentEndtDateFilter); - } - } - - // Used time filter - const usedTime = filters.find((f) => f.key === 'usedTime'); - if (usedTime) { - const [min, max] = usedTime.value.split('-').map((v) => parseFloat(v)); - let currentUsedTimeFilter; - if (Number.isNaN(min) === false) { - this.baseQuery.andWhere('coupon.used_time', '>=', min); - currentUsedTimeFilter = { key: 'usedTime', value: `${min}` }; - } + // Apply the filters + const couponCollectionFilters = await getValue( + 'couponCollectionFilters', + [] + ); - if (Number.isNaN(max) === false) { - this.baseQuery.andWhere('coupon.used_time', '<=', max); - currentUsedTimeFilter = { - key: 'usedTime', - value: `${currentUsedTimeFilter.value}-${max}` - }; - } - if (currentUsedTimeFilter) { - currentFilters.push(currentUsedTimeFilter); + couponCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } } - } - - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => f.key === 'sortOrder' && ['ASC', 'DESC'].includes(f.value) - ) || { value: 'ASC' }; - - if (sortBy && sortBy.value === 'coupon') { - this.baseQuery.orderBy('coupon.coupon', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - this.baseQuery.orderBy('coupon.coupon_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } + }); // Clone the main query for getting total right before doing the paging const totalQuery = this.baseQuery.clone(); totalQuery.select('COUNT(coupon.coupon_id)', 'total'); totalQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit' && f.value > 0) || { - value: 20 - }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - this.baseQuery.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); + totalQuery.removeLimit(); + this.currentFilters = currentFilters; this.totalQuery = totalQuery; } diff --git a/packages/evershop/src/modules/promotion/services/registerDefaultCouponCollectionFilters.js b/packages/evershop/src/modules/promotion/services/registerDefaultCouponCollectionFilters.js new file mode 100644 index 000000000..a108ec4e9 --- /dev/null +++ b/packages/evershop/src/modules/promotion/services/registerDefaultCouponCollectionFilters.js @@ -0,0 +1,60 @@ +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultCouponCollectionFilters() { + // List of default supported filters + const defaultFilters = [ + { + key: 'coupon', + operation: ['eq', 'like'], + callback: (query, operation, value, currentFilters) => { + if (operation === 'eq') { + query.andWhere('coupon.coupon', '=', value); + } else { + query.andWhere('coupon.coupon', 'ILIKE', `%${value}%`); + } + currentFilters.push({ + key: 'name', + operation, + value + }); + } + }, + { + key: 'status', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere('coupon.status', OPERATION_MAP[operation], value); + currentFilters.push({ + key: 'status', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const couponCollectionSortBy = getValueSync('couponCollectionSortBy', { + coupon: (query) => query.orderBy('coupon.coupon'), + status: (query) => query.orderBy('coupon.status'), + used_time: (query) => query.orderBy('coupon.used_time') + }); + + if (couponCollectionSortBy[value]) { + couponCollectionSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } + } + } + ]; + + return defaultFilters; +}; diff --git a/packages/postgres-query-builder/index.js b/packages/postgres-query-builder/index.js index 42e2c07f5..8751ebf79 100644 --- a/packages/postgres-query-builder/index.js +++ b/packages/postgres-query-builder/index.js @@ -599,6 +599,11 @@ class SelectQuery extends Query { return this; } + orderDirection(direction) { + this._orderBy._direction = direction; + return this; + } + sql() { if (!this._table) { throw Error('You must specific table by calling `from` method'); @@ -701,6 +706,11 @@ class SelectQuery extends Query { this._groupBy = new GroupBy(); return this; } + + removeLimit() { + this._limit = new Limit(); + return this; + } } class UpdateQuery extends Query { diff --git a/packages/product_review/bootstrap.js b/packages/product_review/bootstrap.js new file mode 100644 index 000000000..cfa9f1b11 --- /dev/null +++ b/packages/product_review/bootstrap.js @@ -0,0 +1,19 @@ +const { addProcessor } = require('@evershop/evershop/src/lib/util/registry'); +const { + defaultPaginationFilters +} = require('@evershop/evershop/src/lib/util/defaultPaginationFilters'); +const registerDefaultReviewCollectionFilters = require('./services/registerDefaultReviewCollectionFilters'); + +module.exports = () => { + // Reigtering the default filters for attribute collection + addProcessor( + 'productReviewCollectionFilters', + registerDefaultReviewCollectionFilters, + 1 + ); + addProcessor( + 'productReviewCollectionFilters', + (filters) => [...filters, ...defaultPaginationFilters], + 2 + ); +}; diff --git a/packages/product_review/graphql/types/Review/Review.resolvers.js b/packages/product_review/graphql/types/Review/Review.resolvers.js index ec9110839..985f91494 100644 --- a/packages/product_review/graphql/types/Review/Review.resolvers.js +++ b/packages/product_review/graphql/types/Review/Review.resolvers.js @@ -30,7 +30,7 @@ module.exports = { reviews: async (_, { filters }, { user }) => { const query = getReviewsBaseQuery(); const root = new ReviewCollection(query); - await root.init({}, { filters }, { user }); + await root.init(filters, !!user); return root; } }, diff --git a/packages/product_review/package.json b/packages/product_review/package.json index 3080e4aea..4bd8fdb42 100644 --- a/packages/product_review/package.json +++ b/packages/product_review/package.json @@ -1,6 +1,6 @@ { "name": "@evershop/product_review", - "version": "1.0.1", + "version": "1.0.2", "description": "An Evershop extension for product review", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/packages/product_review/pages/admin/reviewGrid/Grid.jsx b/packages/product_review/pages/admin/reviewGrid/Grid.jsx index 0c4c2c042..137ce9878 100644 --- a/packages/product_review/pages/admin/reviewGrid/Grid.jsx +++ b/packages/product_review/pages/admin/reviewGrid/Grid.jsx @@ -7,10 +7,12 @@ import { useAlertContext } from '@components/common/modal/Alert'; import { Checkbox } from '@components/common/form/fields/Checkbox'; import { Card } from '@components/admin/cms/Card'; import Area from '@components/common/Area'; -import BasicColumnHeader from '@components/common/grid/headers/Basic'; import BasicRow from '@components/common/grid/rows/BasicRow'; +import { Form } from '@components/common/form/Form'; +import { Field } from '@components/common/form/Field'; +import SortableHeader from '@components/common/grid/headers/Sortable'; +import DummyColumnHeader from '@components/common/grid/headers/Dummy'; import IsApprovedRow from './row/IsApprovedRow'; -import IsApprovedHeader from './header/IsApprovedHeader'; import RatingRow from './row/RatingRow'; import CommentRow from './row/CommentRow'; import ProductRow from './row/ProductRow'; @@ -174,6 +176,64 @@ export default function ReviewGrid({ return ( + + ( + f.key === 'keyword')?.value + } + onKeyPress={(e) => { + // If the user press enter, we should submit the form + if (e.key === 'Enter') { + const url = new URL(document.location); + const keyword = + document.getElementById('keyword')?.value; + if (keyword) { + url.searchParams.set( + 'keyword[operation]', + 'like' + ); + url.searchParams.set('keyword[value]', keyword); + } else { + url.searchParams.delete('keyword[operation]'); + url.searchParams.delete('keyword[value]'); + } + window.location.href = url; + } + }} + /> + ) + }, + sortOrder: 10 + } + ]} + /> + + } + actions={[ + { + variant: 'interactive', + name: 'Clear filter', + onAction: () => { + // Just get the url and remove all query params + const url = new URL(document.location); + url.search = ''; + window.location.href = url.href; + } + } + ]} + />
@@ -196,9 +256,9 @@ export default function ReviewGrid({ { component: { default: () => ( - ) @@ -207,34 +267,22 @@ export default function ReviewGrid({ }, { component: { - default: () => ( - - ) + default: () => }, sortOrder: 5 }, { component: { - default: () => ( - - ) + default: () => }, sortOrder: 10 }, { component: { default: () => ( - ) @@ -244,12 +292,10 @@ export default function ReviewGrid({ { component: { default: () => ( - f.key === 'approved' - )} + name="status" + currentFilters={currentFilters} /> ) }, diff --git a/packages/product_review/pages/admin/reviewGrid/header/IsApprovedHeader.jsx b/packages/product_review/pages/admin/reviewGrid/header/IsApprovedHeader.jsx deleted file mode 100644 index 98d0cde49..000000000 --- a/packages/product_review/pages/admin/reviewGrid/header/IsApprovedHeader.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { Select } from '@components/common/form/fields/Select'; - -export default function IsApprovedHeader({ title, id, currentFilter = {} }) { - const [current, setCurrent] = React.useState(''); - - const onChange = (e) => { - const url = new URL(document.location); - if (e.target.value === 'all') url.searchParams.delete(id); - else url.searchParams.set(id, e.target.value); - window.location.href = url.href; - }; - - React.useEffect(() => { - setCurrent(currentFilter.value || 'all'); - }, []); - - return ( - - ); -} - -IsApprovedHeader.propTypes = { - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - currentFilter: PropTypes.shape({ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - }) -}; - -IsApprovedHeader.defaultProps = { - currentFilter: {} -}; diff --git a/packages/product_review/pages/admin/reviewGrid/index.js b/packages/product_review/pages/admin/reviewGrid/index.js index 04242b6fb..d1cd661ed 100644 --- a/packages/product_review/pages/admin/reviewGrid/index.js +++ b/packages/product_review/pages/admin/reviewGrid/index.js @@ -11,6 +11,5 @@ module.exports = (request, response) => { title: 'Reviews', description: 'Reviews' }); - const { query } = request; - setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(query)); + setContextValue(request, 'filtersFromUrl', buildFilterFromUrl(request)); }; diff --git a/packages/product_review/services/ReviewCollection.js b/packages/product_review/services/ReviewCollection.js index fa8a4ccdc..8c7003cb1 100644 --- a/packages/product_review/services/ReviewCollection.js +++ b/packages/product_review/services/ReviewCollection.js @@ -1,106 +1,45 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getValue } = require('@evershop/evershop/src/lib/util/registry'); class ReviewCollection { constructor(baseQuery) { this.baseQuery = baseQuery; + this.baseQuery.orderBy('product_review.review_id', 'DESC'); } - async init(args, { filters = [] }, { user }) { - if (!user) { + async init(filters = [], isAdmin = false) { + if (!isAdmin) { this.baseQuery.andWhere('product_review.approved', '=', 't'); } const currentFilters = []; - // Product Name filter - const productNameFilter = filters.find((f) => f.key === 'product'); - if (productNameFilter) { - this.baseQuery.andWhere( - 'product_description.name', - 'ILIKE', - `%${productNameFilter.value}%` - ); - currentFilters.push({ - key: 'product', - operation: '=', - value: productNameFilter.value - }); - } - - // Customer Name filter - const customerNameFilter = filters.find((f) => f.key === 'customer_name'); - if (customerNameFilter) { - this.baseQuery.andWhere( - 'product_review.customer_name', - 'ILIKE', - `%${customerNameFilter.value}%` - ); - currentFilters.push({ - key: 'customer_name', - operation: '=', - value: customerNameFilter.value - }); - } - - // Status filter - const statusFilter = filters.find((f) => f.key === 'approved'); - if (statusFilter) { - this.baseQuery.andWhere( - 'product_review.approved', - '=', - statusFilter.value - ); - currentFilters.push({ - key: 'approved', - operation: '=', - value: statusFilter.value - }); - } - - const sortBy = filters.find((f) => f.key === 'sortBy'); - const sortOrder = filters.find( - (f) => f.key === 'sortOrder' && ['ASC', 'DESC'].includes(f.value) - ) || { value: 'ASC' }; + // Apply the filters + const productReviewCollectionFilters = await getValue( + 'productReviewCollectionFilters', + [] + ); - if (sortBy && sortBy.value === 'rating') { - this.baseQuery.orderBy('product_review.rating', sortOrder.value); - currentFilters.push({ - key: 'sortBy', - operation: '=', - value: sortBy.value - }); - } else { - this.baseQuery.orderBy('product_review.review_id', 'DESC'); - } - if (sortOrder.key) { - currentFilters.push({ - key: 'sortOrder', - operation: '=', - value: sortOrder.value - }); - } + productReviewCollectionFilters.forEach((filter) => { + const check = filters.find((f) => f.key === filter.key); + if (check) { + if (filter.operation.includes(check.operation)) { + filter.callback( + this.baseQuery, + check.operation, + check.value, + currentFilters + ); + } + } + }); // Clone the main query for getting total right before doing the paging const totalQuery = this.baseQuery.clone(); totalQuery.select('COUNT(product_review.review_id)', 'total'); totalQuery.removeOrderBy(); - // Paging - const page = filters.find((f) => f.key === 'page') || { value: 1 }; - const limit = filters.find((f) => f.key === 'limit') || { value: 20 }; // TODO: Get from the config - currentFilters.push({ - key: 'page', - operation: '=', - value: page.value - }); - currentFilters.push({ - key: 'limit', - operation: '=', - value: limit.value - }); - this.baseQuery.limit( - (page.value - 1) * parseInt(limit.value, 10), - parseInt(limit.value, 10) - ); + totalQuery.removeLimit(); + this.currentFilters = currentFilters; this.totalQuery = totalQuery; } diff --git a/packages/product_review/services/registerDefaultReviewCollectionFilters.js b/packages/product_review/services/registerDefaultReviewCollectionFilters.js new file mode 100644 index 000000000..98da7c45c --- /dev/null +++ b/packages/product_review/services/registerDefaultReviewCollectionFilters.js @@ -0,0 +1,66 @@ +const { + OPERATION_MAP +} = require('@evershop/evershop/src/lib/util/filterOperationMapp'); +const { getValueSync } = require('@evershop/evershop/src/lib/util/registry'); + +module.exports = async function registerDefaultReviewCollectionFilters() { + // List of default supported filters + const defaultFilters = [ + { + key: 'keyword', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query + .andWhere('product_description.name', 'ILIKE', `%${value}%`) + .or('product_review.customer_name', 'ILIKE', `%${value}%`) + .or('product_review.comment', 'ILIKE', `%${value}%`); + currentFilters.push({ + key: 'keyword', + operation, + value + }); + } + }, + { + key: 'status', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + query.andWhere( + 'product_review.status', + OPERATION_MAP[operation], + value + ); + currentFilters.push({ + key: 'status', + operation, + value + }); + } + }, + { + key: 'ob', + operation: ['eq'], + callback: (query, operation, value, currentFilters) => { + const productReviewCollectionSortBy = getValueSync( + 'productReviewCollectionSortBy', + { + product: (query) => query.orderBy('product_description.name'), + rating: (query) => query.orderBy('product_review.rating'), + status: (query) => query.orderBy('product_review.status') + } + ); + + if (productReviewCollectionSortBy[value]) { + productReviewCollectionSortBy[value](query, operation); + currentFilters.push({ + key: 'ob', + operation, + value + }); + } + } + } + ]; + + return defaultFilters; +}; From 4cbfcc86c14200324c963b08428386130497290b Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:49:30 +0700 Subject: [PATCH 10/12] Fix logging icon alignment --- packages/evershop/src/lib/log/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evershop/src/lib/log/logger.js b/packages/evershop/src/lib/log/logger.js index 35701700e..05328068b 100644 --- a/packages/evershop/src/lib/log/logger.js +++ b/packages/evershop/src/lib/log/logger.js @@ -30,7 +30,7 @@ const format = winston.format.combine( icon = '❌'; // Error icon break; case 'warn': - icon = '⚠️'; // Warning icon + icon = '⚠️ '; // Warning icon break; case 'info': icon = 'ℹ️'; // Info icon From 42ce9957b2f44c74a13d8b51bd84043db8a66059 Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:50:16 +0700 Subject: [PATCH 11/12] Adding more font size to admin tailwind config --- .../src/modules/cms/services/tailwind.admin.config.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/evershop/src/modules/cms/services/tailwind.admin.config.js b/packages/evershop/src/modules/cms/services/tailwind.admin.config.js index 10e8d5152..eed1de4b7 100644 --- a/packages/evershop/src/modules/cms/services/tailwind.admin.config.js +++ b/packages/evershop/src/modules/cms/services/tailwind.admin.config.js @@ -43,7 +43,12 @@ module.exports = { }, theme: { fontSize: { - base: '.875rem' + sm: '0.8rem', + xs: '.75rem', + base: '.875rem', + lg: '1.125rem', + xl: '1.25rem', + '2xl': '1.5rem' }, colors: { primary: '#008060', From 76e15a2a6beae5cbb1e250318c01a26e90de799e Mon Sep 17 00:00:00 2001 From: The Nguyen <6950941+treoden@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:51:06 +0700 Subject: [PATCH 12/12] Improve the collection filtering --- .../services/registerDefaultAttributeCollectionFilters.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/evershop/src/modules/catalog/services/registerDefaultAttributeCollectionFilters.js b/packages/evershop/src/modules/catalog/services/registerDefaultAttributeCollectionFilters.js index 9860b9117..7c61d4de9 100644 --- a/packages/evershop/src/modules/catalog/services/registerDefaultAttributeCollectionFilters.js +++ b/packages/evershop/src/modules/catalog/services/registerDefaultAttributeCollectionFilters.js @@ -113,7 +113,9 @@ module.exports = async function registerDefaultAttributeCollectionFilters() { 'attributeCollectionSortBy', { name: (query) => query.orderBy('attribute.name'), - type: (query) => query.orderBy('attribute.type') + type: (query) => query.orderBy('attribute.type'), + is_required: (query) => query.orderBy('attribute.is_required'), + is_filterable: (query) => query.orderBy('attribute.is_filterable') } );
-
-
- {title} -
-
-