x
This commit is contained in:
1 parent
9370869dc2
commit
b8873f88b0
4 files changed
+351
-3
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
|
||||
function fixNode(node: any) {
|
||||
if (node.translations) {
|
||||
const labelKey = node.title !== undefined ? 'title' : 'label';
|
||||
if (typeof node[labelKey] === 'string') {
|
||||
node[labelKey] = { en: node[labelKey] };
|
||||
}
|
||||
node[labelKey] = { ...(node[labelKey] || {}), ...node.translations };
|
||||
delete node.translations;
|
||||
}
|
||||
|
||||
if (node.columns && Array.isArray(node.columns)) {
|
||||
node.columns.forEach((col: any) => fixNode(col));
|
||||
}
|
||||
if (node.links && Array.isArray(node.links)) {
|
||||
node.links.forEach((link: any) => fixNode(link));
|
||||
}
|
||||
if (node.items && Array.isArray(node.items)) {
|
||||
node.items.forEach((item: any) => fixNode(item));
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const menu = await prisma.menu.findUnique({
|
||||
where: { name: 'main' }
|
||||
});
|
||||
|
||||
if (!menu || !menu.payload) {
|
||||
console.log('No main menu payload found');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = menu.payload as any[];
|
||||
console.log('Original payload length:', payload.length);
|
||||
|
||||
payload.forEach(fixNode);
|
||||
|
||||
await prisma.menu.update({
|
||||
where: { name: 'main' },
|
||||
data: { payload }
|
||||
});
|
||||
|
||||
console.log('Menu fixed successfully');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import prisma from '../src/lib/prisma';
|
||||
import crypto from 'crypto';
|
||||
|
||||
async function main() {
|
||||
const menu = await prisma.menu.findUnique({
|
||||
where: { name: 'main' }
|
||||
});
|
||||
|
||||
if (!menu || !menu.payload) {
|
||||
console.error('Menu main not found or payload is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = menu.payload as any[];
|
||||
let updated = false;
|
||||
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
const node = payload[i];
|
||||
if (node.nodeId === 'b4b2faf8-8881-4ca9-a8d5-a88054a37385') {
|
||||
if (node.links && !node.columns) {
|
||||
console.log('Fixing Scent Guide node...');
|
||||
|
||||
const newColumn = {
|
||||
nodeId: crypto.randomUUID(),
|
||||
title: {
|
||||
en: 'Essential Oils',
|
||||
th: 'น้ำมันหอมระเหย',
|
||||
ja: 'エッセンシャルオイル',
|
||||
ko: '에센셜 오일',
|
||||
vi: 'Tinh dầu',
|
||||
zh: '精油'
|
||||
},
|
||||
links: node.links
|
||||
};
|
||||
|
||||
node.columns = [newColumn];
|
||||
delete node.links;
|
||||
updated = true;
|
||||
} else {
|
||||
console.log('Scent Guide node already has columns or no links to fix.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
await prisma.menu.update({
|
||||
where: { name: 'main' },
|
||||
data: { payload }
|
||||
});
|
||||
console.log('Successfully updated main menu payload.');
|
||||
} else {
|
||||
console.log('No updates were necessary.');
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
+237
-1
@@ -1,3 +1,4 @@
|
||||
import crypto from "crypto";
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
@@ -5,9 +6,36 @@ import {
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import axios from "axios";
|
||||
import * as FormData from "form-data";
|
||||
import FormData from "form-data";
|
||||
import { PAGE_SECTION_TYPES } from "@homaroma/types";
|
||||
|
||||
function getChildrenArray(node: any): any[] {
|
||||
if (Array.isArray(node.columns)) return node.columns;
|
||||
if (Array.isArray(node.links)) return node.links;
|
||||
if (Array.isArray(node.items)) return node.items;
|
||||
if (Array.isArray(node.children)) return node.children;
|
||||
node.links = [];
|
||||
return node.links;
|
||||
}
|
||||
|
||||
function findNodeAndParent(tree: any[], targetId: string, parent: any = null, list: any[] = tree): { node: any, parent: any, list: any[], index: number } | null {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const item = tree[i];
|
||||
if (item && typeof item === 'object') {
|
||||
if (item.nodeId === targetId) {
|
||||
return { node: item, parent, list: tree, index: i };
|
||||
}
|
||||
for (const key of Object.keys(item)) {
|
||||
if (Array.isArray(item[key])) {
|
||||
const res = findNodeAndParent(item[key], targetId, item, item[key]);
|
||||
if (res) return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let BACKEND_URL = "http://localhost:3051/api";
|
||||
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "admin";
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "homaroma123"; // เปลี่ยนรหัสผ่านตรงนี้ให้ตรงกับ Production ด้วยนะครับ
|
||||
@@ -348,6 +376,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
description: "List all dynamic pages and layouts managed by the CMS",
|
||||
inputSchema: { type: "object", properties: {} }
|
||||
},
|
||||
{
|
||||
name: "list_dynamic_pages_metadata",
|
||||
description: "List all dynamic pages and layouts managed by the CMS (metadata only)",
|
||||
inputSchema: { type: "object", properties: {} }
|
||||
},
|
||||
{
|
||||
name: "get_dynamic_page_detail",
|
||||
description: "Get full detail of a specific dynamic page by slug",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
slug: { type: "string", description: "Slug of the page" }
|
||||
},
|
||||
required: ["slug"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "create_dynamic_page",
|
||||
description: "Create or update a dynamic page or layout",
|
||||
@@ -452,6 +496,62 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
name: "audit_missing_pages",
|
||||
description: "Audit all menus and find links that do not have a corresponding dynamic page or hardcoded route",
|
||||
inputSchema: { type: "object", properties: {} }
|
||||
},
|
||||
{
|
||||
name: "get_mega_menu_tree",
|
||||
description: "Read Mega Menu (main) payload, auto-generate nodeId for all nodes if missing, and return the tree.",
|
||||
inputSchema: { type: "object", properties: {} }
|
||||
},
|
||||
{
|
||||
name: "add_menu_node",
|
||||
description: "Add a new node to the Mega Menu tree. CRITICAL RULE: Mega Menu structure is Root -> Column -> Link. Do NOT add a Link directly under Root. If you want to add a Link, you MUST check if a Column exists first. If no Column exists, create a Column node first, then add the Link under that Column.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
parentId: { type: "string", description: "UUID of parent node, or 'root'" },
|
||||
nodeData: { type: "object", description: "Node data including label, url, translations, etc." }
|
||||
},
|
||||
required: ["nodeData"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "update_menu_translation",
|
||||
description: "Update 6-language translations for a specific node in the Mega Menu",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
nodeId: { type: "string", description: "UUID of the node" },
|
||||
translations: {
|
||||
type: "object",
|
||||
description: "Object containing language codes as keys (th, en, etc.) and translated strings as values."
|
||||
}
|
||||
},
|
||||
required: ["nodeId", "translations"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "move_menu_node",
|
||||
description: "Move a node to a new parent or change its order",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
nodeId: { type: "string" },
|
||||
newParentId: { type: "string", description: "UUID of new parent, or 'root'" },
|
||||
orderIndex: { type: "number", description: "New index in the parent's children array" }
|
||||
},
|
||||
required: ["nodeId"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "remove_menu_node",
|
||||
description: "Remove a node and all its children from the Mega Menu",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
nodeId: { type: "string" }
|
||||
},
|
||||
required: ["nodeId"]
|
||||
}
|
||||
}
|
||||
],
|
||||
};
|
||||
@@ -655,6 +755,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const res = await api.get("/pages");
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
case "list_dynamic_pages_metadata": {
|
||||
const res = await api.get("/pages");
|
||||
let metadata = res.data;
|
||||
if (Array.isArray(metadata)) {
|
||||
metadata = metadata.map((p: any) => ({ id: p.id, slug: p.slug, title: p.title }));
|
||||
} else if (metadata && Array.isArray(metadata.data)) {
|
||||
metadata = {
|
||||
...metadata,
|
||||
data: metadata.data.map((p: any) => ({ id: p.id, slug: p.slug, title: p.title }))
|
||||
};
|
||||
}
|
||||
return { content: [{ type: "text", text: JSON.stringify(metadata, null, 2) }] };
|
||||
}
|
||||
case "get_dynamic_page_detail": {
|
||||
const res = await api.get(`/pages/${params.slug}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
case "create_dynamic_page": {
|
||||
const payload: any = {
|
||||
title: params.title,
|
||||
@@ -784,6 +901,125 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
|
||||
return { content: [{ type: "text", text: JSON.stringify({ missingPages: missingLinks }, null, 2) }] };
|
||||
}
|
||||
case "get_mega_menu_tree": {
|
||||
const res = await api.get(`/menus/main`);
|
||||
const menu = res.data;
|
||||
if (!menu.payload) menu.payload = [];
|
||||
|
||||
let modified = false;
|
||||
function ensureNodeIds(obj: any) {
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach(ensureNodeIds);
|
||||
} else if (obj !== null && typeof obj === 'object') {
|
||||
if (!obj.nodeId) {
|
||||
obj.nodeId = crypto.randomUUID();
|
||||
modified = true;
|
||||
}
|
||||
Object.values(obj).forEach(ensureNodeIds);
|
||||
}
|
||||
}
|
||||
|
||||
ensureNodeIds(menu.payload);
|
||||
|
||||
if (modified) {
|
||||
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
|
||||
}
|
||||
|
||||
return { content: [{ type: "text", text: JSON.stringify(menu.payload, null, 2) }] };
|
||||
}
|
||||
case "add_menu_node": {
|
||||
const { parentId, nodeData } = params;
|
||||
const res = await api.get(`/menus/main`);
|
||||
const menu = res.data;
|
||||
if (!menu.payload) menu.payload = [];
|
||||
|
||||
nodeData.nodeId = crypto.randomUUID();
|
||||
|
||||
// Map translations to label/title
|
||||
if (nodeData.translations) {
|
||||
const labelKey = nodeData.title !== undefined ? 'title' : 'label';
|
||||
if (typeof nodeData[labelKey] === 'string') {
|
||||
// fallback if it was a string
|
||||
nodeData[labelKey] = { en: nodeData[labelKey] };
|
||||
}
|
||||
nodeData[labelKey] = { ...(nodeData[labelKey] || {}), ...nodeData.translations };
|
||||
delete nodeData.translations;
|
||||
}
|
||||
|
||||
if (!parentId || parentId === 'root') {
|
||||
if (nodeData.url && !nodeData.columns && !nodeData.items && !nodeData.children) {
|
||||
throw new Error("Invalid schema: Cannot add a Link directly under Root. Please create a Column first.");
|
||||
}
|
||||
menu.payload.push(nodeData);
|
||||
} else {
|
||||
const found = findNodeAndParent(menu.payload, parentId);
|
||||
if (!found) {
|
||||
throw new Error(`Parent node ${parentId} not found`);
|
||||
}
|
||||
const children = getChildrenArray(found.node);
|
||||
children.push(nodeData);
|
||||
}
|
||||
|
||||
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
|
||||
return { content: [{ type: "text", text: JSON.stringify({ success: true, nodeId: nodeData.nodeId, payload: menu.payload }, null, 2) }] };
|
||||
}
|
||||
case "update_menu_translation": {
|
||||
const { nodeId, translations } = params;
|
||||
const res = await api.get(`/menus/main`);
|
||||
const menu = res.data;
|
||||
if (!menu.payload) menu.payload = [];
|
||||
|
||||
const found = findNodeAndParent(menu.payload, nodeId);
|
||||
if (!found) throw new Error(`Node ${nodeId} not found`);
|
||||
|
||||
const labelKey = found.node.title !== undefined ? 'title' : 'label';
|
||||
found.node[labelKey] = { ...(found.node[labelKey] || {}), ...translations };
|
||||
|
||||
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
|
||||
return { content: [{ type: "text", text: JSON.stringify({ success: true, node: found.node }, null, 2) }] };
|
||||
}
|
||||
case "move_menu_node": {
|
||||
const { nodeId, newParentId, orderIndex } = params;
|
||||
const res = await api.get(`/menus/main`);
|
||||
const menu = res.data;
|
||||
if (!menu.payload) menu.payload = [];
|
||||
|
||||
const found = findNodeAndParent(menu.payload, nodeId);
|
||||
if (!found) throw new Error(`Node ${nodeId} not found`);
|
||||
|
||||
// Remove from current list
|
||||
found.list.splice(found.index, 1);
|
||||
|
||||
let targetList = menu.payload;
|
||||
if (newParentId && newParentId !== 'root') {
|
||||
const newParent = findNodeAndParent(menu.payload, newParentId);
|
||||
if (!newParent) throw new Error(`New parent ${newParentId} not found`);
|
||||
targetList = getChildrenArray(newParent.node);
|
||||
}
|
||||
|
||||
if (typeof orderIndex === 'number' && orderIndex >= 0) {
|
||||
targetList.splice(orderIndex, 0, found.node);
|
||||
} else {
|
||||
targetList.push(found.node);
|
||||
}
|
||||
|
||||
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
|
||||
return { content: [{ type: "text", text: JSON.stringify({ success: true, payload: menu.payload }, null, 2) }] };
|
||||
}
|
||||
case "remove_menu_node": {
|
||||
const { nodeId } = params;
|
||||
const res = await api.get(`/menus/main`);
|
||||
const menu = res.data;
|
||||
if (!menu.payload) menu.payload = [];
|
||||
|
||||
const found = findNodeAndParent(menu.payload, nodeId);
|
||||
if (!found) throw new Error(`Node ${nodeId} not found`);
|
||||
|
||||
found.list.splice(found.index, 1);
|
||||
|
||||
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
|
||||
return { content: [{ type: "text", text: JSON.stringify({ success: true, payload: menu.payload }, null, 2) }] };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
if (view.type === 'root') {
|
||||
return (
|
||||
<div className="px-6 py-2 space-y-1">
|
||||
{view.data.map((item) => (
|
||||
<div key={item.id} className="border-b border-spa-stone/10 last:border-0">
|
||||
{view.data.map((item, index) => (
|
||||
<div key={item.id || `menu-root-${index}`} className="border-b border-spa-stone/10 last:border-0">
|
||||
{item.columns && item.columns.length > 0 ? (
|
||||
<button
|
||||
onClick={() => pushView({ type: 'mega', data: item.columns!, title: item.label })}
|
||||
|
||||
Reference in new issue
Block a user