From 98490f00458876abf59e6b3df2385350fa14da88 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Wed, 17 Dec 2025 20:20:58 +0100 Subject: [PATCH 01/12] feat: Enhance automation rule value handling to support multiple data types - Updated schema to allow 'value' to be a string, number, or array in both the form and API schemas. - Changed database column type for 'value' from text to jsonb to accommodate new data types. - Adjusted model definitions and interfaces to reflect the updated 'value' type. - Added new operator types for better query capabilities. --- .../automations-form/types/schema.ts | 9 +++- src/api/admin/mpn/automations/rules/route.ts | 11 ++++- .../.snapshot-medusa-mpn-automation.json | 4 +- .../migrations/Migration20251217190839.ts | 13 +++++ .../models/mpn_automation_rule_value.ts | 3 +- .../mpn-automation/types/interfaces.ts | 3 +- src/modules/mpn-automation/types/types.ts | 47 ++++++++++++++++++- 7 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 src/modules/mpn-automation/migrations/Migration20251217190839.ts diff --git a/src/admin/automations/automations-form/types/schema.ts b/src/admin/automations/automations-form/types/schema.ts index dfd1e58..111b792 100644 --- a/src/admin/automations/automations-form/types/schema.ts +++ b/src/admin/automations/automations-form/types/schema.ts @@ -32,9 +32,14 @@ export const baseAutomationFormSchema = z.object({ .array( z.object({ id: z.string().optional(), + // Value can be string, number, or array (for array operators like contains, in, etc.) value: z - .string() - .min(1, "Value is required"), + .union([ + z.string().min(1, "Value is required"), + z.number(), + z.array(z.string().min(1)), + z.array(z.number()), + ]), }) ) .optional(), diff --git a/src/api/admin/mpn/automations/rules/route.ts b/src/api/admin/mpn/automations/rules/route.ts index d09f285..d05a2e5 100644 --- a/src/api/admin/mpn/automations/rules/route.ts +++ b/src/api/admin/mpn/automations/rules/route.ts @@ -23,7 +23,16 @@ export const PostAutomationRulesSchema = z.object({ .array( z.object({ id: z.string().optional(), - value: z.string().nullable().optional(), + // Value can be string, number, or array (for array operators like contains, in, etc.) + value: z + .union([ + z.string(), + z.number(), + z.array(z.string()), + z.array(z.number()), + ]) + .nullable() + .optional(), metadata: z .record(z.any()) .nullable() diff --git a/src/modules/mpn-automation/migrations/.snapshot-medusa-mpn-automation.json b/src/modules/mpn-automation/migrations/.snapshot-medusa-mpn-automation.json index bf027ab..b90e4a3 100644 --- a/src/modules/mpn-automation/migrations/.snapshot-medusa-mpn-automation.json +++ b/src/modules/mpn-automation/migrations/.snapshot-medusa-mpn-automation.json @@ -473,12 +473,12 @@ }, "value": { "name": "value", - "type": "text", + "type": "jsonb", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "mappedType": "text" + "mappedType": "json" }, "metadata": { "name": "metadata", diff --git a/src/modules/mpn-automation/migrations/Migration20251217190839.ts b/src/modules/mpn-automation/migrations/Migration20251217190839.ts new file mode 100644 index 0000000..5a1ef5d --- /dev/null +++ b/src/modules/mpn-automation/migrations/Migration20251217190839.ts @@ -0,0 +1,13 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20251217190839 extends Migration { + + override async up(): Promise { + this.addSql(`alter table if exists "mpn_automation_rule_value" alter column "value" type jsonb using ("value"::jsonb);`); + } + + override async down(): Promise { + this.addSql(`alter table if exists "mpn_automation_rule_value" alter column "value" type text using ("value"::text);`); + } + +} diff --git a/src/modules/mpn-automation/models/mpn_automation_rule_value.ts b/src/modules/mpn-automation/models/mpn_automation_rule_value.ts index 318f984..e70b007 100644 --- a/src/modules/mpn-automation/models/mpn_automation_rule_value.ts +++ b/src/modules/mpn-automation/models/mpn_automation_rule_value.ts @@ -6,7 +6,8 @@ export const MpnAutomationRuleValue = model.define( { id: model.id().primaryKey(), - value: model.text().nullable(), + // Value can be string, number, or array (for array operators like contains) + value: model.json().nullable(), metadata: model.json().nullable(), diff --git a/src/modules/mpn-automation/types/interfaces.ts b/src/modules/mpn-automation/types/interfaces.ts index 50da154..bd97c67 100644 --- a/src/modules/mpn-automation/types/interfaces.ts +++ b/src/modules/mpn-automation/types/interfaces.ts @@ -2,7 +2,8 @@ import { TriggerType } from "./types" export interface AutomationRuleValue { id?: string - value: string | null + // Value can be string, number, or array (for array operators like contains, in, etc.) + value: string | number | string[] | number[] | null metadata: Record | null } diff --git a/src/modules/mpn-automation/types/types.ts b/src/modules/mpn-automation/types/types.ts index 728415b..f41bde0 100644 --- a/src/modules/mpn-automation/types/types.ts +++ b/src/modules/mpn-automation/types/types.ts @@ -43,6 +43,21 @@ export type CustomAction = { export type Attribute = { value?: string label?: string + /** + * Type of the attribute value + * - "primitive": single value (string, number, boolean) + * - "array": array of values (e.g., tags, categories) + * - "object": nested object + */ + type?: "primitive" | "array" | "object" + /** + * Whether this attribute represents a relation + */ + isRelation?: boolean + /** + * Type of relation (e.g., "tags", "categories", "variants") + */ + relationType?: string } export interface FieldConfig { @@ -171,11 +186,17 @@ export enum TriggerType { export enum OperatorType { EQUAL = "eq", - NOT_EQUAL = "neq", + NOT_EQUAL = "ne", + IN = "in", + NOT_IN = "nin", GREATER_THAN = "gt", LESS_THAN = "lt", GREATER_THAN_OR_EQUAL = "gte", LESS_THAN_OR_EQUAL = "lte", + CONTAINS = "contains", + NOT_CONTAINS = "not_contains", + EMPTY = "empty", + NOT_EMPTY = "not_empty", } export const OPERATOR_TYPES = [ @@ -187,6 +208,14 @@ export const OPERATOR_TYPES = [ value: OperatorType.NOT_EQUAL, label: "Not Equal", }, + { + value: OperatorType.IN, + label: "In", + }, + { + value: OperatorType.NOT_IN, + label: "Not In", + }, { value: OperatorType.GREATER_THAN, label: "Greater Than", @@ -203,6 +232,22 @@ export const OPERATOR_TYPES = [ value: OperatorType.LESS_THAN_OR_EQUAL, label: "Less Than or Equal", }, + { + value: OperatorType.CONTAINS, + label: "Contains", + }, + { + value: OperatorType.NOT_CONTAINS, + label: "Not Contains", + }, + { + value: OperatorType.EMPTY, + label: "Empty", + }, + { + value: OperatorType.NOT_EMPTY, + label: "Not Empty", + }, ] export const TRIGGER_TYPES = [ From 4f3c076e9ed67fd93ef1f7bd573ab6368c80cf49 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Wed, 17 Dec 2025 20:21:22 +0100 Subject: [PATCH 02/12] feat: Add product attributes and event metadata for product categories, tags, and types - Introduced new attributes for product categories, tags, and types to enhance product management capabilities. - Updated event metadata registry to include handling for "product.updated", "product-tag.updated", "product-type.updated", and "product-category.updated" events. - Expanded existing product attributes to include additional fields such as thumbnail, HS code, and various dimensions. - Enhanced product variant attributes with new fields for inventory management and product details. --- .../mpn-automation/types/modules/index.ts | 38 ++++- .../types/modules/product-category/index.ts | 1 + .../product-category/product-category.ts | 43 +++++ .../types/modules/product-tag/index.ts | 1 + .../types/modules/product-tag/product-tag.ts | 18 +++ .../types/modules/product-type/index.ts | 1 + .../modules/product-type/product-type.ts | 18 +++ .../product-variant/product-variant.ts | 60 +++++++ .../types/modules/product/product.ts | 148 ++++++++++++++++++ src/subscribers/product-updated.ts | 2 + src/utils/validate-rules.ts | 44 +++++- .../product/steps/get-product-by-id.ts | 33 ++-- 12 files changed, 382 insertions(+), 25 deletions(-) create mode 100644 src/modules/mpn-automation/types/modules/product-category/index.ts create mode 100644 src/modules/mpn-automation/types/modules/product-category/product-category.ts create mode 100644 src/modules/mpn-automation/types/modules/product-tag/index.ts create mode 100644 src/modules/mpn-automation/types/modules/product-tag/product-tag.ts create mode 100644 src/modules/mpn-automation/types/modules/product-type/index.ts create mode 100644 src/modules/mpn-automation/types/modules/product-type/product-type.ts diff --git a/src/modules/mpn-automation/types/modules/index.ts b/src/modules/mpn-automation/types/modules/index.ts index 29eb6bb..385b94d 100644 --- a/src/modules/mpn-automation/types/modules/index.ts +++ b/src/modules/mpn-automation/types/modules/index.ts @@ -4,6 +4,9 @@ import { } from "./inventory" import { PRODUCT_ATTRIBUTES } from "./product" import { PRODUCT_VARIANT_ATTRIBUTES } from "./product-variant" +import { PRODUCT_TAG_ATTRIBUTES } from "./product-tag" +import { PRODUCT_TYPE_ATTRIBUTES } from "./product-type" +import { PRODUCT_CATEGORY_ATTRIBUTES } from "./product-category" import { Attribute } from "../types" /** @@ -91,6 +94,15 @@ const EVENT_METADATA_REGISTRY: Record< }, ], }, + "product.updated": { + attributes: PRODUCT_ATTRIBUTES, + templates: [ + { + value: "product", + name: "Product", + }, + ], + }, "product-variant.updated": { attributes: PRODUCT_VARIANT_ATTRIBUTES, templates: [ @@ -100,12 +112,30 @@ const EVENT_METADATA_REGISTRY: Record< }, ], }, - "product.updated": { - attributes: PRODUCT_ATTRIBUTES, + "product-tag.updated": { + attributes: PRODUCT_TAG_ATTRIBUTES, templates: [ { - value: "product", - name: "Product", + value: "product-tag", + name: "Product Tag", + }, + ], + }, + "product-type.updated": { + attributes: PRODUCT_TYPE_ATTRIBUTES, + templates: [ + { + value: "product-type", + name: "Product Type", + }, + ], + }, + "product-category.updated": { + attributes: PRODUCT_CATEGORY_ATTRIBUTES, + templates: [ + { + value: "product-category", + name: "Product Category", }, ], }, diff --git a/src/modules/mpn-automation/types/modules/product-category/index.ts b/src/modules/mpn-automation/types/modules/product-category/index.ts new file mode 100644 index 0000000..9085556 --- /dev/null +++ b/src/modules/mpn-automation/types/modules/product-category/index.ts @@ -0,0 +1 @@ +export * from "./product-category" \ No newline at end of file diff --git a/src/modules/mpn-automation/types/modules/product-category/product-category.ts b/src/modules/mpn-automation/types/modules/product-category/product-category.ts new file mode 100644 index 0000000..d05e621 --- /dev/null +++ b/src/modules/mpn-automation/types/modules/product-category/product-category.ts @@ -0,0 +1,43 @@ +export const PRODUCT_CATEGORY_ATTRIBUTES = [ + { + value: "product_category.id", + label: "ID", + }, + { + value: "product_category.name", + label: "Name", + }, + { + value: "product_category.description", + label: "Description", + }, + { + value: "product_category.handle", + label: "Handle", + }, + { + value: "product_category.is_active", + label: "Is Active", + }, + { + value: "product_category.is_internal", + label: "Is Internal", + }, + { + value: "product_category.rank", + label: "Rank", + }, + { + value: "product_category.parent_category_id", + label: "Parent Category ID", + }, + { + value: "product_category.created_at", + label: "Created At", + }, + { + value: "product_category.updated_at", + label: "Updated At", + }, +] + diff --git a/src/modules/mpn-automation/types/modules/product-tag/index.ts b/src/modules/mpn-automation/types/modules/product-tag/index.ts new file mode 100644 index 0000000..bb9a3bd --- /dev/null +++ b/src/modules/mpn-automation/types/modules/product-tag/index.ts @@ -0,0 +1 @@ +export * from "./product-tag" \ No newline at end of file diff --git a/src/modules/mpn-automation/types/modules/product-tag/product-tag.ts b/src/modules/mpn-automation/types/modules/product-tag/product-tag.ts new file mode 100644 index 0000000..711100b --- /dev/null +++ b/src/modules/mpn-automation/types/modules/product-tag/product-tag.ts @@ -0,0 +1,18 @@ +export const PRODUCT_TAG_ATTRIBUTES = [ + { + value: "product_tag.id", + label: "ID", + }, + { + value: "product_tag.value", + label: "Value", + }, + { + value: "product_tag.created_at", + label: "Created At", + }, + { + value: "product_tag.updated_at", + label: "Updated At", + }, +] \ No newline at end of file diff --git a/src/modules/mpn-automation/types/modules/product-type/index.ts b/src/modules/mpn-automation/types/modules/product-type/index.ts new file mode 100644 index 0000000..b17fae8 --- /dev/null +++ b/src/modules/mpn-automation/types/modules/product-type/index.ts @@ -0,0 +1 @@ +export * from "./product-type" \ No newline at end of file diff --git a/src/modules/mpn-automation/types/modules/product-type/product-type.ts b/src/modules/mpn-automation/types/modules/product-type/product-type.ts new file mode 100644 index 0000000..1ee5aef --- /dev/null +++ b/src/modules/mpn-automation/types/modules/product-type/product-type.ts @@ -0,0 +1,18 @@ +export const PRODUCT_TYPE_ATTRIBUTES = [ + { + value: "product_type.id", + label: "ID", + }, + { + value: "product_type.value", + label: "Value", + }, + { + value: "product_type.created_at", + label: "Created At", + }, + { + value: "product_type.updated_at", + label: "Updated At", + }, +] \ No newline at end of file diff --git a/src/modules/mpn-automation/types/modules/product-variant/product-variant.ts b/src/modules/mpn-automation/types/modules/product-variant/product-variant.ts index 0305944..7ee8c6c 100644 --- a/src/modules/mpn-automation/types/modules/product-variant/product-variant.ts +++ b/src/modules/mpn-automation/types/modules/product-variant/product-variant.ts @@ -23,4 +23,64 @@ export const PRODUCT_VARIANT_ATTRIBUTES = [ value: "product_variant.upc", label: "UPC", }, + { + value: "product_variant.allow_backorder", + label: "Allow Backorder", + }, + { + value: "product_variant.manage_inventory", + label: "Manage Inventory", + }, + { + value: "product_variant.hs_code", + label: "HS Code", + }, + { + value: "product_variant.origin_country", + label: "Origin Country", + }, + { + value: "product_variant.mid_code", + label: "MID Code", + }, + { + value: "product_variant.material", + label: "Material", + }, + { + value: "product_variant.weight", + label: "Weight", + }, + { + value: "product_variant.length", + label: "Length", + }, + { + value: "product_variant.height", + label: "Height", + }, + { + value: "product_variant.width", + label: "Width", + }, + // { + // value: "product_variant.metadata", + // label: "Metadata", + // }, + { + value: "product_variant.variant_rank", + label: "Variant Rank", + }, + { + value: "product_variant.product_id", + label: "Product ID", + }, + { + value: "product_variant.created_at", + label: "Created At", + }, + { + value: "product_variant.updated_at", + label: "Updated At", + }, ] \ No newline at end of file diff --git a/src/modules/mpn-automation/types/modules/product/product.ts b/src/modules/mpn-automation/types/modules/product/product.ts index 38f770c..1afbeb6 100644 --- a/src/modules/mpn-automation/types/modules/product/product.ts +++ b/src/modules/mpn-automation/types/modules/product/product.ts @@ -43,4 +43,152 @@ export const PRODUCT_ATTRIBUTES = [ value: "product.upc", label: "UPC", }, + { + value: "product.thumbnail", + label: "Thumbnail", + }, + { + value: "product.hs_code", + label: "HS Code", + }, + { + value: "product.origin_country", + label: "Origin Country", + }, + { + value: "product.mid_code", + label: "MID Code", + }, + { + value: "product.material", + label: "Material", + }, + { + value: "product.weight", + label: "Weight", + }, + { + value: "product.length", + label: "Length", + }, + { + value: "product.height", + label: "Height", + }, + { + value: "product.width", + label: "Width", + }, + // { + // value: "product.metadata", + // label: "Metadata", + // }, + { + value: "product.created_at", + label: "Created At", + }, + { + value: "product.updated_at", + label: "Updated At", + }, + { + value: "product.deleted_at", + label: "Deleted At", + }, + // Relations - Tags + { + value: "product.tags.id", + label: "Tag ID", + type: "array", + isRelation: true, + relationType: "tags", + }, + { + value: "product.tags.value", + label: "Tag Value", + type: "array", + isRelation: true, + relationType: "tags", + }, + // Relations - Categories + { + value: "product.categories.id", + label: "Category ID", + type: "array", + isRelation: true, + relationType: "categories", + }, + { + value: "product.categories.name", + label: "Category Name", + type: "array", + isRelation: true, + relationType: "categories", + }, + { + value: "product.categories.handle", + label: "Category Handle", + type: "array", + isRelation: true, + relationType: "categories", + }, + // Relations - Variants + { + value: "product.variants.id", + label: "Variant ID", + type: "array", + isRelation: true, + relationType: "variants", + }, + { + value: "product.variants.sku", + label: "Variant SKU", + type: "array", + isRelation: true, + relationType: "variants", + }, + { + value: "product.variants.title", + label: "Variant Title", + type: "array", + isRelation: true, + relationType: "variants", + }, + // Relations - Type + { + value: "product.type.id", + label: "Type ID", + type: "object", + isRelation: true, + relationType: "type", + }, + { + value: "product.type.value", + label: "Type Value", + type: "object", + isRelation: true, + relationType: "type", + }, + // Relations - Collection + { + value: "product.collection.id", + label: "Collection ID", + type: "object", + isRelation: true, + relationType: "collection", + }, + { + value: "product.collection.title", + label: "Collection Title", + type: "object", + isRelation: true, + relationType: "collection", + }, + { + value: "product.collection.handle", + label: "Collection Handle", + type: "object", + isRelation: true, + relationType: "collection", + }, ] \ No newline at end of file diff --git a/src/subscribers/product-updated.ts b/src/subscribers/product-updated.ts index 4588e2f..aab86e9 100644 --- a/src/subscribers/product-updated.ts +++ b/src/subscribers/product-updated.ts @@ -36,6 +36,8 @@ export default async function productUpdatedHandler({ product: product, } + console.log(contextData) + // Run automation workflow - this will: // 1. Retrieve triggers for the event // 2. Validate triggers against context diff --git a/src/utils/validate-rules.ts b/src/utils/validate-rules.ts index baef3dc..bbbe882 100644 --- a/src/utils/validate-rules.ts +++ b/src/utils/validate-rules.ts @@ -57,6 +57,34 @@ export function validateRuleValueCondition( (val) => !ruleValueSet.has(`${val}`) ) } + case "nin": { + const ruleValueSet = new Set(ruleValues) + return valuesToCheck.every( + (val) => !ruleValueSet.has(`${val}`) + ) + } + case "contains": { + // Check if array contains any of the rule values + const ruleValueSet = new Set(ruleValues) + return valuesToCheck.some((val) => + ruleValueSet.has(`${val}`) + ) + } + case "not_contains": { + // Check if array does NOT contain any of the rule values + const ruleValueSet = new Set(ruleValues) + return !valuesToCheck.some((val) => + ruleValueSet.has(`${val}`) + ) + } + case "empty": { + // Check if array is empty + return valuesToCheck.length === 0 + } + case "not_empty": { + // Check if array is not empty + return valuesToCheck.length > 0 + } case "gt": return valuesToCheck.every((val) => ruleValues.some((ruleVal) => @@ -104,8 +132,20 @@ export function validateRulesForContext( const validRuleValues: string[] = [] for (const value of rule.rule_values) { - if (value.value && isString(value.value)) { - validRuleValues.push(value.value) + if (value.value !== null && value.value !== undefined) { + // Handle different value types + if (isString(value.value)) { + validRuleValues.push(value.value) + } else if (typeof value.value === "number") { + validRuleValues.push(`${value.value}`) + } else if (Array.isArray(value.value)) { + // If value is an array, convert all elements to strings + value.value.forEach((item) => { + if (item !== null && item !== undefined) { + validRuleValues.push(`${item}`) + } + }) + } } } diff --git a/src/workflows/product/steps/get-product-by-id.ts b/src/workflows/product/steps/get-product-by-id.ts index a50f20f..5d64632 100644 --- a/src/workflows/product/steps/get-product-by-id.ts +++ b/src/workflows/product/steps/get-product-by-id.ts @@ -70,30 +70,25 @@ export const getProductByIdStep = createStep( "metadata", "created_at", "updated_at", - "variants.id", - "variants.title", - "variants.sku", - "variants.barcode", - "variants.ean", - "variants.upc", - "variants.inventory_quantity", - "variants.allow_backorder", - "variants.manage_inventory", - "variants.weight", - "variants.length", - "variants.height", - "variants.width", - "variants.metadata", - "images.id", - "images.url", - "images.metadata", + "deleted_at", + // Relations - Tags "tags.id", "tags.value", + // Relations - Categories "categories.id", "categories.name", "categories.handle", - "categories.description", - "categories.metadata", + // Relations - Variants + "variants.id", + "variants.sku", + "variants.title", + // Relations - Type + "type.id", + "type.value", + // Relations - Collection + "collection.id", + "collection.title", + "collection.handle", ], filters: { id: { From 1eaad83fb826483176d37ddbc4d8f83ac81b154f Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Wed, 17 Dec 2025 20:33:54 +0100 Subject: [PATCH 03/12] feat: Refactor automation rules form to improve value input handling - Replaced the inline value input with a dedicated RuleValueInput component - Updated the schema documentation to clarify that the 'value' can also be null, - Changed button label from "Add Item" to "Add condition" --- .../automations-rules-form/index.tsx | 28 ++---- .../rule-value-input/index.ts | 2 + .../rule-value-input/rule-value-input.tsx | 92 +++++++++++++++++++ .../automations-form/types/schema.ts | 15 ++- 4 files changed, 112 insertions(+), 25 deletions(-) create mode 100644 src/admin/automations/automations-form/automations-rules-form/rule-value-input/index.ts create mode 100644 src/admin/automations/automations-form/automations-rules-form/rule-value-input/rule-value-input.tsx diff --git a/src/admin/automations/automations-form/automations-rules-form/index.tsx b/src/admin/automations/automations-form/automations-rules-form/index.tsx index 0a1927e..915cc9f 100644 --- a/src/admin/automations/automations-form/automations-rules-form/index.tsx +++ b/src/admin/automations/automations-form/automations-rules-form/index.tsx @@ -1,9 +1,10 @@ -import { Input, Label, Select, Button } from "@medusajs/ui" +import { Label, Select, Button } from "@medusajs/ui" import { useAvailableEvents } from "../../../../hooks/api/available-events" import { OPERATOR_TYPES } from "../../../../modules/mpn-automation/types/types" import { Controller, useFieldArray } from "react-hook-form" import { useMemo } from "react" import { Trash, Plus } from "@medusajs/icons" +import { RuleValueInput } from "./rule-value-input" export function AutomationsRulesForm({ form, @@ -163,27 +164,10 @@ export function AutomationsRulesForm({ )} /> - ( - <> - - { - field.onChange(e.target.value) - }} - onBlur={field.onBlur} - ref={field.ref} - /> - {fieldState.error && ( - - {fieldState.error.message} - - )} - - )} + name={`rules.items.${index}.rule_values.0.value`} + operator={form.watch(`rules.items.${index}.operator`)} /> diff --git a/src/admin/automations/automations-form/automations-rules-form/rule-value-input/index.ts b/src/admin/automations/automations-form/automations-rules-form/rule-value-input/index.ts new file mode 100644 index 0000000..875560e --- /dev/null +++ b/src/admin/automations/automations-form/automations-rules-form/rule-value-input/index.ts @@ -0,0 +1,2 @@ +export { RuleValueInput } from "./rule-value-input" + diff --git a/src/admin/automations/automations-form/automations-rules-form/rule-value-input/rule-value-input.tsx b/src/admin/automations/automations-form/automations-rules-form/rule-value-input/rule-value-input.tsx new file mode 100644 index 0000000..dd3d665 --- /dev/null +++ b/src/admin/automations/automations-form/automations-rules-form/rule-value-input/rule-value-input.tsx @@ -0,0 +1,92 @@ +import { Input, Label } from "@medusajs/ui" +import { Controller, Control } from "react-hook-form" +import { OperatorType } from "../../../../../modules/mpn-automation/types/types" +import { ChipInput } from "../../../../components/inputs/chip-input" + +type RuleValueInputProps = { + control: Control + name: string + operator: string +} + +export function RuleValueInput({ + control, + name, + operator, +}: RuleValueInputProps) { + const arrayOperators = [ + OperatorType.IN, + OperatorType.NOT_IN, + OperatorType.CONTAINS, + OperatorType.NOT_CONTAINS, + ] + + const noValueOperators = [ + OperatorType.EMPTY, + OperatorType.NOT_EMPTY, + ] + + const isArrayOperator = arrayOperators.includes(operator as OperatorType) + const isNoValueOperator = noValueOperators.includes(operator as OperatorType) + + if (isNoValueOperator) { + return null + } + + return ( + { + if (isArrayOperator) { + const arrayValue = Array.isArray(field.value) + ? field.value + : field.value + ? [String(field.value)] + : [] + + return ( + <> + + field.onChange(values)} + onBlur={field.onBlur} + placeholder="Add values (press Enter or comma)" + allowDuplicates={false} + /> + {fieldState.error && ( + + {fieldState.error.message} + + )} + + ) + } + + const stringValue = Array.isArray(field.value) + ? field.value[0] || "" + : field.value ?? "" + + return ( + <> + + field.onChange(e.target.value)} + onBlur={field.onBlur} + ref={field.ref} + placeholder="Enter value" + /> + {fieldState.error && ( + + {fieldState.error.message} + + )} + + ) + }} + /> + ) +} + diff --git a/src/admin/automations/automations-form/types/schema.ts b/src/admin/automations/automations-form/types/schema.ts index 111b792..2e6dc6a 100644 --- a/src/admin/automations/automations-form/types/schema.ts +++ b/src/admin/automations/automations-form/types/schema.ts @@ -32,14 +32,23 @@ export const baseAutomationFormSchema = z.object({ .array( z.object({ id: z.string().optional(), - // Value can be string, number, or array (for array operators like contains, in, etc.) + // Value can be string, number, array, or null (for empty/not_empty operators) value: z .union([ z.string().min(1, "Value is required"), z.number(), - z.array(z.string().min(1)), + z.array( + z + .string() + .min( + 1, + "Array values cannot be empty" + ) + ), z.array(z.number()), - ]), + z.null(), + ]) + .optional(), }) ) .optional(), From 5303a0c05c09f127c1c9777a18b31be25191ca3e Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Wed, 17 Dec 2025 20:34:31 +0100 Subject: [PATCH 04/12] fix: Correct import order in automation form schema file --- .../automations-form/utils/automation-form-schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/admin/automations/automations-form/utils/automation-form-schema.ts b/src/admin/automations/automations-form/utils/automation-form-schema.ts index dc7448d..efa98e9 100644 --- a/src/admin/automations/automations-form/utils/automation-form-schema.ts +++ b/src/admin/automations/automations-form/utils/automation-form-schema.ts @@ -1,5 +1,5 @@ -import { baseAutomationFormSchema } from "../types" import { z } from "zod" +import { baseAutomationFormSchema } from "../types" // Function to create schema with dynamic validation based on availableActions export function createAutomationFormSchema( From 98c886fe109ffb5a6158d13951e1f3b8f8a20dd9 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Wed, 17 Dec 2025 20:55:06 +0100 Subject: [PATCH 05/12] - Added a new utility function `getFieldsFromAttributes` to field extraction from product attributes. - Updated `get-product-by-id` and `get-product-variant-by-id` steps for dynamic field generation. --- .../types/modules/product/product.ts | 9 --- src/utils/attribute-helpers.ts | 19 +++++++ src/utils/index.ts | 1 + .../steps/get-product-variant-by-id.ts | 10 +--- .../product/steps/get-product-by-id.ts | 56 ++++--------------- 5 files changed, 31 insertions(+), 64 deletions(-) create mode 100644 src/utils/attribute-helpers.ts diff --git a/src/modules/mpn-automation/types/modules/product/product.ts b/src/modules/mpn-automation/types/modules/product/product.ts index 1afbeb6..742a877 100644 --- a/src/modules/mpn-automation/types/modules/product/product.ts +++ b/src/modules/mpn-automation/types/modules/product/product.ts @@ -79,10 +79,6 @@ export const PRODUCT_ATTRIBUTES = [ value: "product.width", label: "Width", }, - // { - // value: "product.metadata", - // label: "Metadata", - // }, { value: "product.created_at", label: "Created At", @@ -95,7 +91,6 @@ export const PRODUCT_ATTRIBUTES = [ value: "product.deleted_at", label: "Deleted At", }, - // Relations - Tags { value: "product.tags.id", label: "Tag ID", @@ -110,7 +105,6 @@ export const PRODUCT_ATTRIBUTES = [ isRelation: true, relationType: "tags", }, - // Relations - Categories { value: "product.categories.id", label: "Category ID", @@ -132,7 +126,6 @@ export const PRODUCT_ATTRIBUTES = [ isRelation: true, relationType: "categories", }, - // Relations - Variants { value: "product.variants.id", label: "Variant ID", @@ -154,7 +147,6 @@ export const PRODUCT_ATTRIBUTES = [ isRelation: true, relationType: "variants", }, - // Relations - Type { value: "product.type.id", label: "Type ID", @@ -169,7 +161,6 @@ export const PRODUCT_ATTRIBUTES = [ isRelation: true, relationType: "type", }, - // Relations - Collection { value: "product.collection.id", label: "Collection ID", diff --git a/src/utils/attribute-helpers.ts b/src/utils/attribute-helpers.ts new file mode 100644 index 0000000..5093325 --- /dev/null +++ b/src/utils/attribute-helpers.ts @@ -0,0 +1,19 @@ +/** + * Extract fields from attributes for use in query.graph() + * Converts "product.tags.value" -> "tags.value" + */ +export function getFieldsFromAttributes( + attributes: Array<{ value?: string }>, + entityPrefix: string = "product" +): string[] { + return attributes + .map((attr) => attr.value) + .filter((value): value is string => !!value) + .map((value) => + value.startsWith(`${entityPrefix}.`) + ? value.slice(entityPrefix.length + 1) + : value + ) + .sort() +} + diff --git a/src/utils/index.ts b/src/utils/index.ts index 7bdb8f5..2dc7b1c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export * from "./plugins" export * from "./validate-rules" export * from "./types" export * from "./i18n" +export * from "./attribute-helpers" diff --git a/src/workflows/product-variant/steps/get-product-variant-by-id.ts b/src/workflows/product-variant/steps/get-product-variant-by-id.ts index 676796e..63b0646 100644 --- a/src/workflows/product-variant/steps/get-product-variant-by-id.ts +++ b/src/workflows/product-variant/steps/get-product-variant-by-id.ts @@ -68,15 +68,7 @@ export const getProductVariantByIdStep = createStep( "mid_code", "hs_code", "material", - "metadata", - "product.id", - "product.title", - "product.description", - "product.handle", - "product.is_giftcard", - "product.status", - "product.images", - "product.metadata", + "product.*", ], filters: { id: { diff --git a/src/workflows/product/steps/get-product-by-id.ts b/src/workflows/product/steps/get-product-by-id.ts index 5d64632..228f38d 100644 --- a/src/workflows/product/steps/get-product-by-id.ts +++ b/src/workflows/product/steps/get-product-by-id.ts @@ -7,18 +7,15 @@ import { StepResponse, createStep, } from "@medusajs/framework/workflows-sdk" +import { PRODUCT_ATTRIBUTES } from "../../../modules/mpn-automation/types/modules/product" +import { getFieldsFromAttributes } from "../../../utils" export interface GetProductByIdStepInput { product_id: string } export interface GetProductByIdStepOutput { - product: ProductTypes.ProductDTO & { - variants?: ProductTypes.ProductVariantDTO[] - images?: ProductTypes.ProductImageDTO[] - tags?: ProductTypes.ProductTagDTO[] - categories?: ProductTypes.ProductCategoryDTO[] - } + product: ProductTypes.ProductDTO } export const getProductByIdStepId = "get-product-by-id" @@ -48,48 +45,15 @@ export const getProductByIdStep = createStep( ) } + // Generate fields from PRODUCT_ATTRIBUTES to keep them in sync + const fields = getFieldsFromAttributes( + PRODUCT_ATTRIBUTES, + "product" + ) + const { data: products } = await query.graph({ entity: "product", - fields: [ - "id", - "title", - "subtitle", - "description", - "handle", - "is_giftcard", - "status", - "thumbnail", - "weight", - "length", - "height", - "width", - "origin_country", - "hs_code", - "mid_code", - "material", - "metadata", - "created_at", - "updated_at", - "deleted_at", - // Relations - Tags - "tags.id", - "tags.value", - // Relations - Categories - "categories.id", - "categories.name", - "categories.handle", - // Relations - Variants - "variants.id", - "variants.sku", - "variants.title", - // Relations - Type - "type.id", - "type.value", - // Relations - Collection - "collection.id", - "collection.title", - "collection.handle", - ], + fields, filters: { id: { $in: [input.product_id], From e7ac64d855f6b9ee2a41eb9cf9c46194c7c05da9 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Wed, 17 Dec 2025 20:59:25 +0100 Subject: [PATCH 06/12] - Add stock location attributes to inventory level - Introduced new attributes for stock location ID and name - Updated the getInventoryLevelById step to dynamically generate fields based on the new attributes for improved data retrieval. --- .../types/modules/inventory/inventory.ts | 14 ++++++++++++ .../steps/get-inventory-level-by-id.ts | 22 ++++++++----------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/modules/mpn-automation/types/modules/inventory/inventory.ts b/src/modules/mpn-automation/types/modules/inventory/inventory.ts index eb8ed5b..986efa6 100644 --- a/src/modules/mpn-automation/types/modules/inventory/inventory.ts +++ b/src/modules/mpn-automation/types/modules/inventory/inventory.ts @@ -51,4 +51,18 @@ export const INVENTORY_LEVEL_ATTRIBUTES = [ value: "inventory_level.location_id", label: "Location ID", }, + { + value: "inventory_level.stock_locations.id", + label: "Stock Location ID", + type: "array", + isRelation: true, + relationType: "stock_locations", + }, + { + value: "inventory_level.stock_locations.name", + label: "Stock Location Name", + type: "array", + isRelation: true, + relationType: "stock_locations", + } ] \ No newline at end of file diff --git a/src/workflows/inventory/steps/get-inventory-level-by-id.ts b/src/workflows/inventory/steps/get-inventory-level-by-id.ts index 8ac92af..16ce925 100644 --- a/src/workflows/inventory/steps/get-inventory-level-by-id.ts +++ b/src/workflows/inventory/steps/get-inventory-level-by-id.ts @@ -10,6 +10,8 @@ import { StepResponse, createStep, } from "@medusajs/framework/workflows-sdk" +import { INVENTORY_LEVEL_ATTRIBUTES } from "../../../modules/mpn-automation/types/modules/inventory" +import { getFieldsFromAttributes } from "../../../utils" export interface GetInventoryLevelByIdStepInput { inventory_level_id: string @@ -52,21 +54,15 @@ export const getInventoryLevelByIdStep = createStep( ) } + // Generate fields from INVENTORY_LEVEL_ATTRIBUTES to keep them in sync + const fields = getFieldsFromAttributes( + INVENTORY_LEVEL_ATTRIBUTES as Array<{ value?: string }>, + "inventory_level" + ) + const { data: inventoryLevels } = await query.graph({ entity: "inventory_level", - fields: [ - "id", - "inventory_item.*", - "stocked_quantity", - "reserved_quantity", - "incoming_quantity", - "available_quantity", - "location_id", - "stock_locations.id", - "stock_locations.name", - "stock_locations.address", - "stock_locations.metadata", - ], + fields, filters: { id: { $in: [input.inventory_level_id], From 9f823ebdcee75eba7c09f44a3688a79b17db8606 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Wed, 17 Dec 2025 21:12:58 +0100 Subject: [PATCH 07/12] Update package.json to include Prettier as a development dependency - Added Prettier version 3.0.0 to the project for consistent code formatting. - Refactored several files to improve code readability and maintainability by applying Prettier formatting rules. --- package.json | 1 + .../automations-actions-form/index.tsx | 23 +- .../automations-create-form/index.tsx | 4 +- .../automations-edit-form/index.tsx | 27 +- .../automations-rules-form/index.tsx | 9 +- .../rule-value-input/index.ts | 1 - .../rule-value-input/rule-value-input.tsx | 19 +- .../automations-list/automations-list.tsx | 234 +++++++++--------- src/admin/utils/dynamic-component.tsx | 6 +- .../automations/available-actions/route.ts | 7 +- .../migrations/Migration20251217190839.ts | 12 +- .../services/base-action-service.ts | 8 +- .../services/email-action-service.ts | 13 +- .../mpn-automation/services/service.ts | 183 +++++++++----- .../services/slack-action-service.ts | 21 +- .../mpn-automation/types/action-handler.ts | 9 +- src/modules/mpn-automation/types/index.ts | 2 +- .../mpn-automation/types/modules/index.ts | 5 +- .../types/modules/inventory/index.ts | 2 +- .../types/modules/inventory/inventory.ts | 5 +- .../types/modules/product-category/index.ts | 2 +- .../product-category/product-category.ts | 1 - .../types/modules/product-tag/index.ts | 2 +- .../types/modules/product-tag/product-tag.ts | 2 +- .../types/modules/product-type/index.ts | 2 +- .../modules/product-type/product-type.ts | 2 +- .../types/modules/product-variant/index.ts | 2 +- .../product-variant/product-variant.ts | 2 +- .../types/modules/product/index.ts | 2 +- .../types/modules/product/product.ts | 2 +- src/modules/mpn-automation/types/types.ts | 2 +- src/providers/slack/service.ts | 4 +- .../inventory-reservation-item-updated.ts | 4 +- .../mpn.automation.action.email.executed.ts | 9 +- .../mpn.automation.action.slack.executed.ts | 14 +- src/subscribers/order-placed.ts | 6 +- src/templates/slack/inventory-level/index.ts | 2 +- .../slack/inventory-level/inventory-level.ts | 10 +- .../inventory-level/translations/index.ts | 9 +- src/templates/slack/product-variant/index.ts | 2 +- .../slack/product-variant/product-variant.ts | 13 +- .../product-variant/translations/index.ts | 9 +- src/templates/slack/product/index.ts | 2 +- src/templates/slack/product/product.ts | 14 +- .../slack/product/translations/index.ts | 9 +- src/utils/attribute-helpers.ts | 1 - src/utils/i18n/i18n.ts | 103 ++++---- src/utils/i18n/index.ts | 2 +- src/utils/types/index.ts | 2 +- src/utils/validate-rules.ts | 5 +- .../steps/get-inventory-level-by-id.ts | 4 +- src/workflows/mpn-automation/index.ts | 2 +- .../mpn-automation/run-email-action.ts | 6 +- .../mpn-automation/run-slack-action.ts | 6 +- .../steps/edit-automation-actions.ts | 22 +- .../steps/run-automation-actions.ts | 3 +- .../notifications/steps/send-email.ts | 32 ++- .../notifications/steps/send-slack.ts | 34 +-- 58 files changed, 557 insertions(+), 384 deletions(-) diff --git a/package.json b/package.json index 20bdc44..0935716 100755 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@types/react-dom": "^18.2.25", "awilix": "^8.0.1", "dotenv": "^17.2.3", + "prettier": "^3.0.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/admin/automations/automations-form/automations-actions-form/index.tsx b/src/admin/automations/automations-form/automations-actions-form/index.tsx index 6a888b0..bb1c252 100644 --- a/src/admin/automations/automations-form/automations-actions-form/index.tsx +++ b/src/admin/automations/automations-form/automations-actions-form/index.tsx @@ -16,7 +16,6 @@ export function AutomationsActionsForm({ isOpen?: boolean availableActionsData?: any }) { - // Reset action configs when eventName changes to ensure templates are updated // useEffect(() => { // const actions = form.getValues("actions.items") || [] @@ -70,11 +69,15 @@ export function AutomationsActionsForm({ return } - form.setValue(`actions.items.${index}.action_type`, value, { - shouldValidate: false, - shouldDirty: true, - }) - + form.setValue( + `actions.items.${index}.action_type`, + value, + { + shouldValidate: false, + shouldDirty: true, + } + ) + // Clear validation errors first to prevent showing errors from previous action type form.clearErrors(`actions.items.${index}`) @@ -83,7 +86,7 @@ export function AutomationsActionsForm({ (a) => a.value === value ) const fields = actionData?.fields - + // Reset config when action type changes to prevent sending // fields from previous action type in payload form.setValue( @@ -158,7 +161,11 @@ export function AutomationsActionsForm({ actionTypeField.value ?? "" } onValueChange={(value) => { - actionTypeValueChange(index, value, isExistingAction) + actionTypeValueChange( + index, + value, + isExistingAction + ) }} disabled={isExistingAction} > diff --git a/src/admin/automations/automations-form/automations-create-form/index.tsx b/src/admin/automations/automations-form/automations-create-form/index.tsx index c79b044..7f25536 100644 --- a/src/admin/automations/automations-form/automations-create-form/index.tsx +++ b/src/admin/automations/automations-form/automations-create-form/index.tsx @@ -191,7 +191,9 @@ export function AutomationsCreateForm() { - Create Automation + + Create Automation +
("") - const [eventName, setEventName] = useState(undefined) + const [eventName, setEventName] = useState< + string | undefined + >(undefined) useEffect(() => { if (Tab.GENERAL === tab) { @@ -80,7 +82,7 @@ export function AutomationsEditForm({ const { data: automationsActionsData, - isLoading: isAutomationsActionsLoading + isLoading: isAutomationsActionsLoading, } = useListAutomationsActions({ trigger_id: id, extraKey: [id], @@ -91,7 +93,7 @@ export function AutomationsEditForm({ const { data: availableActionsData } = useAvailableActions({ enabled: open, - eventName: eventName + eventName: eventName, }) const { @@ -181,7 +183,12 @@ export function AutomationsEditForm({ }, }) } - }, [open, automationsTriggerData, automationsRulesData, automationsActionsData]) + }, [ + open, + automationsTriggerData, + automationsRulesData, + automationsActionsData, + ]) // Reset form when modal is closed useEffect(() => { @@ -251,7 +258,7 @@ export function AutomationsEditForm({ } await editAutomationRule(items) - + queryClient.invalidateQueries({ queryKey: ["automations-rules", id], }) @@ -270,7 +277,7 @@ export function AutomationsEditForm({ trigger_id: id, actions: data.actions?.items || [], } - + await editAutomationAction(items) queryClient.invalidateQueries({ @@ -332,7 +339,9 @@ export function AutomationsEditForm({ - Edit Automation + + Edit Automation +
)} diff --git a/src/admin/automations/automations-form/automations-rules-form/index.tsx b/src/admin/automations/automations-form/automations-rules-form/index.tsx index 915cc9f..48a8fa1 100644 --- a/src/admin/automations/automations-form/automations-rules-form/index.tsx +++ b/src/admin/automations/automations-form/automations-rules-form/index.tsx @@ -110,7 +110,10 @@ export function AutomationsRulesForm({ attribute.value || "ss" } > - {attribute.label} ({attribute.value}) + {attribute.label}{" "} + + ({attribute.value}) + ) )} @@ -167,7 +170,9 @@ export function AutomationsRulesForm({