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 { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import {
|
import {
|
||||||
@@ -5,9 +6,36 @@ import {
|
|||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import { PAGE_SECTION_TYPES } from "@homaroma/types";
|
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";
|
let BACKEND_URL = "http://localhost:3051/api";
|
||||||
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "admin";
|
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "admin";
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "homaroma123"; // เปลี่ยนรหัสผ่านตรงนี้ให้ตรงกับ Production ด้วยนะครับ
|
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",
|
description: "List all dynamic pages and layouts managed by the CMS",
|
||||||
inputSchema: { type: "object", properties: {} }
|
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",
|
name: "create_dynamic_page",
|
||||||
description: "Create or update a dynamic page or layout",
|
description: "Create or update a dynamic page or layout",
|
||||||
@@ -452,6 +496,62 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|||||||
name: "audit_missing_pages",
|
name: "audit_missing_pages",
|
||||||
description: "Audit all menus and find links that do not have a corresponding dynamic page or hardcoded route",
|
description: "Audit all menus and find links that do not have a corresponding dynamic page or hardcoded route",
|
||||||
inputSchema: { type: "object", properties: {} }
|
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");
|
const res = await api.get("/pages");
|
||||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
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": {
|
case "create_dynamic_page": {
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
title: params.title,
|
title: params.title,
|
||||||
@@ -784,6 +901,125 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
|
|
||||||
return { content: [{ type: "text", text: JSON.stringify({ missingPages: missingLinks }, null, 2) }] };
|
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:
|
default:
|
||||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
|||||||
if (view.type === 'root') {
|
if (view.type === 'root') {
|
||||||
return (
|
return (
|
||||||
<div className="px-6 py-2 space-y-1">
|
<div className="px-6 py-2 space-y-1">
|
||||||
{view.data.map((item) => (
|
{view.data.map((item, index) => (
|
||||||
<div key={item.id} className="border-b border-spa-stone/10 last:border-0">
|
<div key={item.id || `menu-root-${index}`} className="border-b border-spa-stone/10 last:border-0">
|
||||||
{item.columns && item.columns.length > 0 ? (
|
{item.columns && item.columns.length > 0 ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => pushView({ type: 'mega', data: item.columns!, title: item.label })}
|
onClick={() => pushView({ type: 'mega', data: item.columns!, title: item.label })}
|
||||||
|
|||||||
Reference in new issue
Block a user