This commit is contained in:
Your Name committed 2026-06-28 01:07:36 -04:00
1 parent ac387185e0
commit 6a35dcf955
6 files changed
+437 -67

No files matched your search

+79 -17
View File
@@ -160,13 +160,27 @@ function resolveCatName(cat, lang) {
return trans?.name ?? cat.name;
}
export const pagesRoute = new Elysia({ prefix: "/pages" })
.get("/", async () => {
return await prisma.page.findMany({
include: {
sections: {
orderBy: { order: "asc" }
.get("/", async ({ query }) => {
const view = String(query?.view || "summary").toLowerCase();
if (view === "full") {
return await prisma.page.findMany({
include: {
sections: {
orderBy: { order: "asc" }
}
}
}
});
}
return await prisma.page.findMany({
select: {
id: true,
slug: true,
title: true,
isPublished: true,
createdAt: true,
updatedAt: true
},
orderBy: { updatedAt: "desc" }
});
})
.get("/:slug", async ({ params: { slug }, query, set }) => {
@@ -407,12 +421,57 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
diagnostic: "The requested slug is reserved and cannot be used for dynamic pages."
};
}
let finalIsPublished = true;
if (status !== undefined) {
finalIsPublished = status === "published";
if (metadata !== undefined) {
set.status = 400;
return {
success: false,
operation,
pageId: id ? Number(id) : null,
slug,
title,
error: "Unsupported field",
diagnostic: "Page metadata is not supported by the current Page model. Remove 'metadata' from the request."
};
}
else if (isPublished !== undefined) {
finalIsPublished = isPublished;
if (status !== undefined) {
set.status = 400;
return {
success: false,
operation,
pageId: id ? Number(id) : null,
slug,
title,
error: "Unsupported publish field",
diagnostic: "Use 'isPublished' only. The legacy 'status' field is no longer accepted."
};
}
let finalIsPublished = isPublished;
let publishBehavior = "request";
if (finalIsPublished === undefined) {
if (id) {
const existingPage = await prisma.page.findUnique({
where: { id: Number(id) },
select: { isPublished: true }
});
if (!existingPage) {
set.status = 404;
return {
success: false,
operation,
pageId: Number(id),
slug,
title,
error: "Page not found",
diagnostic: "The requested page ID does not exist."
};
}
finalIsPublished = existingPage.isPublished;
publishBehavior = "preserve-existing";
}
else {
finalIsPublished = false;
publishBehavior = "default-create-false";
}
}
const page = id
? await prisma.page.update({
@@ -429,10 +488,15 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
slug: page.slug,
title: page.title,
isPublished: page.isPublished,
publishBehavior,
page,
diagnostic: operation === "create"
? "Page created successfully."
: "Page updated successfully."
? publishBehavior === "default-create-false"
? "Page created successfully. Defaulted to isPublished=false because the field was omitted."
: "Page created successfully."
: publishBehavior === "preserve-existing"
? "Page updated successfully. Existing publish state was preserved because isPublished was omitted."
: "Page updated successfully."
};
}
catch (err) {
@@ -453,9 +517,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
id: t.Optional(t.Number()),
slug: t.String(),
title: t.String(),
isPublished: t.Optional(t.Boolean()),
status: t.Optional(t.String()),
metadata: t.Optional(t.Any())
isPublished: t.Optional(t.Boolean())
})
})
.delete("/:slug", async ({ params: { slug }, set }) => {
@@ -736,4 +798,4 @@ function validateSectionPayload(type, payload) {
}
return null;
}
//# sourceMappingURL=pages.js.map
//# sourceMappingURL=pages.js.map
+93 -14
View File
@@ -64,14 +64,37 @@ function resolveCatName(cat: any, lang: string): any {
return trans?.name ?? cat.name;
}
function summarizeValidationIssues(issues: z.ZodIssue[]) {
return issues.map((issue) => ({
path: issue.path.join(".") || "(root)",
message: issue.message,
code: issue.code,
}));
}
export const pagesRoute = new Elysia({ prefix: "/pages" })
.get("/", async () => {
return await prisma.page.findMany({
include: {
sections: {
orderBy: { order: "asc" },
.get("/", async ({ query }) => {
const view = String((query as any)?.view || "summary").toLowerCase();
if (view === "full") {
return await prisma.page.findMany({
include: {
sections: {
orderBy: { order: "asc" },
},
},
});
}
return await prisma.page.findMany({
select: {
id: true,
slug: true,
title: true,
isPublished: true,
createdAt: true,
updatedAt: true,
},
orderBy: { updatedAt: "desc" },
});
})
.get("/:slug", async ({ params: { slug }, query, set }) => {
@@ -114,6 +137,8 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
})
.get("/sections/:sectionId", async ({ params: { sectionId }, query, set }) => {
const lang = (query as any)?.lang || "th";
const hydrate =
String((query as any)?.hydrate ?? "true").toLowerCase() !== "false";
try {
const section = await prisma.pageSection.findUnique({
where: { id: Number(sectionId) },
@@ -122,7 +147,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
set.status = 404;
return { error: "Section not found" };
}
return await hydrateSectionPayload(section, lang);
return hydrate ? await hydrateSectionPayload(section, lang) : section;
} catch (err) {
set.status = 500;
return { error: "Failed to fetch page section" };
@@ -182,11 +207,60 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
};
}
let finalIsPublished = true;
if (metadata !== undefined) {
set.status = 400;
return {
success: false,
operation,
pageId: id ? Number(id) : null,
slug,
title,
error: "Unsupported field",
diagnostic:
"Page metadata is not supported by the current Page model. Remove 'metadata' from the request.",
};
}
if (status !== undefined) {
finalIsPublished = status === "published";
} else if (isPublished !== undefined) {
finalIsPublished = isPublished;
set.status = 400;
return {
success: false,
operation,
pageId: id ? Number(id) : null,
slug,
title,
error: "Unsupported publish field",
diagnostic:
"Use 'isPublished' only. The legacy 'status' field is no longer accepted.",
};
}
let finalIsPublished = isPublished;
let publishBehavior: "request" | "default-create-false" | "preserve-existing" = "request";
if (finalIsPublished === undefined) {
if (id) {
const existingPage = await prisma.page.findUnique({
where: { id: Number(id) },
select: { isPublished: true },
});
if (!existingPage) {
set.status = 404;
return {
success: false,
operation,
pageId: Number(id),
slug,
title,
error: "Page not found",
diagnostic: "The requested page ID does not exist.",
};
}
finalIsPublished = existingPage.isPublished;
publishBehavior = "preserve-existing";
} else {
finalIsPublished = false;
publishBehavior = "default-create-false";
}
}
const page = id
@@ -205,11 +279,16 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
slug: page.slug,
title: page.title,
isPublished: page.isPublished,
publishBehavior,
page,
diagnostic:
operation === "create"
? "Page created successfully."
: "Page updated successfully.",
? publishBehavior === "default-create-false"
? "Page created successfully. Defaulted to isPublished=false because the field was omitted."
: "Page created successfully."
: publishBehavior === "preserve-existing"
? "Page updated successfully. Existing publish state was preserved because isPublished was omitted."
: "Page updated successfully.",
};
} catch (err) {
console.error("[Pages] Error:", err);
@@ -234,8 +313,6 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
slug: t.String(),
title: t.String(),
isPublished: t.Optional(t.Boolean()),
status: t.Optional(t.String()),
metadata: t.Optional(t.Any()),
}),
},
)
@@ -308,6 +385,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
return {
error: "Validation failed",
details: validationResult.issues,
validationIssues: summarizeValidationIssues(validationResult.issues || []),
};
}
@@ -380,6 +458,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
return {
error: "Validation failed",
details: validationResult.issues,
validationIssues: summarizeValidationIssues(validationResult.issues || []),
};
}
finalPayload = validationResult.data;
+11 -12
View File
@@ -21,6 +21,8 @@ export const api = axios.create({
baseURL: BACKEND_URL,
});
// MCP server state is process-local because this server runs over stdio.
api.interceptors.request.use(async (config) => {
if (!authToken && config.url !== "/auth/login") {
console.error("[MCP] No token found. Attempting auto-login...");
@@ -143,7 +145,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
{
name: "update_page_translation",
description: "Add or update translations for a specific page.",
description: "Add or update one page title translation for a specific page. Use list_dynamic_pages_metadata first to discover the numeric page ID.",
inputSchema: {
type: "object",
properties: {
@@ -156,7 +158,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
{
name: "update_page_translations",
description: "Add or update multiple page title translations in one call. Use this when you want to set several languages at once instead of calling update_page_translation repeatedly.",
description: "Add or update multiple page title translations in one call. Use list_dynamic_pages_metadata first to discover the numeric page ID.",
inputSchema: {
type: "object",
properties: {
@@ -700,17 +702,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
{
name: "list_dynamic_pages",
description: "List all dynamic pages and layouts managed by the CMS",
description: "List all dynamic pages and layouts managed by the CMS in a lightweight discovery format. Use get_dynamic_page_detail for full section payloads.",
inputSchema: { type: "object", properties: {} }
},
{
name: "list_dynamic_pages_metadata",
description: "List all dynamic pages and layouts managed by the CMS (metadata only)",
description: "List all dynamic pages and layouts managed by the CMS in the smallest discovery format (id, slug, title only).",
inputSchema: { type: "object", properties: {} }
},
{
name: "get_dynamic_page_detail",
description: "Get full detail of a specific dynamic page by slug",
description: "Get full detail of a specific dynamic page by slug. By default only active sections are returned; set includeInactive=true to include inactive sections.",
inputSchema: {
type: "object",
properties: {
@@ -722,15 +724,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
{
name: "create_dynamic_page",
description: "Create or update a dynamic page or layout",
description: "Create or update a dynamic page or layout. Use isPublished as the only publish field. If omitted on create, it defaults to false. If omitted on update, the existing publish state is preserved. The response includes publishBehavior to make this explicit. Page-level metadata is not supported by the current Page model.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Title of the page" },
slug: { type: "string", description: "Unique slug identifier. Do not use reserved system slugs (e.g., shop, category, cart, checkout, account, article, etc.)." },
isPublished: { type: "boolean", description: "Publish status" },
status: { type: "string", description: "Publish status (e.g. 'published' or 'draft')" },
metadata: { type: "object", description: "SEO metadata" },
isPublished: { type: "boolean", description: "Publish status. Optional; defaults to false if omitted." },
id: { type: "number", description: "Numeric page ID. Required if you are updating an existing page. Use list_dynamic_pages_metadata or get_dynamic_page_detail to find the ID first." }
},
required: ["title", "slug"]
@@ -1160,8 +1160,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
slug: params.slug,
};
if (params.isPublished !== undefined) payload.isPublished = params.isPublished;
if (params.status !== undefined) payload.status = params.status;
if (params.metadata !== undefined) payload.metadata = params.metadata;
if (params.id) {
payload.id = params.id;
}
@@ -1173,7 +1171,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
pageId: res.data?.pageId ?? pageData?.id ?? null,
slug: res.data?.slug ?? pageData?.slug ?? params.slug,
title: res.data?.title ?? pageData?.title ?? params.title,
isPublished: res.data?.isPublished ?? pageData?.isPublished ?? payload.isPublished ?? true,
isPublished: res.data?.isPublished ?? pageData?.isPublished ?? payload.isPublished ?? false,
publishBehavior: res.data?.publishBehavior ?? (payload.isPublished === undefined ? (params.id ? "preserve-existing" : "default-create-false") : "request"),
diagnostic: res.data?.diagnostic ?? (params.id ? "Page updated successfully." : "Page created successfully."),
page: pageData,
};
+181 -22
View File
@@ -19,6 +19,91 @@ const localizedStringListSchema = {
const localizedStringExample = '{ "th": "ข้อความภาษาไทย", "en": "English text" }';
const localizedStringListExample = `[ ${localizedStringExample} ]`;
const categoryIdListExample = `[1, 2]`;
const productIdListExample = `[101, 102]`;
const ADD_PAGE_SECTION_GUIDANCE: Record<string, { required: string[]; example: string }> = {
HERO: {
required: ["tagline", "title", "description", "imageUrl", "buttonText", "buttonLink"],
example: `{ "tagline": ${localizedStringExample}, "title": ${localizedStringExample}, "description": ${localizedStringExample}, "imageUrl": "https://example.com/hero.jpg", "buttonText": ${localizedStringExample}, "buttonLink": "/shop" }`
},
ANNOUNCEMENT_BAR: {
required: ["slides"],
example: `{ "slides": [${localizedStringExample}] }`
},
NEWSLETTER: {
required: ["title", "description", "buttonText", "showPrivacyCheckbox"],
example: `{ "title": ${localizedStringExample}, "description": ${localizedStringExample}, "buttonText": ${localizedStringExample}, "showPrivacyCheckbox": true }`
},
EDITORIAL_TILE: {
required: ["title", "description", "imageUrl", "imagePosition"],
example: `{ "title": ${localizedStringExample}, "description": ${localizedStringExample}, "imageUrl": "https://example.com/tile.jpg", "imagePosition": "left" }`
},
EDITORIAL_CAROUSEL: {
required: ["title", "slides[].category", "slides[].title", "slides[].description", "slides[].imageUrl"],
example: `{ "title": ${localizedStringExample}, "slides": [ { "category": ${localizedStringExample}, "title": ${localizedStringExample}, "description": ${localizedStringExample}, "imageUrl": "https://example.com/slide.jpg" } ] }`
},
PRODUCTS: {
required: ["title", "type"],
example: `{ "title": ${localizedStringExample}, "type": "custom", "productIds": ${productIdListExample}, "limit": 4 }`
},
CATEGORIES: {
required: ["title"],
example: `{ "title": ${localizedStringExample}, "categoryIds": ${categoryIdListExample} }`
},
TEXT: {
required: ["content"],
example: `{ "title": ${localizedStringExample}, "content": ${localizedStringExample} }`
},
BANNER: {
required: ["imageUrl"],
example: `{ "imageUrl": "https://example.com/banner.jpg", "title": ${localizedStringExample}, "buttonText": ${localizedStringExample} }`
},
HERO_ARTICLE: {
required: ["title"],
example: `{ "title": ${localizedStringExample}, "subtitle": ${localizedStringExample}, "imageUrl": "https://example.com/article.jpg" }`
},
RICH_TEXT: {
required: ["content"],
example: `{ "title": ${localizedStringExample}, "content": ${localizedStringExample} }`
},
SCENT_NOTES_BREAKDOWN: {
required: [],
example: `{ "title": ${localizedStringExample}, "topNotes": ${localizedStringListExample} }`
},
QUOTE_CALLOUT: {
required: ["quote"],
example: `{ "quote": ${localizedStringExample}, "author": ${localizedStringExample} }`
},
PRODUCT_CAROUSEL: {
required: ["type"],
example: `{ "type": "custom", "productIds": ${productIdListExample}, "title": ${localizedStringExample}, "limit": 4 }`
},
ACCORDION: {
required: [],
example: `{ "title": ${localizedStringExample}, "categories": [ { "name": ${localizedStringExample}, "questions": [ { "q": ${localizedStringExample}, "a": ${localizedStringExample} } ] } ] }`
},
CONTACT_INFO: {
required: ["address", "email", "line", "hours"],
example: `{ "title": ${localizedStringExample}, "address": ${localizedStringExample}, "email": "hello@example.com", "line": "@homaroma", "hours": ${localizedStringExample} }`
},
CONTACT_FORM: {
required: [],
example: `{ "title": ${localizedStringExample}, "subtitle": ${localizedStringExample} }`
}
};
function buildAddPageSectionDescription() {
const toolSummaries = PAGE_SECTION_TYPES.map((type) => {
const guide = ADD_PAGE_SECTION_GUIDANCE[type] ?? {
required: [],
example: `{ }`
};
return `- ${type}: required ${guide.required.length > 0 ? guide.required.join(", ") : "no always-required payload fields"}. Minimal example ${guide.example}`;
}).join("\n");
return `Add a block section to a dynamic page layout. The payload is fully replaced on create and must satisfy the exact schema for the chosen section type. Use the following required fields and minimal examples for first-time use:\n${toolSummaries}`;
}
const SECTION_TOOL_CONFIGS = [
{
@@ -97,16 +182,20 @@ const SECTION_TOOL_CONFIGS = [
{
name: "update_product_carousel",
sectionType: "PRODUCT_CAROUSEL",
description: `Update a PRODUCT_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. Title and description are localized. collectionId, limit, showPrice, showAddToCart, and products are plain values. If products is omitted, backend can auto-populate products on read-back using collectionId/type/productIds/limit. Example payload: { "sectionId": 52, "title": ${localizedStringExample}, "collectionId": "summer", "limit": 6, "showPrice": true }`,
description: `Update a PRODUCT_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. Title and description are localized. type is required for a valid stored payload. Use categoryId for featured carousels, brandId for brand carousels, or productIds for custom carousels. Example payload: { "sectionId": 52, "type": "custom", "productIds": ${productIdListExample}, "title": ${localizedStringExample}, "limit": 6, "showPrice": true }`,
properties: {
sectionId: { type: "number", description: "The numeric ID of the section to update" },
title: { ...localizedStringSchema, description: `Localized title, for example ${localizedStringExample}` },
description: { ...localizedStringSchema, description: `Localized supporting description, for example ${localizedStringExample}` },
collectionId: { type: "string", description: "Optional plain string collection identifier" },
type: { type: "string", enum: ["featured", "brand", "custom"], description: "Canonical carousel mode used for validation and product resolution" },
categoryId: { type: "number", description: "Category ID for featured carousels" },
brandId: { type: "number", description: "Brand ID for brand carousels" },
productIds: { type: "array", description: `Explicit product IDs for custom carousels, for example ${productIdListExample}`, items: { type: "number" } },
limit: { type: "number", description: "Maximum number of products to show" },
showPrice: { type: "boolean", description: "Show product price" },
showAddToCart: { type: "boolean", description: "Show add to cart button" },
products: { type: "array", description: "Optional explicit product list. Omit this to let backend auto-populate on read-back.", items: { type: "object" } }
products: { type: "array", description: "Optional explicit product list. Omit this to let backend resolve products by type/categoryId/brandId/productIds.", items: { type: "object" } },
includeHydratedProducts: { type: "boolean", description: "Optional. Set true to return hydrated products in the response. Default false for lightweight responses." }
},
required: ["sectionId"]
},
@@ -219,13 +308,35 @@ function buildSectionPayload(params: any, fields: readonly string[]) {
return payload;
}
function buildSectionUpdateResult(toolName: string, sectionType: string, updatedSection: any, payload: Record<string, any>) {
function sanitizeSectionForResponse(sectionType: string, section: any, options?: { includeHydratedProducts?: boolean }) {
if (!section || !section.payload) return section;
if (sectionType === "PRODUCT_CAROUSEL" && !options?.includeHydratedProducts) {
return {
...section,
payload: {
...section.payload,
products: undefined,
}
};
}
return section;
}
function buildSectionUpdateResult(
toolName: string,
sectionType: string,
updatedSection: any,
payload: Record<string, any>,
options?: { includeHydratedProducts?: boolean }
) {
const notes: string[] = [];
if (sectionType === "SCENT_NOTES_BREAKDOWN") {
notes.push("heartNotes and middleNotes are mirrored automatically by the backend");
}
if (sectionType === "PRODUCT_CAROUSEL") {
notes.push("If products was omitted, the backend may populate products on read-back");
if (sectionType === "PRODUCT_CAROUSEL" && !options?.includeHydratedProducts) {
notes.push("Hydrated products are omitted by default. Pass includeHydratedProducts=true to inspect resolved products.");
}
return {
content: [
@@ -240,7 +351,7 @@ function buildSectionUpdateResult(toolName: string, sectionType: string, updated
updatedFields: Object.keys(payload),
appliedPayload: payload,
notes,
section: updatedSection
section: sanitizeSectionForResponse(sectionType, updatedSection, options)
},
null,
2
@@ -265,14 +376,14 @@ export const SECTION_TOOL_DEFINITIONS = [
},
{
name: "add_page_section",
description: "Add a block section to a dynamic page layout. For the payload field, ensure it matches the section type. Examples: For TEXT, use { \"text\": \"...\" }. For RICH_TEXT, use { \"title\": \"...\", \"content\": \"...\" }.",
description: buildAddPageSectionDescription(),
inputSchema: {
type: "object",
properties: {
pageId: { type: "number", description: "The numeric ID of the page" },
type: { type: "string", enum: [...PAGE_SECTION_TYPES], description: "Block type (e.g. HERO, ANNOUNCEMENT_BAR, NEWSLETTER, EDITORIAL_TILE, EDITORIAL_CAROUSEL, PRODUCTS, CATEGORIES, TEXT, BANNER, HERO_ARTICLE, RICH_TEXT, SCENT_NOTES_BREAKDOWN, QUOTE_CALLOUT, PRODUCT_CAROUSEL)" },
order: { type: "number", description: "Display order sequence (starts at 1)" },
payload: { type: "object", description: "The JSON payload matching the section type rules" },
payload: { type: "object", description: "The JSON payload matching the exact schema for the chosen type. See the tool description for required fields and minimal valid examples per type." },
isActive: { type: "boolean", description: "Initial active state" }
},
required: ["pageId", "type", "payload"]
@@ -280,7 +391,7 @@ export const SECTION_TOOL_DEFINITIONS = [
},
{
name: "update_page_section",
description: "Update an existing block section payload, order or active state. If you are modifying the payload, it is strongly recommended to also provide the 'type' field to ensure correct validation.",
description: "Update an existing block section payload, order, or active state. If 'payload' is provided here, it replaces the stored payload after backend validation. This tool does not merge payload keys. Use the type-specific update tools when you want MCP to merge a partial payload with the existing stored payload before writing.",
inputSchema: {
type: "object",
properties: {
@@ -367,7 +478,7 @@ export const SECTION_TOOL_DEFINITIONS = [
question: { ...localizedStringSchema, description: "Optional localized question text" },
answer: { ...localizedStringSchema, description: "Optional localized answer text" }
},
required: ["sectionId", "categoryIndex", "itemIndex"]
required: ["sectionId", "categoryId", "questionId"]
}
},
{
@@ -436,7 +547,8 @@ export async function handleSectionTool(api: AxiosInstance, name: string, params
throw new Error(`Unknown tool config for: ${name}`);
}
const payload = buildSectionPayload(params, Object.keys(config.properties).filter((key) => key !== "sectionId"));
const payloadFields = Object.keys(config.properties).filter((key) => key !== "sectionId" && key !== "includeHydratedProducts");
const payload = buildSectionPayload(params, payloadFields);
if (config.sectionType === "SCENT_NOTES_BREAKDOWN") {
if (payload.heartNotes !== undefined && payload.middleNotes === undefined) {
payload.middleNotes = payload.heartNotes;
@@ -448,7 +560,7 @@ export async function handleSectionTool(api: AxiosInstance, name: string, params
// Fetch existing section to merge payload so AI doesn't have to provide all required fields every time
let mergedPayload = payload;
try {
const existingRes = await api.get(`/pages/sections/${params.sectionId}?lang=all`);
const existingRes = await api.get(`/pages/sections/${params.sectionId}?hydrate=false`);
if (existingRes.data && existingRes.data.payload) {
mergedPayload = { ...existingRes.data.payload, ...payload };
}
@@ -461,19 +573,31 @@ export async function handleSectionTool(api: AxiosInstance, name: string, params
payload: mergedPayload
});
return buildSectionUpdateResult(name, config.sectionType, res.data, payload);
return buildSectionUpdateResult(name, config.sectionType, res.data, payload, {
includeHydratedProducts: params.includeHydratedProducts === true
});
}
case "delete_page_section": {
const res = await api.delete(`/pages/sections/${params.sectionId}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
await api.delete(`/pages/sections/${params.sectionId}`);
return {
content: [{
type: "text",
text: JSON.stringify({ success: true, sectionId: params.sectionId }, null, 2)
}]
};
}
case "add_accordion_category":
case "update_accordion_category":
case "add_accordion_question":
case "update_accordion_question": {
const existingRes = await api.get(`/pages/sections/${params.sectionId}?lang=all`);
const existingRes = await api.get(`/pages/sections/${params.sectionId}?hydrate=false`);
const payload = existingRes.data?.payload || {};
payload.categories = payload.categories || [];
let responseBody: Record<string, any> = {
success: true,
tool: name,
sectionId: params.sectionId,
};
if (name === "add_accordion_category") {
const questions = params.questions?.map((q: any) => ({
@@ -481,42 +605,77 @@ export async function handleSectionTool(api: AxiosInstance, name: string, params
q: q.q,
a: q.a
})) || [];
payload.categories.push({
const category = {
id: crypto.randomUUID(),
name: params.name,
icon: params.icon,
questions
});
};
payload.categories.push(category);
responseBody = {
...responseBody,
categoryId: category.id,
category,
categoryCount: payload.categories.length,
};
} else if (name === "update_accordion_category") {
const cat = payload.categories.find((c: any) => c.id === params.categoryId);
if (!cat) throw new Error(`Category with id ${params.categoryId} not found`);
const changedFields: string[] = [];
if (params.name) {
cat.name = typeof params.name === "object" && typeof cat.name === "object"
? { ...cat.name, ...params.name }
: params.name;
changedFields.push("name");
}
if (params.icon !== undefined) cat.icon = params.icon;
if (params.icon !== undefined) {
cat.icon = params.icon;
changedFields.push("icon");
}
responseBody = {
...responseBody,
categoryId: cat.id,
changedFields,
category: cat,
};
} else if (name === "add_accordion_question") {
const cat = payload.categories.find((c: any) => c.id === params.categoryId);
if (!cat) throw new Error(`Category with id ${params.categoryId} not found`);
cat.questions = cat.questions || [];
cat.questions.push({ id: crypto.randomUUID(), q: params.question, a: params.answer });
const question = { id: crypto.randomUUID(), q: params.question, a: params.answer };
cat.questions.push(question);
responseBody = {
...responseBody,
categoryId: cat.id,
questionId: question.id,
question: question,
};
} else if (name === "update_accordion_question") {
const cat = payload.categories.find((c: any) => c.id === params.categoryId);
if (!cat) throw new Error(`Category with id ${params.categoryId} not found`);
cat.questions = cat.questions || [];
const item = cat.questions.find((q: any) => q.id === params.questionId);
if (!item) throw new Error(`Question with id ${params.questionId} not found`);
const changedFields: string[] = [];
if (params.question) {
item.q = typeof params.question === "object" && typeof item.q === "object"
? { ...item.q, ...params.question }
: params.question;
changedFields.push("question");
}
if (params.answer) {
item.a = typeof params.answer === "object" && typeof item.a === "object"
? { ...item.a, ...params.answer }
: params.answer;
changedFields.push("answer");
}
responseBody = {
...responseBody,
categoryId: cat.id,
questionId: item.id,
changedFields,
question: item,
};
}
await api.put(`/pages/sections/${params.sectionId}`, {
@@ -524,7 +683,7 @@ export async function handleSectionTool(api: AxiosInstance, name: string, params
payload
});
return { content: [{ type: "text", text: "Success message" }] };
return { content: [{ type: "text", text: JSON.stringify(responseBody, null, 2) }] };
}
default:
return null;