From 9ebab131f8950c43e0c3d8fcde5e38d72de4e213 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Tue, 27 Jan 2026 10:56:35 +0100 Subject: [PATCH 01/41] feat: add mpn-builder module --- src/modules/mpn-builder/index.ts | 8 + .../.snapshot-medusa-mpn-builder.json | 283 ++++++++++++++++++ .../migrations/Migration20260126192907.ts | 26 ++ .../mpn-builder/models/builder_template.ts | 19 ++ .../models/builder_template_block.ts | 13 + src/modules/mpn-builder/models/index.ts | 2 + src/modules/mpn-builder/services/index.ts | 2 + src/modules/mpn-builder/services/service.ts | 36 +++ src/modules/mpn-builder/types/index.ts | 2 + src/modules/mpn-builder/types/interfaces.ts | 0 src/modules/mpn-builder/types/types.ts | 30 ++ 11 files changed, 421 insertions(+) create mode 100755 src/modules/mpn-builder/index.ts create mode 100644 src/modules/mpn-builder/migrations/.snapshot-medusa-mpn-builder.json create mode 100644 src/modules/mpn-builder/migrations/Migration20260126192907.ts create mode 100644 src/modules/mpn-builder/models/builder_template.ts create mode 100644 src/modules/mpn-builder/models/builder_template_block.ts create mode 100644 src/modules/mpn-builder/models/index.ts create mode 100644 src/modules/mpn-builder/services/index.ts create mode 100644 src/modules/mpn-builder/services/service.ts create mode 100644 src/modules/mpn-builder/types/index.ts create mode 100644 src/modules/mpn-builder/types/interfaces.ts create mode 100644 src/modules/mpn-builder/types/types.ts diff --git a/src/modules/mpn-builder/index.ts b/src/modules/mpn-builder/index.ts new file mode 100755 index 0000000..3475e37 --- /dev/null +++ b/src/modules/mpn-builder/index.ts @@ -0,0 +1,8 @@ +import { Module } from "@medusajs/framework/utils" +import { MpnBuilderService } from "./services" + +export const MPN_BUILDER_MODULE = "mpnBuilder" + +export default Module(MPN_BUILDER_MODULE, { + service: MpnBuilderService, +}) diff --git a/src/modules/mpn-builder/migrations/.snapshot-medusa-mpn-builder.json b/src/modules/mpn-builder/migrations/.snapshot-medusa-mpn-builder.json new file mode 100644 index 0000000..1e3d0c5 --- /dev/null +++ b/src/modules/mpn-builder/migrations/.snapshot-medusa-mpn-builder.json @@ -0,0 +1,283 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "name": { + "name": "name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "label": { + "name": "label", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "channel": { + "name": "channel", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "locale": { + "name": "locale", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "true", + "mappedType": "boolean" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "mpn_builder_template", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_mpn_builder_template_name_unique", + "columnNames": [], + "composite": false, + "constraint": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_mpn_builder_template_name_unique\" ON \"mpn_builder_template\" (name) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_mpn_builder_template_deleted_at", + "columnNames": [], + "composite": false, + "constraint": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_mpn_builder_template_deleted_at\" ON \"mpn_builder_template\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "mpn_builder_template_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "type": { + "name": "type", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "position": { + "name": "position", + "type": "integer", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "template_id": { + "name": "template_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "mpn_builder_template_block", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_mpn_builder_template_block_template_id", + "columnNames": [], + "composite": false, + "constraint": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_mpn_builder_template_block_template_id\" ON \"mpn_builder_template_block\" (template_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_mpn_builder_template_block_deleted_at", + "columnNames": [], + "composite": false, + "constraint": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_mpn_builder_template_block_deleted_at\" ON \"mpn_builder_template_block\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "mpn_builder_template_block_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "mpn_builder_template_block_template_id_foreign": { + "constraintName": "mpn_builder_template_block_template_id_foreign", + "columnNames": [ + "template_id" + ], + "localTableName": "public.mpn_builder_template_block", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.mpn_builder_template", + "updateRule": "cascade" + } + }, + "nativeEnums": {} + } + ], + "nativeEnums": {} +} diff --git a/src/modules/mpn-builder/migrations/Migration20260126192907.ts b/src/modules/mpn-builder/migrations/Migration20260126192907.ts new file mode 100644 index 0000000..7ade4f7 --- /dev/null +++ b/src/modules/mpn-builder/migrations/Migration20260126192907.ts @@ -0,0 +1,26 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260126192907 extends Migration { + + override async up(): Promise { + this.addSql(`alter table if exists "mpn_builder_template" drop constraint if exists "mpn_builder_template_name_unique";`); + this.addSql(`create table if not exists "mpn_builder_template" ("id" text not null, "name" text not null, "label" text null, "description" text null, "channel" text not null, "locale" text not null, "is_active" boolean not null default true, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "mpn_builder_template_pkey" primary key ("id"));`); + this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_mpn_builder_template_name_unique" ON "mpn_builder_template" (name) WHERE deleted_at IS NULL;`); + this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_mpn_builder_template_deleted_at" ON "mpn_builder_template" (deleted_at) WHERE deleted_at IS NULL;`); + + this.addSql(`create table if not exists "mpn_builder_template_block" ("id" text not null, "type" text not null, "parent_id" text null, "position" integer not null default 0, "metadata" jsonb null, "template_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "mpn_builder_template_block_pkey" primary key ("id"));`); + this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_mpn_builder_template_block_template_id" ON "mpn_builder_template_block" (template_id) WHERE deleted_at IS NULL;`); + this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_mpn_builder_template_block_deleted_at" ON "mpn_builder_template_block" (deleted_at) WHERE deleted_at IS NULL;`); + + this.addSql(`alter table if exists "mpn_builder_template_block" add constraint "mpn_builder_template_block_template_id_foreign" foreign key ("template_id") references "mpn_builder_template" ("id") on update cascade;`); + } + + override async down(): Promise { + this.addSql(`alter table if exists "mpn_builder_template_block" drop constraint if exists "mpn_builder_template_block_template_id_foreign";`); + + this.addSql(`drop table if exists "mpn_builder_template" cascade;`); + + this.addSql(`drop table if exists "mpn_builder_template_block" cascade;`); + } + +} diff --git a/src/modules/mpn-builder/models/builder_template.ts b/src/modules/mpn-builder/models/builder_template.ts new file mode 100644 index 0000000..43416a8 --- /dev/null +++ b/src/modules/mpn-builder/models/builder_template.ts @@ -0,0 +1,19 @@ +import { model } from "@medusajs/framework/utils" +import MpnBuilderTemplateBlock from "./builder_template_block" + +const MpnBuilderTemplate = model.define("mpn_builder_template", { + id: model.id().primaryKey(), + name: model.text().unique(), + label: model.text().nullable(), + description: model.text().nullable(), + channel: model.text(), // "email" | "slack" | "sms" + locale: model.text(), // "pl" | "en" | ... + // event_type: model.text().nullable(), // "order.placed", "inventory.updated" + // subject: model.text().nullable(), // dla email + is_active: model.boolean().default(true), + blocks: model.hasMany(() => MpnBuilderTemplateBlock, { + mappedBy: "template", + }), +}) + +export default MpnBuilderTemplate diff --git a/src/modules/mpn-builder/models/builder_template_block.ts b/src/modules/mpn-builder/models/builder_template_block.ts new file mode 100644 index 0000000..d00c848 --- /dev/null +++ b/src/modules/mpn-builder/models/builder_template_block.ts @@ -0,0 +1,13 @@ +import { model } from "@medusajs/framework/utils" +import MpnBuilderTemplate from "./builder_template" + +const MpnBuilderTemplateBlock = model.define("mpn_builder_template_block", { + id: model.id().primaryKey(), + type: model.text(), // "group", "heading", "text", "row", "separator", "repeater", "table", etc. + parent_id: model.text().nullable(), // self-reference dla hierarchii + position: model.number().default(0), + metadata: model.json().nullable(), + template: model.belongsTo(() => MpnBuilderTemplate, { mappedBy: "blocks" }), +}) + +export default MpnBuilderTemplateBlock diff --git a/src/modules/mpn-builder/models/index.ts b/src/modules/mpn-builder/models/index.ts new file mode 100644 index 0000000..b99cc0d --- /dev/null +++ b/src/modules/mpn-builder/models/index.ts @@ -0,0 +1,2 @@ +export { default as MpnBuilderTemplate } from "./builder_template" +export { default as MpnBuilderTemplateBlock } from "./builder_template_block" diff --git a/src/modules/mpn-builder/services/index.ts b/src/modules/mpn-builder/services/index.ts new file mode 100644 index 0000000..463209c --- /dev/null +++ b/src/modules/mpn-builder/services/index.ts @@ -0,0 +1,2 @@ +import { default as MpnBuilderService } from "./service" +export { MpnBuilderService } \ No newline at end of file diff --git a/src/modules/mpn-builder/services/service.ts b/src/modules/mpn-builder/services/service.ts new file mode 100644 index 0000000..23b6739 --- /dev/null +++ b/src/modules/mpn-builder/services/service.ts @@ -0,0 +1,36 @@ +import { + MedusaService, + MedusaError, +} from "@medusajs/framework/utils" +import { + MpnBuilderTemplate, + MpnBuilderTemplateBlock, +} from "../models" +import { + ModuleOptions, +} from "../types" +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger +} + +class MpnBuilderService extends MedusaService({ + MpnBuilderTemplate, + MpnBuilderTemplateBlock, +}) { + private options_: ModuleOptions + private logger_: Logger + + constructor( + { logger }: InjectedDependencies, + options?: ModuleOptions + ) { + super(...arguments) + + this.logger_ = logger + this.options_ = options || {} + } +} + +export default MpnBuilderService diff --git a/src/modules/mpn-builder/types/index.ts b/src/modules/mpn-builder/types/index.ts new file mode 100644 index 0000000..326299b --- /dev/null +++ b/src/modules/mpn-builder/types/index.ts @@ -0,0 +1,2 @@ +export * from "./types" +export * from "./interfaces" diff --git a/src/modules/mpn-builder/types/interfaces.ts b/src/modules/mpn-builder/types/interfaces.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/mpn-builder/types/types.ts b/src/modules/mpn-builder/types/types.ts new file mode 100644 index 0000000..a309e84 --- /dev/null +++ b/src/modules/mpn-builder/types/types.ts @@ -0,0 +1,30 @@ +export interface FieldConfig { + name: string + key: string + label: string + description?: string + type: + | "text" + | "textarea" + | "number" + | "select" + | "checkbox" + | "date" + | "chip-input" + | "email" + | "password" + | "file" + placeholder?: string + required?: boolean + defaultValue?: any + options?: Array<{ value: string; name: string }> + min?: number + max?: number + step?: number +} + +export type ModuleOptions = { + builder?: { + + } +} \ No newline at end of file From f5d543b9438e5fca727eec157f3c35035d666bb4 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Tue, 27 Jan 2026 18:35:26 +0100 Subject: [PATCH 02/41] - add template builder - refactor routes with a new urls --- .gitignore | 1 + package.json | 6 + .../routes/mpn/notifications/list/page.tsx | 31 ++ .../list => mpn/notifications}/page.tsx | 0 .../notifications}/render/page.tsx | 8 +- src/admin/routes/mpn/page.tsx | 21 ++ src/admin/routes/mpn/templates/page.tsx | 22 ++ .../routes/notifications-emails/page.tsx | 17 -- .../automations-actions-form/index.tsx | 264 ++++++++++++++++++ src/admin/templates/templates-form/index.tsx | 2 + .../templates-create-form/index.tsx | 186 ++++++++++++ .../templates-edit-form/index.tsx | 212 ++++++++++++++ .../templates-general-form/index.tsx | 214 ++++++++++++++ .../templates/templates-form/types/index.ts | 2 + .../templates/templates-form/types/schema.ts | 24 ++ .../templates/templates-form/types/types.ts | 13 + .../template-delete-button/index.tsx | 55 ++++ src/admin/templates/templates-list/index.tsx | 1 + .../templates-list/templates-list.tsx | 196 +++++++++++++ .../events/route.ts | 0 .../notifications/route.ts | 0 .../render-template/route.ts | 0 src/api/admin/mpn/templates/route.ts | 119 ++++++++ src/api/middlewares.ts | 34 ++- src/hooks/api/events.ts | 2 +- src/hooks/api/notifications.ts | 2 +- src/hooks/api/preview.ts | 2 +- src/hooks/api/templates/index.ts | 1 + src/hooks/api/templates/templates.ts | 135 +++++++++ src/modules/mpn-builder/types/interfaces.ts | 19 ++ .../mpn-templates/create-template.ts | 23 ++ .../mpn-templates/delete-template.ts | 22 ++ src/workflows/mpn-templates/edit-template.ts | 24 ++ src/workflows/mpn-templates/index.ts | 3 + .../mpn-templates/steps/create-template.ts | 37 +++ .../mpn-templates/steps/delete-template.ts | 31 ++ .../mpn-templates/steps/edit-template.ts | 36 +++ src/workflows/mpn-templates/steps/index.ts | 3 + src/workflows/steps/log-step.ts | 21 ++ 39 files changed, 1764 insertions(+), 25 deletions(-) create mode 100755 src/admin/routes/mpn/notifications/list/page.tsx rename src/admin/routes/{notifications-emails/list => mpn/notifications}/page.tsx (100%) rename src/admin/routes/{notifications-emails => mpn/notifications}/render/page.tsx (88%) create mode 100644 src/admin/routes/mpn/page.tsx create mode 100644 src/admin/routes/mpn/templates/page.tsx delete mode 100755 src/admin/routes/notifications-emails/page.tsx create mode 100644 src/admin/templates/templates-form/automations-actions-form/index.tsx create mode 100644 src/admin/templates/templates-form/index.tsx create mode 100644 src/admin/templates/templates-form/templates-create-form/index.tsx create mode 100644 src/admin/templates/templates-form/templates-edit-form/index.tsx create mode 100644 src/admin/templates/templates-form/templates-general-form/index.tsx create mode 100644 src/admin/templates/templates-form/types/index.ts create mode 100644 src/admin/templates/templates-form/types/schema.ts create mode 100644 src/admin/templates/templates-form/types/types.ts create mode 100644 src/admin/templates/templates-list/components/template-delete-button/index.tsx create mode 100644 src/admin/templates/templates-list/index.tsx create mode 100644 src/admin/templates/templates-list/templates-list.tsx rename src/api/admin/{notification-plugin => mpn}/events/route.ts (100%) rename src/api/admin/{notification-plugin => mpn}/notifications/route.ts (100%) rename src/api/admin/{notification-plugin => mpn}/render-template/route.ts (100%) create mode 100644 src/api/admin/mpn/templates/route.ts create mode 100644 src/hooks/api/templates/index.ts create mode 100644 src/hooks/api/templates/templates.ts create mode 100644 src/workflows/mpn-templates/create-template.ts create mode 100644 src/workflows/mpn-templates/delete-template.ts create mode 100644 src/workflows/mpn-templates/edit-template.ts create mode 100644 src/workflows/mpn-templates/index.ts create mode 100644 src/workflows/mpn-templates/steps/create-template.ts create mode 100644 src/workflows/mpn-templates/steps/delete-template.ts create mode 100644 src/workflows/mpn-templates/steps/edit-template.ts create mode 100644 src/workflows/mpn-templates/steps/index.ts create mode 100644 src/workflows/steps/log-step.ts diff --git a/.gitignore b/.gitignore index 9145ace..83fc0d0 100755 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ yarn-error.log* # Notes and internal docs things/ +builder-lexical/ diff --git a/package.json b/package.json index efeb992..a6d12af 100755 --- a/package.json +++ b/package.json @@ -52,9 +52,15 @@ "release:manual": "npm run build && npm publish --access public" }, "dependencies": { + "@lexical/extension": "^0.39.0", + "@lexical/history": "^0.39.0", + "@lexical/html": "^0.39.0", + "@lexical/react": "^0.39.0", + "@lexical/rich-text": "^0.39.0", "@react-email/components": "^0.5.7", "@react-email/preview-server": "^4.3.1", "@react-email/render": "^1.4.0", + "lexical": "^0.39.0", "react-email": "^4.3.1" }, "devDependencies": { diff --git a/src/admin/routes/mpn/notifications/list/page.tsx b/src/admin/routes/mpn/notifications/list/page.tsx new file mode 100755 index 0000000..6b26d24 --- /dev/null +++ b/src/admin/routes/mpn/notifications/list/page.tsx @@ -0,0 +1,31 @@ +import { useState } from "react" // TODO: Remove this +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ListBullet } from "@medusajs/icons" +import { + Container, + Heading, + Select, + Text, +} from "@medusajs/ui" +import { SingleColumnPage } from "../../../../components/layout/pages" +import { NotificationsFullList } from "../../../../notifications/notifications-full-list" + +const ListPage = () => { + return ( + + + + ) +} + +export const config = defineRouteConfig({ + label: "List", + icon: ListBullet, +}) + +export default ListPage diff --git a/src/admin/routes/notifications-emails/list/page.tsx b/src/admin/routes/mpn/notifications/page.tsx similarity index 100% rename from src/admin/routes/notifications-emails/list/page.tsx rename to src/admin/routes/mpn/notifications/page.tsx diff --git a/src/admin/routes/notifications-emails/render/page.tsx b/src/admin/routes/mpn/notifications/render/page.tsx similarity index 88% rename from src/admin/routes/notifications-emails/render/page.tsx rename to src/admin/routes/mpn/notifications/render/page.tsx index 4f58e30..1ddb347 100755 --- a/src/admin/routes/notifications-emails/render/page.tsx +++ b/src/admin/routes/mpn/notifications/render/page.tsx @@ -7,10 +7,10 @@ import { Select, Text, } from "@medusajs/ui" -import { SingleColumnPage } from "../../../components/layout/pages" -import { OrderTemplateGroup } from "../../../notifications-templates/groups/order" -import { ContactFormTemplateGroup } from "../../../notifications-templates/groups/contact-form" -import { TEMPLATES_NAMES } from "../../../../templates/emails/types" +import { SingleColumnPage } from "../../../../components/layout/pages" +import { OrderTemplateGroup } from "../../../../notifications-templates/groups/order" +import { ContactFormTemplateGroup } from "../../../../notifications-templates/groups/contact-form" +import { TEMPLATES_NAMES } from "../../../../../templates/emails/types" const PreviewTemplatePage = () => { const templateName = "" diff --git a/src/admin/routes/mpn/page.tsx b/src/admin/routes/mpn/page.tsx new file mode 100644 index 0000000..a1e4d4c --- /dev/null +++ b/src/admin/routes/mpn/page.tsx @@ -0,0 +1,21 @@ +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { SingleColumnPage } from "../../components/layout/pages" + +const ListPage = () => { + return ( + + Notifications + + ) +} + +export const config = defineRouteConfig({ + label: "Notifications", +}) + +export default ListPage diff --git a/src/admin/routes/mpn/templates/page.tsx b/src/admin/routes/mpn/templates/page.tsx new file mode 100644 index 0000000..6c5978d --- /dev/null +++ b/src/admin/routes/mpn/templates/page.tsx @@ -0,0 +1,22 @@ +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { SingleColumnPage } from "../../../components/layout/pages" +import { TemplatesList } from "../../../templates/templates-list" + +const ListPage = () => { + return ( + + + + ) +} + +export const config = defineRouteConfig({ + label: "Templates", +}) + +export default ListPage diff --git a/src/admin/routes/notifications-emails/page.tsx b/src/admin/routes/notifications-emails/page.tsx deleted file mode 100755 index 28115b0..0000000 --- a/src/admin/routes/notifications-emails/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" - -const NotificationsPage = () => { - return ( -
-

Notifications

-
- ) -} - -export const config = defineRouteConfig({ - label: "Notification Emails", - icon: ChatBubbleLeftRight, -}) - -export default NotificationsPage diff --git a/src/admin/templates/templates-form/automations-actions-form/index.tsx b/src/admin/templates/templates-form/automations-actions-form/index.tsx new file mode 100644 index 0000000..bb1c252 --- /dev/null +++ b/src/admin/templates/templates-form/automations-actions-form/index.tsx @@ -0,0 +1,264 @@ +import { Label, Select, Button } from "@medusajs/ui" +import { + Controller, + useFieldArray, + useWatch, +} from "react-hook-form" +import { Trash, Plus } from "@medusajs/icons" +import LoadActionComponent from "../../../utils/dynamic-component" + +export function AutomationsActionsForm({ + form, + isOpen, + availableActionsData, +}: { + form: any + isOpen?: boolean + availableActionsData?: any +}) { + // Reset action configs when eventName changes to ensure templates are updated + // useEffect(() => { + // const actions = form.getValues("actions.items") || [] + // if (actions.length > 0 && eventName) { + // actions.forEach((_: any, index: number) => { + // const currentConfig = form.getValues(`actions.items.${index}.config`) || {} + // // Only reset templateName if it exists and event changed + // if (currentConfig.templateName) { + // // form.setValue(`actions.items.${index}.config.templateName`, undefined, { + // // shouldValidate: false, + // // shouldDirty: true, + // // }) + // } + // }) + // } + // }, [eventName, form]) + + const { + fields = [], + append, + remove, + } = useFieldArray({ + control: form.control, + name: "actions.items", + }) + + // Watch actual values from form (not just metadata from fields) + const watchedActions = useWatch({ + control: form.control, + name: "actions.items", + }) + + const handleAddAction = () => { + append({ + action_type: "", + config: {}, + }) + } + + const handleRemoveRule = (index: number) => { + remove(index) + } + + const actionTypeValueChange = ( + index: number, + value: string, + isExistingAction: boolean + ) => { + // Don't allow changing action type for existing actions + if (isExistingAction) { + return + } + + 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}`) + + const actionData: any = + availableActionsData?.actions?.find( + (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( + `actions.items.${index}.config`, + fields?.reduce((acc: any, field: any) => { + acc[field.name || field.key] = "" + return acc + }, {}), + { + shouldValidate: false, + shouldDirty: true, + } + ) + } + + return ( +
+
+
+ {fields.length === 0 && ( +
+ No actions added yet. Click "Add Item" to + create a new action. +
+ )} + {fields.map((field, index) => { + return ( + { + const actionType = actionTypeField.value + const actionData: any = + availableActionsData?.actions?.find( + (a) => a.value === actionType + ) + const configComponentKey = + actionData?.configComponentKey + const fields = actionData?.fields + const isEnabled = actionType + ? actionData?.enabled + : true + + // Check if this is an existing action (has id from database) + // watchedActions contains actual form values, including id from DB + const currentAction = + watchedActions?.[index] + const isExistingAction = + !!currentAction?.id + + return ( +
+
+
+ + + {fieldState.error && ( + + {fieldState.error.message} + + )} +
+ +
+ + {/* Dynamic configuration component */} + {actionType && configComponentKey && ( +
+ + | undefined + } + fields={fields} + /> +
+ )} + + {!isEnabled && ( +

+ The action is disabled by the + configuration +

+ )} +
+ ) + }} + /> + ) + })} + + +
+
+
+ ) +} diff --git a/src/admin/templates/templates-form/index.tsx b/src/admin/templates/templates-form/index.tsx new file mode 100644 index 0000000..755c6c3 --- /dev/null +++ b/src/admin/templates/templates-form/index.tsx @@ -0,0 +1,2 @@ +export { TemplatesCreateForm } from "./templates-create-form" +export { TemplatesEditForm } from "./templates-edit-form" diff --git a/src/admin/templates/templates-form/templates-create-form/index.tsx b/src/admin/templates/templates-form/templates-create-form/index.tsx new file mode 100644 index 0000000..1b7a84c --- /dev/null +++ b/src/admin/templates/templates-form/templates-create-form/index.tsx @@ -0,0 +1,186 @@ +import { + Button, + FocusModal, + ProgressTabs, + toast, + Heading, +} from "@medusajs/ui" +import { Plus } from "@medusajs/icons" +import { useState, useEffect, useMemo } from "react" +import { useForm, useWatch } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { useQueryClient } from "@tanstack/react-query" +import { + useCreateTemplate, + useListTemplates, +} from "../../../../hooks/api/templates" +import { TemplatesGeneralForm } from "../templates-general-form" +import { + TemplateFormValues, + Tab, + TabState, +} from "../types" +import { baseTemplateFormSchema } from "../types/schema" + +export function TemplatesCreateForm() { + const [open, setOpen] = useState(false) + const [tab, setTab] = useState(Tab.GENERAL) + const [tabState, setTabState] = useState({ + [Tab.GENERAL]: "in-progress", + }) + const [buttonText, setButtonText] = useState("") + + useEffect(() => { + if (Tab.GENERAL === tab) { + setButtonText("Save template") + } + }, [tab]) + + const queryClient = useQueryClient() + + const { + data: templatesData, + isLoading: isTemplatesLoading, + } = useListTemplates({ + extraKey: [], + enabled: open, + }) + + const { + mutateAsync: createTemplate, + isPending: isCreateTemplatePending, + } = useCreateTemplate() + + // // Create dynamic schema + // const templateFormSchema = useMemo(() => { + // return baseTemplateFormSchema + // }, [templatesData?.templates]) + + const form = useForm({ + resolver: zodResolver(baseTemplateFormSchema), + defaultValues: { + general: { + name: "", + label: "", + description: "", + channel: "email", + locale: "en", + is_active: true + } + }, + }) + + useEffect(() => { + if (open === false) { + form.reset({ + general: { + name: "", + label: "", + description: "", + channel: "email", + locale: "en", + is_active: true, + } + }) + } + }, [open]) + + const general = useWatch({ + control: form.control, + name: "general", + }) + + async function handleSubmit(data: TemplateFormValues) { + if (Tab.GENERAL === tab) { + const items = { + name: data.general.name, + label: data.general.label, + description: data.general.description, + channel: data.general.channel, + locale: data.general.locale, + is_active: data.general.is_active, + } + + await createTemplate({ + items: [items], + }) + .then(() => { + queryClient.invalidateQueries({ + queryKey: ["templates"], + }) + toast.success("Template created successfully", { + position: "top-right", + duration: 3000, + }) + setOpen(false) + }) + .catch((error) => { + toast.error(error.message) + }) + } + } + + return ( + + + + + + + + Create Template + +
+ + + + General + + + +
+
+ + {isTemplatesLoading ? ( +
Loading...
+ ) : ( +
+ {tab === Tab.GENERAL && ( + + )} + + )} +
+ + + + + + +
+
+ ) +} diff --git a/src/admin/templates/templates-form/templates-edit-form/index.tsx b/src/admin/templates/templates-form/templates-edit-form/index.tsx new file mode 100644 index 0000000..d006b61 --- /dev/null +++ b/src/admin/templates/templates-form/templates-edit-form/index.tsx @@ -0,0 +1,212 @@ +import { + Button, + FocusModal, + ProgressTabs, + toast, + Heading, +} from "@medusajs/ui" +import { Pencil } from "@medusajs/icons" +import { useState, useEffect, useMemo } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { useQueryClient } from "@tanstack/react-query" +import { + useEditTemplate, + useListTemplates, +} from "../../../../hooks/api/templates" +import { + TemplateFormValues, + Tab, + TabState, +} from "../types" +import { baseTemplateFormSchema } from "../types/schema" +import { TemplatesGeneralForm } from "../templates-general-form" + +export function TemplatesEditForm({ + id, +}: { + id: string +}) { + const [open, setOpen] = useState(false) + const [tab, setTab] = useState(Tab.GENERAL) + const [tabState, setTabState] = useState({ + [Tab.GENERAL]: "in-progress", + }) + const [buttonText, setButtonText] = useState("") + const [eventName, setEventName] = useState< + string | undefined + >(undefined) + + useEffect(() => { + if (Tab.GENERAL === tab) { + setButtonText("Save template") + } + }, [tab]) + + const queryClient = useQueryClient() + + const { + data: templatesData, + isLoading: isTemplatesLoading, + } = useListTemplates({ + id: id, + extraKey: [], + enabled: open && !!id, + }) + + const { + mutateAsync: editTemplate, + isPending: isEditTemplatePending, + } = useEditTemplate() + + const form = useForm({ + resolver: zodResolver(baseTemplateFormSchema), + defaultValues: { + general: { + name: "", + label: "", + description: "", + channel: "email", + locale: "en", + is_active: true, + }, + }, + }) + + // Update form when data is loaded and modal is open + useEffect(() => { + if ( + templatesData + ) { + const template = templatesData?.templates?.[0] + form.reset({ + general: { + name: template.name || "", + label: template.label || "", + description: template.description || "", + channel: template.channel || "email", + locale: template.locale || "en", + is_active: template.is_active || true, + }, + }) + } + }, [open, templatesData]) + + // Reset form when modal is closed + useEffect(() => { + if (open === false) { + setEventName(undefined) + setTab(Tab.GENERAL) + + form.reset({ + general: { + name: "", + label: "", + description: "", + channel: "email", + locale: "en", + is_active: true, + }, + }) + } + }, [open]) + + async function handleSubmit(data: TemplateFormValues) { + if (Tab.GENERAL === tab) { + const items = { + id: id, + name: data.general.name, + label: data.general.label, + description: data.general.description, + channel: data.general.channel, + locale: data.general.locale, + is_active: data.general.is_active, + } + + await editTemplate({ + id: id, + items: [items], + }) + + queryClient.invalidateQueries({ + queryKey: ["templates"], + }) + + toast.success("Template updated successfully", { + position: "top-right", + duration: 3000, + }) + } + } + + return ( + + + + + + + + Edit Automation + +
+ + + + General + + + +
+
+ + {isTemplatesLoading ? ( +
Loading...
+ ) : ( +
+ {tab === Tab.GENERAL && ( + + )} + + )} +
+ + + + + + +
+
+ ) +} diff --git a/src/admin/templates/templates-form/templates-general-form/index.tsx b/src/admin/templates/templates-form/templates-general-form/index.tsx new file mode 100644 index 0000000..5457e11 --- /dev/null +++ b/src/admin/templates/templates-form/templates-general-form/index.tsx @@ -0,0 +1,214 @@ +import { + Input, + Label, + Select, + Checkbox, + Text, +} from "@medusajs/ui" +import { Controller, useWatch } from "react-hook-form" + +export function TemplatesGeneralForm({ + form, + isOpen, + isEditMode = false, +}: { + form: any + isOpen?: boolean + isEditMode?: boolean +}) { + const availableChannels = [ + { value: "email", label: "Email" }, + { value: "slack", label: "Slack" }, + ] + const availableLocales = [ + { value: "en", label: "English" }, + { value: "pl", label: "Polish" }, + ] + + return ( +
+
+
+
+ + ( + <> + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> +
+
+ + ( + <> + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> +
+
+ + ( + <> + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> +
+
+ + ( + <> + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> +
+
+ + ( + <> + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> +
+ +
+ +
+ ( + <> + + field.onChange(checked === true) + } + id="is_active" + /> + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + )} + /> +
+
+
+
+
+ ) +} diff --git a/src/admin/templates/templates-form/types/index.ts b/src/admin/templates/templates-form/types/index.ts new file mode 100644 index 0000000..4f130d7 --- /dev/null +++ b/src/admin/templates/templates-form/types/index.ts @@ -0,0 +1,2 @@ +export * from "./types" +export * from "./schema" diff --git a/src/admin/templates/templates-form/types/schema.ts b/src/admin/templates/templates-form/types/schema.ts new file mode 100644 index 0000000..82253fa --- /dev/null +++ b/src/admin/templates/templates-form/types/schema.ts @@ -0,0 +1,24 @@ +import { z } from "zod" + +// Base schema without dynamic validation +export const baseTemplateFormSchema = z.object({ + general: z.object({ + name: z + .string() + .min(1, "Name is required") + .min(3, "Name must be at least 3 characters"), + label: z + .string() + .min(1, "Label is required") + .min(3, "Label must be at least 3 characters"), + description: z + .string() + .min(1, "Description is required") + .min(3, "Description must be at least 3 characters"), + channel: z.enum(["email", "slack"]).transform((val) => val.toLowerCase()).refine((val) => val === "email" || val === "slack", { + message: "Channel must be either Email or Slack", + }), + locale: z.string().min(1, "Locale is required"), + is_active: z.boolean(), + }) +}) diff --git a/src/admin/templates/templates-form/types/types.ts b/src/admin/templates/templates-form/types/types.ts new file mode 100644 index 0000000..c7a9c9b --- /dev/null +++ b/src/admin/templates/templates-form/types/types.ts @@ -0,0 +1,13 @@ +import { z } from "zod" +import { ProgressStatus } from "@medusajs/ui" +import { baseTemplateFormSchema } from "./schema" + +export type TemplateFormValues = z.infer< + typeof baseTemplateFormSchema +> + +export enum Tab { + GENERAL = "general", +} + +export type TabState = Record diff --git a/src/admin/templates/templates-list/components/template-delete-button/index.tsx b/src/admin/templates/templates-list/components/template-delete-button/index.tsx new file mode 100644 index 0000000..ad2c11d --- /dev/null +++ b/src/admin/templates/templates-list/components/template-delete-button/index.tsx @@ -0,0 +1,55 @@ +import { Button, usePrompt, toast } from "@medusajs/ui" +import { useQueryClient } from "@tanstack/react-query" +import { useDeleteTemplate } from "../../../../../hooks/api/templates" +import { Trash } from "@medusajs/icons" + +export const TemplateDeleteButton = ({ + id, +}: { + id: string +}) => { + const queryClient = useQueryClient() + const prompt = usePrompt() + + const { mutate: deleteTemplate } = useDeleteTemplate() + + const handleDelete = async () => { + await deleteTemplate( + { id: id }, + { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["templates"], + }) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + + const handleDeleteConfirmation = async () => { + const result = await prompt({ + title: + "Are you sure you want to delete this template?", + description: "This action cannot be undone.", + confirmText: "Delete", + cancelText: "Cancel", + }) + + if (result) { + handleDelete() + } + } + + return ( + + ) +} diff --git a/src/admin/templates/templates-list/index.tsx b/src/admin/templates/templates-list/index.tsx new file mode 100644 index 0000000..54897c3 --- /dev/null +++ b/src/admin/templates/templates-list/index.tsx @@ -0,0 +1 @@ +export { TemplatesList } from "./templates-list" diff --git a/src/admin/templates/templates-list/templates-list.tsx b/src/admin/templates/templates-list/templates-list.tsx new file mode 100644 index 0000000..93f178b --- /dev/null +++ b/src/admin/templates/templates-list/templates-list.tsx @@ -0,0 +1,196 @@ +import { InformationCircleSolid } from "@medusajs/icons" +import { + Container, + Heading, + DataTable, + useDataTable, + createDataTableColumnHelper, + DataTablePaginationState, + Tooltip, + Badge, + Divider, +} from "@medusajs/ui" +import { useQueryClient } from "@tanstack/react-query" +import { useListTemplates } from "../../../hooks/api/templates" +import { useState, useMemo } from "react" +import { + TemplatesEditForm, + TemplatesCreateForm, +} from "../templates-form" +import { TemplateDeleteButton } from "./components/template-delete-button" + +export const TemplatesList = () => { + const [pagination, setPagination] = + useState({ + pageSize: 8, + pageIndex: 0, + }) + + const limit = 8 + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + + const queryClient = useQueryClient() + + const { + data: templatesData, + isLoading: isTemplatesLoading, + } = useListTemplates({ + extraKey: [], + limit: limit, + offset: offset, + order: "-created_at", + }) + + const columnHelper = createDataTableColumnHelper() + + // Memoize columns to prevent re-creation on every render + // This prevents unmounting of cells when data updates, which would close modals + const columns = useMemo( + () => [ + columnHelper.accessor("to", { + header: "Name and descriptions", + cell: ({ row }) => { + const tooltip = `Device (DB) ID: \n ${row?.original?.id}` + return ( + <> +
+
+ {row?.original?.name} + + } + maxWidth={400} + > + + +
+
+ {row?.original?.description} +
+
+ + ) + }, + }), + columnHelper.accessor("locale", { + header: "Locale", + cell: ({ row }) => { + return {row?.original?.locale} + }, + }), + // columnHelper.accessor("event_name", { + // header: "Event Name", + // cell: ({ row }) => { + // return {row?.original?.event_name} + // }, + // }), + // columnHelper.accessor("last_run_at", { + // header: "Last Run At", + // cell: ({ row }) => { + // const lastRunAtAll = row?.original?.states + // ?.map((state: any) => state.last_triggered_at) + // .sort( + // (a: any, b: any) => + // new Date(b).getTime() - + // new Date(a).getTime() + // ) + // return ( + // + // {lastRunAtAll.length > 0 + // ? new Date(lastRunAtAll[0]).toLocaleString() + // : "-"} + // + // ) + // }, + // }), + // columnHelper.accessor("active", { + // header: "Active", + // cell: ({ row }) => { + // const color = row?.original?.active + // ? "green" + // : "red" + // const text = row?.original?.active ? "Yes" : "No" + + // return ( + // + // {text} + // + // ) + // }, + // }), + columnHelper.accessor("created_at", { + header: "Created At", + cell: ({ row }) => { + return ( + + {row?.original?.created_at + ? new Date( + row.original.created_at + ).toLocaleString() + : "-"} + + ) + }, + }), + columnHelper.accessor("updated_at", { + header: "Updated At", + cell: ({ row }) => { + return ( + + {row?.original?.updated_at + ? new Date( + row.original.updated_at + ).toLocaleString() + : "-"} + + ) + }, + }), + columnHelper.accessor("actions", { + header: "Actions", + cell: ({ row }) => { + return ( +
+ + +
+ ) + }, + }), + ], + [] + ) + + const table = useDataTable({ + columns, + data: templatesData?.templates ?? [], + isLoading: isTemplatesLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + rowCount: templatesData?.count ?? 0, + }) + + return ( + + + + List of templates + + + + + + + ) +} diff --git a/src/api/admin/notification-plugin/events/route.ts b/src/api/admin/mpn/events/route.ts similarity index 100% rename from src/api/admin/notification-plugin/events/route.ts rename to src/api/admin/mpn/events/route.ts diff --git a/src/api/admin/notification-plugin/notifications/route.ts b/src/api/admin/mpn/notifications/route.ts similarity index 100% rename from src/api/admin/notification-plugin/notifications/route.ts rename to src/api/admin/mpn/notifications/route.ts diff --git a/src/api/admin/notification-plugin/render-template/route.ts b/src/api/admin/mpn/render-template/route.ts similarity index 100% rename from src/api/admin/notification-plugin/render-template/route.ts rename to src/api/admin/mpn/render-template/route.ts diff --git a/src/api/admin/mpn/templates/route.ts b/src/api/admin/mpn/templates/route.ts new file mode 100644 index 0000000..ff5c999 --- /dev/null +++ b/src/api/admin/mpn/templates/route.ts @@ -0,0 +1,119 @@ +import { + MedusaStoreRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, + MedusaError, +} from "@medusajs/framework/utils" +import { z } from "zod" +import { + CreateTemplateWorkflowInput, + createTemplateWorkflow, + DeleteTemplateWorkflowInput, + deleteTemplateWorkflow, + EditTemplateWorkflowInput, + editTemplateWorkflow, +} from "../../../../workflows/mpn-templates" + +export const PostTemplateSchema = z.object({ + id: z.string().optional(), + items: z.array( + z.object({ + id: z.string().optional(), + name: z.string(), + label: z.string(), + description: z.string(), + channel: z.string(), + locale: z.string(), + is_active: z.boolean(), + }) + ), +}) + +type PostAutomationSchema = z.infer< + typeof PostTemplateSchema +> + +export async function POST( + req: MedusaStoreRequest, + res: MedusaResponse +) { + if (req.body?.id) { + const { result: template } = + await editTemplateWorkflow(req.scope).run({ + input: req.body as EditTemplateWorkflowInput, + }) + + res.json({ + template: template, + }) + } else { + const { result: template } = + await createTemplateWorkflow(req.scope).run({ + input: req.body as CreateTemplateWorkflowInput, + }) + + res.json({ + template: template, + }) + } +} + +export async function GET( + req: MedusaStoreRequest, + res: MedusaResponse +) { + const query = req.scope.resolve( + ContainerRegistrationKeys.QUERY + ) + const { id } = req.query + const filters: any = {} + + if (id) { + filters.id = { + $eq: id, + } + } + + const { + data: templates, + metadata: { count, take, skip } = {}, + } = await query.graph({ + entity: "mpn_builder_template", + filters: filters, + ...req.queryConfig, + }) + + res.json({ + templates: templates, + count: count || 0, + limit: take || 15, + offset: skip || 0, + }) +} + +export const DeleteTemplateSchema = z.object({ + id: z.string(), +}) + +type DeleteTemplateSchema = z.infer< + typeof DeleteTemplateSchema +> + +export async function DELETE( + req: MedusaStoreRequest, + res: MedusaResponse +) { + const { result } = await deleteTemplateWorkflow( + req.scope + ).run({ + input: { + id: req.body.id as string, + } as DeleteTemplateWorkflowInput, + }) + + res.json({ + result: result, + }) +} diff --git a/src/api/middlewares.ts b/src/api/middlewares.ts index c942ad8..8acd833 100644 --- a/src/api/middlewares.ts +++ b/src/api/middlewares.ts @@ -18,10 +18,42 @@ export const AdminNotificationListParams = resource_type: z.string().optional(), }) +export const AdminTemplateListParams = + createFindParams().extend({ + id: z.string().optional(), + }) + export default defineMiddlewares({ routes: [ { - matcher: "/admin/notification-plugin/notifications", + matcher: "/admin/mpn/templates", + methods: ["GET"], + middlewares: [ + authenticate("user", ["session", "bearer"], { + allowUnauthenticated: false, + }), + validateAndTransformQuery( + AdminTemplateListParams, + { + defaults: [ + "id", + "name", + "label", + "description", + "created_at", + "updated_at", + "channel", + "locale", + "is_active", + "blocks", + ], + isList: true, + } + ), + ], + }, + { + matcher: "/admin/mpn/notifications", methods: ["GET"], middlewares: [ authenticate("user", ["session", "bearer"], { diff --git a/src/hooks/api/events.ts b/src/hooks/api/events.ts index 5c16a0b..dc502c7 100644 --- a/src/hooks/api/events.ts +++ b/src/hooks/api/events.ts @@ -44,7 +44,7 @@ export const useEvents = ( >({ mutationFn: async (variables) => { return await sdk.client.fetch( - "/admin/notification-plugin/events", + "/admin/mpn/events", { method: "POST", body: { diff --git a/src/hooks/api/notifications.ts b/src/hooks/api/notifications.ts index b4a25f4..e5fce83 100644 --- a/src/hooks/api/notifications.ts +++ b/src/hooks/api/notifications.ts @@ -68,7 +68,7 @@ export const useListNotifications = ( queryKey, queryFn: async () => { return await sdk.client.fetch( - "/admin/notification-plugin/notifications", + "/admin/mpn/notifications", { method: "GET", query, diff --git a/src/hooks/api/preview.ts b/src/hooks/api/preview.ts index 6f54ec0..44b8da3 100644 --- a/src/hooks/api/preview.ts +++ b/src/hooks/api/preview.ts @@ -60,7 +60,7 @@ export const usePreview = ( queryKey, queryFn: async ({ queryKey }) => { return await sdk.client.fetch( - "/admin/notification-plugin/render-template", + "/admin/mpn/render-template", { method: "POST", body: { diff --git a/src/hooks/api/templates/index.ts b/src/hooks/api/templates/index.ts new file mode 100644 index 0000000..06f9525 --- /dev/null +++ b/src/hooks/api/templates/index.ts @@ -0,0 +1 @@ +export * from "./templates" diff --git a/src/hooks/api/templates/templates.ts b/src/hooks/api/templates/templates.ts new file mode 100644 index 0000000..5565ea8 --- /dev/null +++ b/src/hooks/api/templates/templates.ts @@ -0,0 +1,135 @@ +import { FetchError } from "@medusajs/js-sdk" +import { + QueryKey, + useQuery, + useMutation, + UseMutationOptions, +} from "@tanstack/react-query" +import { sdk } from "../../../admin/lib/sdk" + +export const useCreateTemplate = (options?: any) => { + return useMutation>( + { + mutationFn: async (data) => { + await sdk.client.fetch("/admin/mpn/templates", { + method: "POST", + body: data, + }) + }, + } + ) +} + +export const useEditTemplate = (options?: any) => { + return useMutation>( + { + mutationFn: async (data) => { + await sdk.client.fetch("/admin/mpn/templates", { + method: "POST", + body: data, + }) + }, + } + ) +} + +export type DeleteTemplateInput = { + id: string +} + +export const useDeleteTemplate = ( + options?: UseMutationOptions< + void, + FetchError, + DeleteTemplateInput + > +) => { + return useMutation< + void, + FetchError, + DeleteTemplateInput + >({ + mutationFn: async ({ id }) => { + await sdk.client.fetch("/admin/mpn/templates", { + method: "DELETE", + body: { + id: id, + }, + }) + }, + ...(options as any), + }) +} + +export type useListTemplatesParams = { + id?: string + limit?: number + offset?: number + extraKey?: unknown[] + enabled?: boolean + fields?: string + order?: string +} + +type ListTemplatesQueryData = { + templates: any + count: number + limit: number + offset: number +} + +export const useListTemplates = ( + params: any, + options?: any +) => { + const { + limit = 100, + offset = 0, + extraKey = [], + enabled, + fields, + order = "created_at", + id, + } = params + + const queryKey: QueryKey = [ + "templates", + id, + limit, + offset, + ...extraKey, + ] + + const query: any = { + limit, + offset, + fields, + order, + } + + if (id) { + query.id = id + } + + const { data, ...rest } = useQuery< + ListTemplatesQueryData, + FetchError, + ListTemplatesQueryData, + QueryKey + >({ + queryKey, + queryFn: async () => { + return await sdk.client.fetch( + "/admin/mpn/templates", + { + method: "GET", + query, + } + ) + }, + enabled, + ...(options as any), + }) + + return { data, ...rest } +} diff --git a/src/modules/mpn-builder/types/interfaces.ts b/src/modules/mpn-builder/types/interfaces.ts index e69de29..9e810a9 100644 --- a/src/modules/mpn-builder/types/interfaces.ts +++ b/src/modules/mpn-builder/types/interfaces.ts @@ -0,0 +1,19 @@ +export interface Template { + id?: string + name: string + label: string | null + description: string | null + channel: string + locale: string + is_active: boolean +} + +export interface TemplateBlock { + id?: string + type: string + props: any +} + +export interface TemplateBlockProps { + [key: string]: any +} diff --git a/src/workflows/mpn-templates/create-template.ts b/src/workflows/mpn-templates/create-template.ts new file mode 100644 index 0000000..778b1b5 --- /dev/null +++ b/src/workflows/mpn-templates/create-template.ts @@ -0,0 +1,23 @@ +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { createTemplateStep } from "./steps" +import { Template } from "../../modules/mpn-builder/types/interfaces" + +export type CreateTemplateWorkflowInput = { + items: Template[] +} + +export const createTemplateWorkflow = createWorkflow( + "create-template", + ({ items }: CreateTemplateWorkflowInput) => { + const template = createTemplateStep({ + items: items, + }) + + return new WorkflowResponse({ + template, + }) + } +) diff --git a/src/workflows/mpn-templates/delete-template.ts b/src/workflows/mpn-templates/delete-template.ts new file mode 100644 index 0000000..e1a251c --- /dev/null +++ b/src/workflows/mpn-templates/delete-template.ts @@ -0,0 +1,22 @@ +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { deleteTemplateStep } from "./steps" + +export type DeleteTemplateWorkflowInput = { + id: string +} + +export const deleteTemplateWorkflow = createWorkflow( + "delete-template", + ({ id }: DeleteTemplateWorkflowInput) => { + const result = deleteTemplateStep({ + id: id, + }) + + return new WorkflowResponse({ + result, + }) + } +) diff --git a/src/workflows/mpn-templates/edit-template.ts b/src/workflows/mpn-templates/edit-template.ts new file mode 100644 index 0000000..065b6b8 --- /dev/null +++ b/src/workflows/mpn-templates/edit-template.ts @@ -0,0 +1,24 @@ +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { editTemplateStep } from "./steps" +import { Template } from "../../modules/mpn-builder/types/interfaces" + +export type EditTemplateWorkflowInput = { + id: string + items: Template[] +} + +export const editTemplateWorkflow = createWorkflow( + "edit-template", + ({ id, items }: EditTemplateWorkflowInput) => { + const template = editTemplateStep({ + items: items, + }) + + return new WorkflowResponse({ + template, + }) + } +) diff --git a/src/workflows/mpn-templates/index.ts b/src/workflows/mpn-templates/index.ts new file mode 100644 index 0000000..6f41f09 --- /dev/null +++ b/src/workflows/mpn-templates/index.ts @@ -0,0 +1,3 @@ +export * from "./create-template" +export * from "./edit-template" +export * from "./delete-template" diff --git a/src/workflows/mpn-templates/steps/create-template.ts b/src/workflows/mpn-templates/steps/create-template.ts new file mode 100644 index 0000000..8ff5d0d --- /dev/null +++ b/src/workflows/mpn-templates/steps/create-template.ts @@ -0,0 +1,37 @@ +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import MpnBuilderService from "../../../modules/mpn-builder/services/service" +import { MPN_BUILDER_MODULE } from "../../../modules/mpn-builder" +import { Template } from "../../../modules/mpn-builder/types/interfaces" + +type CreateTemplateStepInput = { + items: Template[] +} + +export const createTemplateStep = createStep( + "create-template", + async ( + { items }: CreateTemplateStepInput, + { container } + ) => { + const mpnBuilderService: MpnBuilderService = + container.resolve(MPN_BUILDER_MODULE) + + const template = + await mpnBuilderService.createMpnBuilderTemplates( + items.map((item) => ({ + name: item.name, + description: item.description, + label: item.label, + channel: item.channel, + locale: item.locale, + is_active: item.is_active, + blocks: item.blocks, + })) + ) + + return new StepResponse(template, template) + } +) diff --git a/src/workflows/mpn-templates/steps/delete-template.ts b/src/workflows/mpn-templates/steps/delete-template.ts new file mode 100644 index 0000000..2a8845f --- /dev/null +++ b/src/workflows/mpn-templates/steps/delete-template.ts @@ -0,0 +1,31 @@ + import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import MpnBuilderService from "../../../modules/mpn-builder/services/service" +import { MPN_BUILDER_MODULE } from "../../../modules/mpn-builder" + +type DeleteTemplateStepInput = { + id: string +} + +export const deleteTemplateStep = createStep( + "delete-template", + async ( + { id }: DeleteTemplateStepInput, + { container } + ) => { + const mpnBuilderService: MpnBuilderService = + container.resolve(MPN_BUILDER_MODULE) + + // Delete the trigger (cascade delete will handle related rules, states, and actions) + await mpnBuilderService.deleteMpnBuilderTemplates([ + id, + ]) + + return new StepResponse( + { id, deleted: true }, + { id, deleted: true } + ) + } +) diff --git a/src/workflows/mpn-templates/steps/edit-template.ts b/src/workflows/mpn-templates/steps/edit-template.ts new file mode 100644 index 0000000..cebe088 --- /dev/null +++ b/src/workflows/mpn-templates/steps/edit-template.ts @@ -0,0 +1,36 @@ +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import MpnBuilderService from "../../../modules/mpn-builder/services/service" +import { MPN_BUILDER_MODULE } from "../../../modules/mpn-builder" +import { Template } from "../../../modules/mpn-builder/types/interfaces" + +type EditTemplateStepInput = { + items: Template[] +} + +export const editTemplateStep = createStep( + "edit-template", + async ( + { items }: EditTemplateStepInput, + { container } + ) => { + const mpnBuilderService: MpnBuilderService = + container.resolve(MPN_BUILDER_MODULE) + + const template = + await mpnBuilderService.updateMpnBuilderTemplates( + items.map((item) => ({ + id: item.id, + name: item.name, + description: item.description, + channel: item.channel, + locale: item.locale, + is_active: item.is_active + })) + ) + + return new StepResponse(template, template) + } +) diff --git a/src/workflows/mpn-templates/steps/index.ts b/src/workflows/mpn-templates/steps/index.ts new file mode 100644 index 0000000..6f41f09 --- /dev/null +++ b/src/workflows/mpn-templates/steps/index.ts @@ -0,0 +1,3 @@ +export * from "./create-template" +export * from "./edit-template" +export * from "./delete-template" diff --git a/src/workflows/steps/log-step.ts b/src/workflows/steps/log-step.ts new file mode 100644 index 0000000..ab677ec --- /dev/null +++ b/src/workflows/steps/log-step.ts @@ -0,0 +1,21 @@ +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const logStep = createStep( + { + name: "log-step", + }, + function (input: any) { + console.log("log-step", JSON.stringify(input, null, 2)) + + return new StepResponse({ + status: "success", + message: "Requested", + input: input, + }) + } +) + +export { logStep } From 22807e7fb06394f59da1805bebd5d44e584021da Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Wed, 28 Jan 2026 14:24:42 +0100 Subject: [PATCH 03/41] feat: add templates page with edit, add, delete methods feat: add blocks page with edit, add, delete blocks --- package.json | 1 + .../components/checkbox-field.tsx | 37 +++ .../manager-fields/components/form-field.tsx | 27 ++ .../manager-fields/components/index.ts | 6 + .../components/number-field.tsx | 38 +++ .../components/select-field.tsx | 41 +++ .../components/text-area-field.tsx | 31 ++ .../manager-fields/components/text-field.tsx | 29 ++ src/admin/components/manager-fields/index.ts | 1 + .../manager-fields/manager-fields.tsx | 311 ++++++++++++++++++ .../components/manager-fields/types/index.ts | 1 + .../manager-fields/types/interfaces.ts | 6 + .../components/manager-fields/types/types.ts | 0 .../routes/mpn/templates/[id]/blocks/page.tsx | 25 ++ src/admin/templates/blocks/blocks-list.tsx | 37 +++ .../block-dropdown/block-dropdown.tsx | 30 ++ .../blocks/components/block-dropdown/index.ts | 1 + .../components/block-form/block-form.tsx | 178 ++++++++++ .../blocks/components/block-form/index.ts | 1 + .../blocks/components/block-item/index.tsx | 41 +++ .../components/block-list/block-list.tsx | 7 + .../blocks/components/block-list/index.ts | 1 + src/admin/templates/blocks/index.tsx | 1 + .../templates/templates-form/types/schema.ts | 13 +- .../templates-form/utils/block-form-schema.ts | 63 ++++ .../templates-list/templates-list.tsx | 4 + .../admin/mpn/available-templates/route.ts | 27 ++ .../admin/mpn/templates/[id]/blocks/route.ts | 117 +++++++ src/api/middlewares.ts | 43 +++ .../available-templates.ts | 61 ++++ src/hooks/api/available-templates/index.ts | 1 + src/hooks/api/templates/blocks/blocks.ts | 97 ++++++ src/hooks/api/templates/blocks/index.ts | 1 + .../services/base-template-service.ts | 127 +++++++ .../services/email-template-service.ts | 98 ++++++ src/modules/mpn-builder/services/index.ts | 5 +- src/modules/mpn-builder/services/service.ts | 114 +++++++ src/modules/mpn-builder/types/base-type.ts | 76 +++++ src/modules/mpn-builder/types/index.ts | 1 + .../mpn-templates/edit-template-blocks.ts | 27 ++ .../mpn-templates/steps/create-template.ts | 1 - .../steps/edit-template-blocks.ts | 117 +++++++ 42 files changed, 1841 insertions(+), 3 deletions(-) create mode 100644 src/admin/components/manager-fields/components/checkbox-field.tsx create mode 100644 src/admin/components/manager-fields/components/form-field.tsx create mode 100644 src/admin/components/manager-fields/components/index.ts create mode 100644 src/admin/components/manager-fields/components/number-field.tsx create mode 100644 src/admin/components/manager-fields/components/select-field.tsx create mode 100644 src/admin/components/manager-fields/components/text-area-field.tsx create mode 100644 src/admin/components/manager-fields/components/text-field.tsx create mode 100644 src/admin/components/manager-fields/index.ts create mode 100644 src/admin/components/manager-fields/manager-fields.tsx create mode 100644 src/admin/components/manager-fields/types/index.ts create mode 100644 src/admin/components/manager-fields/types/interfaces.ts create mode 100644 src/admin/components/manager-fields/types/types.ts create mode 100644 src/admin/routes/mpn/templates/[id]/blocks/page.tsx create mode 100644 src/admin/templates/blocks/blocks-list.tsx create mode 100644 src/admin/templates/blocks/components/block-dropdown/block-dropdown.tsx create mode 100644 src/admin/templates/blocks/components/block-dropdown/index.ts create mode 100644 src/admin/templates/blocks/components/block-form/block-form.tsx create mode 100644 src/admin/templates/blocks/components/block-form/index.ts create mode 100644 src/admin/templates/blocks/components/block-item/index.tsx create mode 100644 src/admin/templates/blocks/components/block-list/block-list.tsx create mode 100644 src/admin/templates/blocks/components/block-list/index.ts create mode 100644 src/admin/templates/blocks/index.tsx create mode 100644 src/admin/templates/templates-form/utils/block-form-schema.ts create mode 100644 src/api/admin/mpn/available-templates/route.ts create mode 100644 src/api/admin/mpn/templates/[id]/blocks/route.ts create mode 100644 src/hooks/api/available-templates/available-templates.ts create mode 100644 src/hooks/api/available-templates/index.ts create mode 100644 src/hooks/api/templates/blocks/blocks.ts create mode 100644 src/hooks/api/templates/blocks/index.ts create mode 100644 src/modules/mpn-builder/services/base-template-service.ts create mode 100644 src/modules/mpn-builder/services/email-template-service.ts create mode 100644 src/modules/mpn-builder/types/base-type.ts create mode 100644 src/workflows/mpn-templates/edit-template-blocks.ts create mode 100644 src/workflows/mpn-templates/steps/edit-template-blocks.ts diff --git a/package.json b/package.json index a6d12af..013e790 100755 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@lexical/html": "^0.39.0", "@lexical/react": "^0.39.0", "@lexical/rich-text": "^0.39.0", + "@dnd-kit/core": "^6.3.1", "@react-email/components": "^0.5.7", "@react-email/preview-server": "^4.3.1", "@react-email/render": "^1.4.0", diff --git a/src/admin/components/manager-fields/components/checkbox-field.tsx b/src/admin/components/manager-fields/components/checkbox-field.tsx new file mode 100644 index 0000000..d7a5462 --- /dev/null +++ b/src/admin/components/manager-fields/components/checkbox-field.tsx @@ -0,0 +1,37 @@ +import { Checkbox, Label } from "@medusajs/ui" + +interface CheckboxFieldProps { + label: string + checked: boolean + onChange: (checked: boolean) => void + required?: boolean + disabled?: boolean +} + +export const CheckboxField = ({ + label, + checked, + onChange, + required = false, + disabled = false, +}: CheckboxFieldProps) => { + return ( +
+ + +
+ ) +} diff --git a/src/admin/components/manager-fields/components/form-field.tsx b/src/admin/components/manager-fields/components/form-field.tsx new file mode 100644 index 0000000..71c5754 --- /dev/null +++ b/src/admin/components/manager-fields/components/form-field.tsx @@ -0,0 +1,27 @@ +import { Input, Label } from "@medusajs/ui" + +interface FormFieldProps { + label: string + required?: boolean + children: React.ReactNode +} + +export const FormField = ({ + label, + required, + children, +}: FormFieldProps) => { + return ( +
+
+ +
+ {children} +
+ ) +} diff --git a/src/admin/components/manager-fields/components/index.ts b/src/admin/components/manager-fields/components/index.ts new file mode 100644 index 0000000..257b0b7 --- /dev/null +++ b/src/admin/components/manager-fields/components/index.ts @@ -0,0 +1,6 @@ +export { TextField } from "./text-field" +export { TextAreaField } from "./text-area-field" +export { NumberField } from "./number-field" +export { SelectField } from "./select-field" +export { CheckboxField } from "./checkbox-field" +export { FormField } from "./form-field" diff --git a/src/admin/components/manager-fields/components/number-field.tsx b/src/admin/components/manager-fields/components/number-field.tsx new file mode 100644 index 0000000..33cdede --- /dev/null +++ b/src/admin/components/manager-fields/components/number-field.tsx @@ -0,0 +1,38 @@ +import { Input, Label } from "@medusajs/ui" + +interface NumberFieldProps { + label: string + value: number + onChange: (value: number) => void + placeholder?: string + required?: boolean + disabled?: boolean + min?: number | null + max?: number | null + step?: number | null +} + +export const NumberField = ({ + label, + value, + onChange, + placeholder = "", + required = false, + disabled = false, + min, + max, + step = null, +}: NumberFieldProps) => { + return ( + onChange(Number(e.target.value))} + placeholder={placeholder} + disabled={disabled} + min={min || undefined} + max={max || undefined} + step={step || undefined} + /> + ) +} diff --git a/src/admin/components/manager-fields/components/select-field.tsx b/src/admin/components/manager-fields/components/select-field.tsx new file mode 100644 index 0000000..b7d39b7 --- /dev/null +++ b/src/admin/components/manager-fields/components/select-field.tsx @@ -0,0 +1,41 @@ +import { Select, Label } from "@medusajs/ui" + +interface SelectFieldProps { + label: string + value: string + onChange: (value: string) => void + options: Array<{ value: string; name: string }> + required?: boolean + disabled?: boolean +} + +export const SelectField = ({ + label, + value, + onChange, + options, + required = false, + disabled = false, +}: SelectFieldProps) => { + return ( + + ) +} diff --git a/src/admin/components/manager-fields/components/text-area-field.tsx b/src/admin/components/manager-fields/components/text-area-field.tsx new file mode 100644 index 0000000..0b4d050 --- /dev/null +++ b/src/admin/components/manager-fields/components/text-area-field.tsx @@ -0,0 +1,31 @@ +import { Textarea, Label } from "@medusajs/ui" + +interface TextAreaFieldProps { + label: string + value: string + onChange: (value: string) => void + placeholder?: string + required?: boolean + disabled?: boolean + rows?: number +} + +export const TextAreaField = ({ + label, + value, + onChange, + placeholder = "", + required = false, + disabled = false, + rows = 3, +}: TextAreaFieldProps) => { + return ( +