/**
* NocoBase Page Tools - Enhanced Version
*
* Uses template-based approach for reliable UI generation.
*/
import { z } from 'zod';
import { NocoBaseClient } from '../client.js';
import * as Templates from '../templates.js';
// UID Generator
function generateUid(): string {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 6);
}
// =============================================================================
// SCHEMA VALIDATION
// =============================================================================
export const CreateCRMTablePageSchema = z.object({
title: z.string().min(1).describe('Page title'),
collection: z.string().min(1).describe('Collection name'),
icon: z.string().optional().default('TableOutlined'),
fields: z.array(z.string()).optional().describe('Fields to show as columns'),
popupFields: z.array(z.string()).optional().describe('Fields to show in View/Edit popups'),
pageSize: z.number().optional().default(20),
parentId: z.string().optional().describe('Parent group ID to place page in'),
});
export const CreateCRMKanbanPageSchema = z.object({
title: z.string().min(1).describe('Page title'),
collection: z.string().min(1).describe('Collection name'),
icon: z.string().optional().default('AppstoreOutlined'),
groupField: z.string().optional().default('status').describe('Field to group by'),
cardFields: z.array(z.string()).optional().describe('Fields to show on cards'),
parentId: z.string().optional().describe('Parent group ID to place page in'),
});
export const CreateCRMCalendarPageSchema = z.object({
title: z.string().min(1).describe('Page title'),
collection: z.string().min(1).describe('Collection name'),
icon: z.string().optional().default('CalendarOutlined'),
startDateField: z.string().optional().default('start_date'),
endDateField: z.string().optional().default('end_date'),
titleField: z.string().optional().default('title'),
parentId: z.string().optional().describe('Parent group ID to place page in'),
});
export const ListRoutesSchema = z.object({
type: z.enum(['desktop', 'mobile']).optional().default('desktop'),
limit: z.number().optional().default(100),
});
export const DeletePageSchema = z.object({
routeId: z.string().describe('Route ID to delete'),
type: z.enum(['desktop', 'mobile']).optional().default('desktop'),
});
export const GetPageSchemaInputSchema = z.object({
routeId: z.string().describe('Route ID'),
type: z.enum(['desktop', 'mobile']).optional().default('desktop'),
});
export const InspectPageSchema = z.object({
schemaUid: z.string().describe('Schema UID to inspect'),
depth: z.number().optional().default(5),
});
export const RawInsertSchemaInput = z.object({
schema: z.any().describe('Raw JSON schema to insert'),
});
export const AddColumnsToTableSchema = z.object({
tableUid: z.string().describe('UID of the TableV2 component'),
collection: z.string().describe('Collection name'),
fields: z.array(z.string()).describe('Field names to add as columns'),
});
export const AddFieldsToFormSchema = z.object({
formGridUid: z.string().describe('UID of the Grid inside FormV2'),
collection: z.string().describe('Collection name'),
fields: z.array(z.string()).describe('Field names to add'),
});
// =============================================================================
// MENU STRUCTURE SCHEMAS
// =============================================================================
export const CreateGroupSchema = z.object({
title: z.string().min(1).describe('Group title'),
icon: z.string().optional().default('FolderOutlined'),
parentId: z.string().optional().describe('Parent group ID (for nested groups)'),
});
export const CreateLinkSchema = z.object({
title: z.string().min(1).describe('Link title'),
url: z.string().min(1).describe('URL (external or internal path)'),
icon: z.string().optional().default('LinkOutlined'),
parentId: z.string().optional().describe('Parent group ID'),
openInNewTab: z.boolean().optional().default(false),
});
export const MoveToGroupSchema = z.object({
routeId: z.string().describe('Route ID to move'),
targetGroupId: z.string().describe('Target group ID'),
type: z.enum(['desktop', 'mobile']).optional().default('desktop'),
});
// =============================================================================
// V1 PAGE SCHEMAS
// =============================================================================
export const CreateV1PageSchema = z.object({
title: z.string().min(1).describe('Page title'),
collection: z.string().optional().describe('Collection name (for data blocks)'),
icon: z.string().optional().default('FileOutlined'),
parentId: z.string().optional().describe('Parent group ID'),
blockType: z.enum(['table', 'kanban', 'calendar', 'form', 'details', 'empty']).optional().default('empty'),
blockOptions: z.any().optional(),
});
// =============================================================================
// POPUP/DRAWER SCHEMAS
// =============================================================================
export const AddPopupTabSchema = z.object({
popupSchemaUid: z.string().describe('UID of the popup/drawer'),
tabTitle: z.string().describe('Tab title'),
collection: z.string().optional().describe('Collection for blocks in this tab'),
});
export const ConfigurePopupBlockSchema = z.object({
popupTabGridUid: z.string().describe('UID of Grid inside the popup tab'),
blockType: z.enum(['details', 'form', 'table', 'related']).describe('Block type'),
collection: z.string().describe('Collection name'),
options: z.any().optional(),
});
export const PageSaveAsTemplateSchema = z.object({
schemaUid: z.string().describe('Schema UID to save'),
name: z.string().describe('Template name'),
collectionName: z.string().optional(),
componentName: z.string().optional(),
});
export const GetSchemaPropertiesSchema = z.object({
schemaUid: z.string().describe('Schema UID to get properties for'),
});
// =============================================================================
// CRM PAGE CREATION TOOLS
// =============================================================================
/**
* Create a CRM Table Page
* Full-featured table with Filter, Add New, View, Edit, Delete actions
*/
export async function createCRMTablePage(client: NocoBaseClient, input: any) {
const validated = CreateCRMTablePageSchema.parse(input);
console.error(`[createCRMTablePage] Creating table page for ${validated.collection}...`);
// Create table block from template
const tableBlock = Templates.createCRMTableTemplate(validated.collection, {
fields: validated.fields,
popupFields: validated.popupFields,
pageSize: validated.pageSize,
});
// Wrap in page
const { pageSchema, pageUid, tabUid, tabName, blockUid } = Templates.wrapInPage(tableBlock);
// Insert schema
try {
const insertRes = await client.insertSchema(pageSchema);
console.error(`[createCRMTablePage] Schema inserted: ${pageUid}`);
} catch (e: any) {
console.error(`[createCRMTablePage] Schema insert failed:`, e.message);
if (e.response?.data) console.error(JSON.stringify(e.response.data));
throw new Error(`Failed to insert schema: ${e.message}`);
}
// Create route
const routePayload: any = {
type: 'page',
title: validated.title,
icon: validated.icon,
schemaUid: pageUid,
menuSchemaUid: generateUid(),
enableTabs: false,
children: [{
type: 'tabs',
schemaUid: tabUid,
tabSchemaName: tabName,
hidden: true
}]
};
// Add parentId if specified
if (validated.parentId) {
routePayload.parentId = validated.parentId;
}
try {
const routeRes = await client.createRoute('desktop', routePayload);
const routeId = routeRes.data?.data?.id;
console.error(`[createCRMTablePage] Route created: ${routeId}`);
return {
success: true,
routeId,
schemaUid: pageUid,
tableUid: blockUid,
url: `/admin/${routeId}`,
message: `Table page "${validated.title}" created successfully. Open NocoBase and navigate to the page. Use "Configure columns" to add fields.`
};
} catch (e: any) {
console.error(`[createCRMTablePage] Route create failed:`, e.message);
if (e.response?.data) console.error(JSON.stringify(e.response.data));
throw new Error(`Failed to create route: ${e.message}`);
}
}
/**
* Create a CRM Kanban Page
* Kanban board grouped by status with card view
*/
export async function createCRMKanbanPage(client: NocoBaseClient, input: any) {
const validated = CreateCRMKanbanPageSchema.parse(input);
console.error(`[createCRMKanbanPage] Creating kanban page for ${validated.collection}...`);
// Create kanban block from template
const kanbanBlock = Templates.createCRMKanbanTemplate(validated.collection, {
groupField: validated.groupField,
cardFields: validated.cardFields,
});
// Wrap in page
const { pageSchema, pageUid, tabUid, tabName, blockUid } = Templates.wrapInPage(kanbanBlock);
// Insert schema
try {
await client.insertSchema(pageSchema);
console.error(`[createCRMKanbanPage] Schema inserted: ${pageUid}`);
} catch (e: any) {
console.error(`[createCRMKanbanPage] Schema insert failed:`, e.message);
throw new Error(`Failed to insert schema: ${e.message}`);
}
// Create route
const routePayload: any = {
type: 'page',
title: validated.title,
icon: validated.icon,
schemaUid: pageUid,
menuSchemaUid: generateUid(),
enableTabs: false,
children: [{
type: 'tabs',
schemaUid: tabUid,
tabSchemaName: tabName,
hidden: true
}]
};
// Add parentId if specified
if (validated.parentId) {
routePayload.parentId = validated.parentId;
}
try {
const routeRes = await client.createRoute('desktop', routePayload);
const routeId = routeRes.data?.data?.id;
return {
success: true,
routeId,
schemaUid: pageUid,
kanbanUid: blockUid,
url: `/admin/${routeId}`,
message: `Kanban page "${validated.title}" created. Cards grouped by "${validated.groupField}".`
};
} catch (e: any) {
throw new Error(`Failed to create route: ${e.message}`);
}
}
/**
* Create a CRM Calendar Page
* Calendar view for activities/tasks
*/
export async function createCRMCalendarPage(client: NocoBaseClient, input: any) {
const validated = CreateCRMCalendarPageSchema.parse(input);
console.error(`[createCRMCalendarPage] Creating calendar page for ${validated.collection}...`);
// Create calendar block from template
const calendarBlock = Templates.createCRMCalendarTemplate(validated.collection, {
startDateField: validated.startDateField,
endDateField: validated.endDateField,
titleField: validated.titleField,
});
// Wrap in page
const { pageSchema, pageUid, tabUid, tabName, blockUid } = Templates.wrapInPage(calendarBlock);
// Insert schema
try {
await client.insertSchema(pageSchema);
console.error(`[createCRMCalendarPage] Schema inserted: ${pageUid}`);
} catch (e: any) {
console.error(`[createCRMCalendarPage] Schema insert failed:`, e.message);
throw new Error(`Failed to insert schema: ${e.message}`);
}
// Create route
const routePayload: any = {
type: 'page',
title: validated.title,
icon: validated.icon,
schemaUid: pageUid,
menuSchemaUid: generateUid(),
enableTabs: false,
children: [{
type: 'tabs',
schemaUid: tabUid,
tabSchemaName: tabName,
hidden: true
}]
};
// Add parentId if specified
if (validated.parentId) {
routePayload.parentId = validated.parentId;
}
try {
const routeRes = await client.createRoute('desktop', routePayload);
const routeId = routeRes.data?.data?.id;
return {
success: true,
routeId,
schemaUid: pageUid,
calendarUid: blockUid,
url: `/admin/${routeId}`,
message: `Calendar page "${validated.title}" created. Using "${validated.startDateField}" as start date field.`
};
} catch (e: any) {
throw new Error(`Failed to create route: ${e.message}`);
}
}
// =============================================================================
// MENU STRUCTURE TOOLS (GROUP, LINK)
// =============================================================================
/**
* Create a menu group
* Groups can contain other groups, pages, or links
*/
export async function createGroup(client: NocoBaseClient, input: any) {
const validated = CreateGroupSchema.parse(input);
const routePayload: any = {
type: 'group',
title: validated.title,
icon: validated.icon,
};
// If has parent, set parentId
if (validated.parentId) {
routePayload.parentId = validated.parentId;
}
const routeRes = await client.createRoute('desktop', routePayload);
const routeId = routeRes.data?.data?.id;
return {
success: true,
groupId: routeId,
title: validated.title,
message: `Group "${validated.title}" created. You can now add pages or subgroups to it.`
};
}
/**
* Create a menu link (external or internal URL)
*/
export async function createLink(client: NocoBaseClient, input: any) {
const validated = CreateLinkSchema.parse(input);
const routePayload: any = {
type: 'link',
title: validated.title,
icon: validated.icon,
options: {
url: validated.url,
target: validated.openInNewTab ? '_blank' : '_self'
}
};
if (validated.parentId) {
routePayload.parentId = validated.parentId;
}
const routeRes = await client.createRoute('desktop', routePayload);
const routeId = routeRes.data?.data?.id;
return {
success: true,
linkId: routeId,
title: validated.title,
url: validated.url,
message: `Link "${validated.title}" created.`
};
}
/**
* Move a page/group to another group
*/
export async function moveToGroup(client: NocoBaseClient, input: any) {
const validated = MoveToGroupSchema.parse(input);
const response = await client.updateRoute(validated.type, validated.routeId, {
parentId: validated.targetGroupId
});
return {
success: true,
routeId: validated.routeId,
newParentId: validated.targetGroupId,
message: 'Page moved to group successfully'
};
}
// =============================================================================
// V1 (CLASSIC) PAGE TOOLS
// =============================================================================
/**
* Create a Classic (V1) page
* Uses older component structure - more block types available
*/
export async function createV1Page(client: NocoBaseClient, input: any) {
const validated = CreateV1PageSchema.parse(input);
console.error(`[createV1Page] Creating V1 page: ${validated.title}...`);
// Create block if collection specified
let blockSchema: any = null;
if (validated.collection && validated.blockType !== 'empty') {
switch (validated.blockType) {
case 'table':
blockSchema = Templates.createV1TableBlock(validated.collection, validated.blockOptions);
break;
case 'kanban':
blockSchema = Templates.createV1KanbanBlock(validated.collection, validated.blockOptions);
break;
case 'calendar':
blockSchema = Templates.createV1CalendarBlock(validated.collection, validated.blockOptions);
break;
case 'form':
blockSchema = Templates.createV1FormBlock(validated.collection, validated.blockOptions);
break;
default:
blockSchema = null;
}
}
// Wrap in V1 page structure
const { pageSchema, pageUid, gridUid, blockUid } = Templates.wrapInV1Page(blockSchema);
// Insert schema
try {
await client.insertSchema(pageSchema);
console.error(`[createV1Page] Schema inserted: ${pageUid}`);
} catch (e: any) {
console.error(`[createV1Page] Schema insert failed:`, e.message);
throw new Error(`Failed to insert schema: ${e.message}`);
}
// Create route (V1 pages don't have tabs structure)
const routePayload: any = {
type: 'page',
title: validated.title,
icon: validated.icon,
schemaUid: pageUid,
menuSchemaUid: generateUid(),
};
if (validated.parentId) {
routePayload.parentId = validated.parentId;
}
try {
const routeRes = await client.createRoute('desktop', routePayload);
const routeId = routeRes.data?.data?.id;
return {
success: true,
routeId,
schemaUid: pageUid,
gridUid,
blockUid,
pageType: 'v1',
url: `/admin/${routeId}`,
message: `Classic page "${validated.title}" created. Use gridUid with add_v1_block to add blocks, or use NocoBase UI.`
};
} catch (e: any) {
throw new Error(`Failed to create route: ${e.message}`);
}
}
/**
* Add a V1 block to an existing V1 page
*/
export const AddV1BlockSchema = z.object({
gridUid: z.string().describe('Grid UID of the V1 page (returned from create_v1_page)'),
blockType: z.enum(['table', 'kanban', 'calendar', 'form', 'details']).describe('Block type'),
collection: z.string().describe('Collection name'),
options: z.any().optional().describe('Block-specific options'),
});
export async function addV1Block(client: NocoBaseClient, input: any) {
const validated = AddV1BlockSchema.parse(input);
console.error(`[addV1Block] Adding ${validated.blockType} block for ${validated.collection}...`);
let blockSchema: any;
const options = validated.options || {};
switch (validated.blockType) {
case 'table':
blockSchema = Templates.createV1TableBlock(validated.collection, options);
break;
case 'kanban':
blockSchema = Templates.createV1KanbanBlock(validated.collection, options);
break;
case 'calendar':
blockSchema = Templates.createV1CalendarBlock(validated.collection, options);
break;
case 'form':
blockSchema = Templates.createV1FormBlock(validated.collection, options);
break;
case 'details':
// V1 Details block
const detailsUid = generateUid();
blockSchema = {
type: 'void',
'x-decorator': 'DetailsBlockProvider',
'x-decorator-props': {
collection: validated.collection,
resource: validated.collection,
action: 'get',
readPretty: true,
},
'x-designer': 'DetailsDesigner',
'x-component': 'CardItem',
'x-uid': detailsUid,
properties: {
details: {
type: 'void',
'x-component': 'Details',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useDetailsBlockProps }}',
},
'x-uid': generateUid(),
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'ReadPrettyFormItemInitializers',
'x-uid': generateUid(),
properties: {}
}
}
}
}
};
break;
default:
throw new Error(`Block type ${validated.blockType} not supported`);
}
// Wrap in Grid.Row > Grid.Col
const rowUid = generateUid();
const colUid = generateUid();
const wrappedBlock = {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': rowUid,
properties: {
[colUid]: {
type: 'void',
'x-component': 'Grid.Col',
'x-uid': colUid,
properties: {
[blockSchema['x-uid']]: blockSchema
}
}
}
};
// Insert into grid
try {
await client.insertSchemaAdjacent(validated.gridUid, 'beforeEnd', Templates.enrichedSchema(wrappedBlock));
return {
success: true,
gridUid: validated.gridUid,
blockUid: blockSchema['x-uid'],
blockType: validated.blockType,
collection: validated.collection,
message: `Added ${validated.blockType} block for ${validated.collection}. Configure columns/fields in NocoBase UI.`
};
} catch (e: any) {
console.error(`[addV1Block] Failed:`, e.message);
throw new Error(`Failed to add block: ${e.message}`);
}
}
/**
* Add action buttons to a V1 table block
*/
export const AddV1TableActionsSchema = z.object({
tableBlockUid: z.string().describe('UID of the V1 table block (CardItem)'),
actions: z.array(z.enum(['filter', 'add', 'refresh', 'bulkDelete'])).describe('Actions to add'),
});
export async function addV1TableActions(client: NocoBaseClient, input: any) {
const validated = AddV1TableActionsSchema.parse(input);
// Get the table block to find action bar
const schemaRes = await client.getSchema(validated.tableBlockUid, true);
const blockSchema = schemaRes.data?.data;
if (!blockSchema) {
throw new Error(`Block ${validated.tableBlockUid} not found`);
}
// Find ActionBar UID
let actionBarUid: string | null = null;
if (blockSchema.properties?.actions) {
actionBarUid = blockSchema.properties.actions['x-uid'];
}
if (!actionBarUid) {
throw new Error('ActionBar not found in block');
}
const addedActions: string[] = [];
for (const action of validated.actions) {
let actionSchema: any;
switch (action) {
case 'filter':
actionSchema = {
type: 'void',
title: '{{ t("Filter") }}',
'x-action': 'filter',
'x-designer': 'Filter.Action.Designer',
'x-component': 'Filter.Action',
'x-component-props': {
icon: 'FilterOutlined',
useProps: '{{ useFilterActionProps }}',
},
'x-align': 'left',
'x-uid': generateUid()
};
break;
case 'add':
actionSchema = {
type: 'void',
title: '{{ t("Add new") }}',
'x-action': 'create',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-component-props': {
icon: 'PlusOutlined',
type: 'primary',
openMode: 'drawer',
},
'x-align': 'right',
'x-uid': generateUid(),
properties: {
drawer: {
type: 'void',
title: '{{ t("Add record") }}',
'x-component': 'Action.Container',
'x-component-props': {},
'x-uid': generateUid(),
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'CreateFormBlockInitializers',
'x-uid': generateUid(),
properties: {}
}
}
}
}
};
break;
case 'refresh':
actionSchema = {
type: 'void',
title: '{{ t("Refresh") }}',
'x-action': 'refresh',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-component-props': {
icon: 'ReloadOutlined',
useProps: '{{ useRefreshActionProps }}',
},
'x-align': 'left',
'x-uid': generateUid()
};
break;
case 'bulkDelete':
actionSchema = {
type: 'void',
title: '{{ t("Delete") }}',
'x-action': 'destroy',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-component-props': {
icon: 'DeleteOutlined',
useProps: '{{ useBulkDestroyActionProps }}',
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}"
}
},
'x-align': 'right',
'x-uid': generateUid()
};
break;
}
try {
await client.insertSchemaAdjacent(actionBarUid, 'beforeEnd', actionSchema);
addedActions.push(action);
} catch (e: any) {
console.error(`Failed to add action ${action}:`, e.message);
}
}
return {
success: true,
actionBarUid,
addedActions,
message: `Added ${addedActions.length} actions to table`
};
}
/**
* Add row actions (View, Edit, Delete) to a V1 table
*/
export const AddV1RowActionsSchema = z.object({
tableUid: z.string().describe('UID of the Table component (not CardItem)'),
collection: z.string().describe('Collection name'),
actions: z.array(z.enum(['view', 'edit', 'delete'])).optional().default(['view', 'edit', 'delete']),
});
export async function addV1RowActions(client: NocoBaseClient, input: any) {
const validated = AddV1RowActionsSchema.parse(input);
// Create actions column
const colUid = generateUid();
const spaceUid = generateUid();
const actionsColumn: any = {
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-component': 'Table.Column',
'x-decorator': 'Table.Column.ActionBar',
'x-designer': 'Table.RowActionDesigner',
'x-initializer': 'TableRecordActionInitializers',
'x-component-props': {
width: 150,
fixed: 'right',
},
'x-uid': colUid,
properties: {
[spaceUid]: {
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': { split: '|' },
'x-uid': spaceUid,
properties: {}
}
}
};
// Add each action
for (const action of validated.actions) {
const actionUid = generateUid();
switch (action) {
case 'view':
actionsColumn.properties[spaceUid].properties.view = {
type: 'void',
title: '{{ t("View") }}',
'x-action': 'view',
'x-designer': 'Action.Designer',
'x-component': 'Action.Link',
'x-component-props': { openMode: 'drawer' },
'x-uid': actionUid,
properties: {
drawer: {
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {},
'x-uid': generateUid(),
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'RecordBlockInitializers',
'x-uid': generateUid(),
properties: {}
}
}
}
}
};
break;
case 'edit':
actionsColumn.properties[spaceUid].properties.edit = {
type: 'void',
title: '{{ t("Edit") }}',
'x-action': 'update',
'x-designer': 'Action.Designer',
'x-component': 'Action.Link',
'x-component-props': { openMode: 'drawer' },
'x-uid': actionUid,
properties: {
drawer: {
type: 'void',
title: '{{ t("Edit record") }}',
'x-component': 'Action.Container',
'x-component-props': {},
'x-uid': generateUid(),
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'RecordBlockInitializers',
'x-uid': generateUid(),
properties: {}
}
}
}
}
};
break;
case 'delete':
actionsColumn.properties[spaceUid].properties.delete = {
type: 'void',
title: '{{ t("Delete") }}',
'x-action': 'destroy',
'x-designer': 'Action.Designer',
'x-component': 'Action.Link',
'x-component-props': {
useProps: '{{ useDestroyActionProps }}',
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}"
}
},
'x-uid': actionUid
};
break;
}
}
// Insert column into table
try {
await client.insertSchemaAdjacent(validated.tableUid, 'beforeEnd', actionsColumn);
return {
success: true,
tableUid: validated.tableUid,
actionsColumnUid: colUid,
actions: validated.actions,
message: `Added row actions column with: ${validated.actions.join(', ')}`
};
} catch (e: any) {
throw new Error(`Failed to add row actions: ${e.message}`);
}
}
/**
* Add columns to a V1 table
*/
export const AddV1ColumnsSchema = z.object({
tableUid: z.string().describe('UID of the Table component'),
collection: z.string().describe('Collection name'),
fields: z.array(z.string()).describe('Field names to add as columns'),
});
export async function addV1Columns(client: NocoBaseClient, input: any) {
const validated = AddV1ColumnsSchema.parse(input);
const addedColumns: string[] = [];
for (const fieldName of validated.fields) {
const colUid = generateUid();
const columnSchema = {
type: 'void',
'x-component': 'Table.Column',
'x-decorator': 'Table.Column.Decorator',
'x-designer': 'Table.Column.Designer',
'x-uid': colUid,
properties: {
[fieldName]: {
'x-collection-field': `${validated.collection}.${fieldName}`,
'x-component': 'CollectionField',
'x-read-pretty': true,
'x-uid': generateUid()
}
}
};
try {
await client.insertSchemaAdjacent(validated.tableUid, 'beforeEnd', columnSchema);
addedColumns.push(fieldName);
} catch (e: any) {
console.error(`Failed to add column ${fieldName}:`, e.message);
}
}
return {
success: true,
tableUid: validated.tableUid,
addedColumns,
message: `Added ${addedColumns.length} columns to V1 table`
};
}
// =============================================================================
// POPUP/DRAWER MANAGEMENT TOOLS
// =============================================================================
/**
* Add a tab to an existing popup/drawer
* Popups are found inside View/Edit actions
*/
export async function addPopupTab(client: NocoBaseClient, input: any) {
const validated = AddPopupTabSchema.parse(input);
// Get the popup schema
const schemaRes = await client.getSchema(validated.popupSchemaUid, true);
const popupSchema = schemaRes.data?.data;
if (!popupSchema) {
throw new Error(`Popup schema ${validated.popupSchemaUid} not found`);
}
// Find the Tabs component
let tabsUid: string | null = null;
function findTabs(node: any): void {
if (!node || typeof node !== 'object') return;
if (node['x-component'] === 'Tabs') {
tabsUid = node['x-uid'];
return;
}
if (node.properties) {
for (const key in node.properties) {
findTabs(node.properties[key]);
if (tabsUid) return;
}
}
}
findTabs(popupSchema);
if (!tabsUid) {
throw new Error('Could not find Tabs component in popup');
}
// Create new tab
const { tabSchema, tabUid, gridUid } = Templates.createPopupTab(validated.tabTitle, validated.collection);
// Insert new tab
await client.insertSchemaAdjacent(tabsUid, 'beforeEnd', tabSchema);
return {
success: true,
tabsUid,
newTabUid: tabUid,
gridUid,
message: `Tab "${validated.tabTitle}" added to popup. Use gridUid to add blocks.`
};
}
/**
* Get popup schema for a View/Edit action in a table
* Helps find the popup UIDs for further configuration
*/
export async function getPopupInfo(client: NocoBaseClient, input: { actionSchemaUid: string }) {
const schemaRes = await client.getSchema(input.actionSchemaUid, true);
const schema = schemaRes.data?.data;
if (!schema) {
throw new Error(`Schema ${input.actionSchemaUid} not found`);
}
const result: any = {
actionUid: input.actionSchemaUid,
actionType: schema['x-action'],
popup: null,
tabs: []
};
// Find drawer/popup
function findPopup(node: any): void {
if (!node || typeof node !== 'object') return;
if (node['x-component'] === 'Action.Container' ||
node['x-component'] === 'Action.Drawer' ||
node['x-component'] === 'Action.Modal') {
result.popup = {
uid: node['x-uid'],
component: node['x-component']
};
}
if (node['x-component'] === 'Tabs.TabPane') {
result.tabs.push({
uid: node['x-uid'],
title: node.title,
gridUid: null
});
// Find grid in tab
if (node.properties) {
for (const key in node.properties) {
if (node.properties[key]['x-component'] === 'Grid') {
result.tabs[result.tabs.length - 1].gridUid = node.properties[key]['x-uid'];
}
}
}
}
if (node.properties) {
for (const key in node.properties) {
findPopup(node.properties[key]);
}
}
}
findPopup(schema);
return result;
}
/**
* Add a block to a popup tab grid
*/
export async function addBlockToPopup(client: NocoBaseClient, input: any) {
const validated = ConfigurePopupBlockSchema.parse(input);
let blockSchema: any;
const options = validated.options || {};
switch (validated.blockType) {
case 'details':
// Details block for viewing record
blockSchema = {
type: 'void',
'x-acl-action': `${validated.collection}:get`,
'x-decorator': 'DetailsBlockProvider',
'x-use-decorator-props': 'useDetailsDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: validated.collection,
readPretty: true,
action: 'get',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:details',
'x-component': 'CardItem',
'x-uid': generateUid(),
properties: {
details: {
type: 'void',
'x-component': 'Details',
'x-read-pretty': true,
'x-use-component-props': 'useDetailsProps',
'x-uid': generateUid(),
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'details:configureFields',
'x-uid': generateUid(),
properties: {}
}
}
}
}
};
break;
case 'form':
// Edit form for updating record
const isCreate = options.action === 'create';
blockSchema = {
type: 'void',
'x-acl-action': `${validated.collection}:update`,
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': isCreate ? 'useCreateFormBlockDecoratorProps' : 'useEditFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: validated.collection,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': isCreate ? 'blockSettings:createForm' : 'blockSettings:editForm',
'x-component': 'CardItem',
'x-uid': generateUid(),
properties: {
form: {
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': isCreate ? 'useCreateFormBlockProps' : 'useEditFormBlockProps',
'x-uid': generateUid(),
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'form:configureFields',
'x-uid': generateUid(),
properties: {}
},
actions: {
type: 'void',
'x-initializer': isCreate ? 'createForm:configureActions' : 'editForm:configureActions',
'x-component': 'ActionBar',
'x-component-props': { layout: 'one-column' },
'x-uid': generateUid(),
properties: {}
}
}
}
}
};
break;
case 'related':
// Related records table
blockSchema = Templates.createRelatedRecordsBlock(
validated.collection,
options.associationField || validated.collection
);
break;
default:
throw new Error(`Block type ${validated.blockType} not supported in popup`);
}
// Wrap in Grid.Row > Grid.Col
const rowUid = generateUid();
const colUid = generateUid();
const wrappedBlock = {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': rowUid,
properties: {
[colUid]: {
type: 'void',
'x-component': 'Grid.Col',
'x-uid': colUid,
properties: {
[blockSchema['x-uid']]: blockSchema
}
}
}
};
// Insert into popup grid
await client.insertSchemaAdjacent(validated.popupTabGridUid, 'beforeEnd', Templates.enrichedSchema(wrappedBlock));
return {
success: true,
gridUid: validated.popupTabGridUid,
blockUid: blockSchema['x-uid'],
blockType: validated.blockType,
message: `Added ${validated.blockType} block to popup`
};
}
// =============================================================================
// POPUP CONFIGURATION TOOLS (View/Edit popups from row actions)
// =============================================================================
/**
* Configure a View popup with Details block and fields
* This automatically finds the popup grid and adds a Details block with specified fields
*/
export const ConfigureViewPopupSchema = z.object({
viewActionUid: z.string().describe('UID of the View action button'),
collection: z.string().describe('Collection name'),
fields: z.array(z.string()).describe('Fields to show in details'),
});
export async function configureViewPopup(client: NocoBaseClient, input: any) {
const validated = ConfigureViewPopupSchema.parse(input);
// Get action schema to find popup
const popupInfo = await getPopupInfo(client, { actionSchemaUid: validated.viewActionUid });
if (!popupInfo.popup) {
throw new Error('Could not find popup in View action');
}
// Find or get gridUid
let gridUid: string | null = null;
if (popupInfo.tabs.length > 0 && popupInfo.tabs[0].gridUid) {
gridUid = popupInfo.tabs[0].gridUid;
}
if (!gridUid) {
// Get full popup schema to find grid
const popupRes = await client.getSchema(popupInfo.popup.uid, true);
const popupSchema = popupRes.data?.data;
function findGrid(node: any): string | null {
if (!node || typeof node !== 'object') return null;
if (node['x-component'] === 'Grid' && node['x-initializer']) {
return node['x-uid'];
}
if (node.properties) {
for (const key in node.properties) {
const found = findGrid(node.properties[key]);
if (found) return found;
}
}
return null;
}
gridUid = findGrid(popupSchema);
}
if (!gridUid) {
throw new Error('Could not find grid in popup');
}
// Create Details block with fields
const detailsBlockUid = generateUid();
const detailsInnerUid = generateUid();
const detailsGridUid = generateUid();
// Generate field rows
const fieldRows: any = {};
validated.fields.forEach((fieldName, index) => {
const rowUid = generateUid();
const colUid = generateUid();
const fieldUid = generateUid();
fieldRows[rowUid] = {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': rowUid,
'x-index': index + 1,
properties: {
[colUid]: {
type: 'void',
'x-component': 'Grid.Col',
'x-uid': colUid,
properties: {
[fieldName]: {
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${validated.collection}.${fieldName}`,
'x-component-props': {},
'x-read-pretty': true,
'x-uid': fieldUid
}
}
}
}
};
});
const detailsBlock = {
type: 'void',
'x-acl-action': `${validated.collection}:get`,
'x-decorator': 'DetailsBlockProvider',
'x-use-decorator-props': 'useDetailsDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: validated.collection,
readPretty: true,
action: 'get',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:details',
'x-component': 'CardItem',
'x-uid': detailsBlockUid,
properties: {
[detailsInnerUid]: {
type: 'void',
'x-component': 'Details',
'x-read-pretty': true,
'x-use-component-props': 'useDetailsProps',
'x-uid': detailsInnerUid,
properties: {
[detailsGridUid]: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'details:configureFields',
'x-uid': detailsGridUid,
properties: fieldRows
}
}
}
}
};
// Wrap in Grid.Row > Grid.Col
const rowUid = generateUid();
const colUid = generateUid();
const wrappedBlock = {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': rowUid,
properties: {
[colUid]: {
type: 'void',
'x-component': 'Grid.Col',
'x-uid': colUid,
properties: {
[detailsBlockUid]: detailsBlock
}
}
}
};
await client.insertSchemaAdjacent(gridUid, 'beforeEnd', Templates.enrichedSchema(wrappedBlock));
return {
success: true,
popupUid: popupInfo.popup.uid,
gridUid,
detailsBlockUid,
detailsGridUid,
fieldsAdded: validated.fields.length,
message: `View popup configured with ${validated.fields.length} fields`
};
}
/**
* Configure an Edit popup with Form block, fields, and Submit action
*/
export const ConfigureEditPopupSchema = z.object({
editActionUid: z.string().describe('UID of the Edit action button'),
collection: z.string().describe('Collection name'),
fields: z.array(z.string()).describe('Fields to show in form'),
addSubmitButton: z.boolean().optional().default(true).describe('Add Submit button'),
addCancelButton: z.boolean().optional().default(false).describe('Add Cancel button'),
});
export async function configureEditPopup(client: NocoBaseClient, input: any) {
const validated = ConfigureEditPopupSchema.parse(input);
// Get action schema to find popup
const popupInfo = await getPopupInfo(client, { actionSchemaUid: validated.editActionUid });
if (!popupInfo.popup) {
throw new Error('Could not find popup in Edit action');
}
// Find grid in popup
let gridUid: string | null = null;
if (popupInfo.tabs.length > 0 && popupInfo.tabs[0].gridUid) {
gridUid = popupInfo.tabs[0].gridUid;
}
if (!gridUid) {
const popupRes = await client.getSchema(popupInfo.popup.uid, true);
const popupSchema = popupRes.data?.data;
function findGrid(node: any): string | null {
if (!node || typeof node !== 'object') return null;
if (node['x-component'] === 'Grid' && node['x-initializer']) {
return node['x-uid'];
}
if (node.properties) {
for (const key in node.properties) {
const found = findGrid(node.properties[key]);
if (found) return found;
}
}
return null;
}
gridUid = findGrid(popupSchema);
}
if (!gridUid) {
throw new Error('Could not find grid in popup');
}
// Create Form block with fields
const formBlockUid = generateUid();
const formInnerUid = generateUid();
const formGridUid = generateUid();
const actionBarUid = generateUid();
// Generate field rows
const fieldRows: any = {};
validated.fields.forEach((fieldName, index) => {
const rowUid = generateUid();
const colUid = generateUid();
const fieldUid = generateUid();
fieldRows[rowUid] = {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': rowUid,
'x-index': index + 1,
properties: {
[colUid]: {
type: 'void',
'x-component': 'Grid.Col',
'x-uid': colUid,
properties: {
[fieldName]: {
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${validated.collection}.${fieldName}`,
'x-component-props': {},
'x-uid': fieldUid
}
}
}
}
};
});
// Build action buttons
const actionSchemas: any = {};
if (validated.addSubmitButton) {
const submitUid = generateUid();
actionSchemas[submitUid] = {
type: 'void',
title: '{{ t("Submit") }}',
'x-action': 'submit',
'x-component': 'Action',
'x-use-component-props': 'useUpdateActionProps',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:updateSubmit',
'x-component-props': {
type: 'primary',
htmlType: 'submit'
},
'x-action-settings': {
triggerWorkflows: []
},
'x-uid': submitUid
};
}
if (validated.addCancelButton) {
const cancelUid = generateUid();
actionSchemas[cancelUid] = {
type: 'void',
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-use-component-props': 'useCloseActionProps',
'x-uid': cancelUid
};
}
const formBlock = {
type: 'void',
'x-acl-action': `${validated.collection}:update`,
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useEditFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: validated.collection,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:editForm',
'x-component': 'CardItem',
'x-uid': formBlockUid,
properties: {
[formInnerUid]: {
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useEditFormBlockProps',
'x-uid': formInnerUid,
properties: {
[formGridUid]: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'form:configureFields',
'x-uid': formGridUid,
properties: fieldRows
},
[actionBarUid]: {
type: 'void',
'x-initializer': 'editForm:configureActions',
'x-component': 'ActionBar',
'x-component-props': { layout: 'one-column' },
'x-uid': actionBarUid,
properties: actionSchemas
}
}
}
}
};
// Wrap in Grid.Row > Grid.Col
const rowUid = generateUid();
const colUid = generateUid();
const wrappedBlock = {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': rowUid,
properties: {
[colUid]: {
type: 'void',
'x-component': 'Grid.Col',
'x-uid': colUid,
properties: {
[formBlockUid]: formBlock
}
}
}
};
await client.insertSchemaAdjacent(gridUid, 'beforeEnd', Templates.enrichedSchema(wrappedBlock));
return {
success: true,
popupUid: popupInfo.popup.uid,
gridUid,
formBlockUid,
formGridUid,
actionBarUid,
fieldsAdded: validated.fields.length,
hasSubmit: validated.addSubmitButton,
hasCancel: validated.addCancelButton,
message: `Edit popup configured with ${validated.fields.length} fields and Submit button`
};
}
/**
* Add Submit/Cancel buttons to an existing form in popup
*/
export const AddFormSubmitSchema = z.object({
actionBarUid: z.string().describe('UID of the ActionBar in the form'),
collection: z.string().describe('Collection name'),
buttonType: z.enum(['submit', 'cancel', 'both']).optional().default('submit'),
});
export async function addFormSubmit(client: NocoBaseClient, input: any) {
const validated = AddFormSubmitSchema.parse(input);
const buttons: any[] = [];
if (validated.buttonType === 'submit' || validated.buttonType === 'both') {
const submitUid = generateUid();
buttons.push({
type: 'void',
title: '{{ t("Submit") }}',
'x-action': 'submit',
'x-component': 'Action',
'x-use-component-props': 'useUpdateActionProps',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:updateSubmit',
'x-component-props': {
type: 'primary',
htmlType: 'submit'
},
'x-action-settings': {
triggerWorkflows: []
},
'x-uid': submitUid
});
}
if (validated.buttonType === 'cancel' || validated.buttonType === 'both') {
const cancelUid = generateUid();
buttons.push({
type: 'void',
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-use-component-props': 'useCloseActionProps',
'x-uid': cancelUid
});
}
// Insert each button
for (const button of buttons) {
await client.insertSchemaAdjacent(validated.actionBarUid, 'beforeEnd', Templates.enrichedSchema(button));
}
return {
success: true,
actionBarUid: validated.actionBarUid,
buttonsAdded: buttons.length,
message: `Added ${buttons.length} button(s) to form`
};
}
// =============================================================================
// ROUTE MANAGEMENT TOOLS
// =============================================================================
/**
* List all routes (pages)
*/
export async function listRoutes(client: NocoBaseClient, input: any) {
const validated = ListRoutesSchema.parse(input);
const response = await client.listRoutes(validated.type, {
pageSize: validated.limit,
sort: ['sort', 'createdAt'],
});
const routes = response.data?.data || [];
return {
total: routes.length,
routes: routes.map((r: any) => ({
id: r.id,
title: r.title,
icon: r.icon,
type: r.type,
schemaUid: r.schemaUid,
url: validated.type === 'mobile' ? `/mobile/${r.id}` : `/admin/${r.id}`,
}))
};
}
/**
* Delete a page
*/
export async function deletePage(client: NocoBaseClient, input: any) {
const validated = DeletePageSchema.parse(input);
// Get route first to find schema UID
const routeRes = await client.getRoute(validated.type, validated.routeId);
const route = routeRes.data?.data;
if (!route) {
throw new Error(`Route ${validated.routeId} not found`);
}
// Delete route (cascade will delete children)
await client.deleteRoute(validated.type, validated.routeId, true);
// Delete schema
if (route.schemaUid) {
try {
await client.removeSchema(route.schemaUid);
} catch (e) {
// Schema might already be deleted
}
}
return {
success: true,
deleted: {
routeId: validated.routeId,
schemaUid: route.schemaUid,
title: route.title
}
};
}
/**
* Get page schema for debugging
*/
export async function getPageSchema(client: NocoBaseClient, input: any) {
const validated = GetPageSchemaInputSchema.parse(input);
// Get route
const routeRes = await client.getRoute(validated.type, validated.routeId);
const route = routeRes.data?.data;
if (!route) {
throw new Error(`Route ${validated.routeId} not found`);
}
// Get schema
const schemaRes = await client.getSchema(route.schemaUid, true);
return {
routeId: validated.routeId,
title: route.title,
schemaUid: route.schemaUid,
schema: schemaRes.data?.data
};
}
// =============================================================================
// DEBUG & UTILITY TOOLS
// =============================================================================
/**
* Inspect schema structure
* Returns simplified tree view of components
*/
export async function inspectSchema(client: NocoBaseClient, input: any) {
const validated = InspectPageSchema.parse(input);
const schemaRes = await client.getSchema(validated.schemaUid, true);
const schema = schemaRes.data?.data;
if (!schema) {
throw new Error(`Schema ${validated.schemaUid} not found`);
}
function simplify(node: any, depth: number): any {
if (!node || typeof node !== 'object') return node;
if (depth > validated.depth) return '...';
const result: any = {
uid: node['x-uid'],
component: node['x-component'],
};
if (node['x-decorator']) result.decorator = node['x-decorator'];
if (node['x-action']) result.action = node['x-action'];
if (node['x-collection-field']) result.field = node['x-collection-field'];
if (node['x-initializer']) result.initializer = node['x-initializer'];
if (node.properties) {
result.children = {};
for (const key in node.properties) {
result.children[key] = simplify(node.properties[key], depth + 1);
}
}
return result;
}
return {
schemaUid: validated.schemaUid,
structure: simplify(schema, 0)
};
}
/**
* Get properties of a schema node
*/
export async function getProperties(client: NocoBaseClient, input: any) {
const validated = GetSchemaPropertiesSchema.parse(input);
const response = await client.getSchemaProperties(validated.schemaUid);
return response.data?.data;
}
/**
* Raw schema insert for advanced users
*/
export async function rawInsertSchema(client: NocoBaseClient, input: any) {
const validated = RawInsertSchemaInput.parse(input);
const response = await client.insertSchema(validated.schema);
return {
success: true,
data: response.data?.data
};
}
/**
* Get raw schema without simplification
*/
export async function getRawSchema(client: NocoBaseClient, input: { uid: string }) {
const response = await client.getSchema(input.uid, true);
return response.data?.data;
}
// =============================================================================
// FIELD CONFIGURATION TOOLS
// =============================================================================
/**
* Add columns to an existing table
* NOTE: tableUid must be the TableV2 component UID, NOT the CardItem block UID
*/
export async function addColumnsToTable(client: NocoBaseClient, input: any) {
const validated = AddColumnsToTableSchema.parse(input);
const addedColumns: string[] = [];
for (let i = 0; i < validated.fields.length; i++) {
const fieldName = validated.fields[i];
const colUid = generateUid();
const fieldUid = generateUid();
const columnSchema = Templates.enrichedSchema({
type: 'void',
name: colUid,
'x-decorator': 'TableV2.Column.Decorator',
'x-toolbar': 'TableColumnSchemaToolbar',
'x-settings': 'fieldSettings:TableColumn',
'x-component': 'TableV2.Column',
'x-component-props': { width: null },
'x-uid': colUid,
properties: {
[fieldName]: {
type: 'string',
name: fieldName,
'x-collection-field': `${validated.collection}.${fieldName}`,
'x-component': 'CollectionField',
'x-component-props': {},
'x-read-pretty': true,
'x-decorator': null,
'x-decorator-props': {
labelStyle: { display: 'none' }
},
'x-uid': fieldUid
}
}
});
try {
await client.insertSchemaAdjacent(validated.tableUid, 'beforeEnd', columnSchema);
addedColumns.push(fieldName);
} catch (e: any) {
console.error(`Failed to add column ${fieldName}:`, e.message);
}
}
return {
success: true,
tableUid: validated.tableUid,
addedColumns,
message: `Added ${addedColumns.length} columns to table. NOTE: Ensure tableUid is the TableV2 component, not the CardItem block.`
};
}
/**
* Add fields to an existing form
*/
export async function addFieldsToForm(client: NocoBaseClient, input: any) {
const validated = AddFieldsToFormSchema.parse(input);
const addedFields: string[] = [];
for (const fieldName of validated.fields) {
const rowUid = generateUid();
const colUid = generateUid();
const fieldUid = generateUid();
const fieldSchema = {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': rowUid,
properties: {
[colUid]: {
type: 'void',
'x-component': 'Grid.Col',
'x-uid': colUid,
properties: {
[fieldName]: {
'x-collection-field': `${validated.collection}.${fieldName}`,
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-uid': fieldUid
}
}
}
}
};
try {
await client.insertSchemaAdjacent(validated.formGridUid, 'beforeEnd', fieldSchema);
addedFields.push(fieldName);
} catch (e: any) {
console.error(`Failed to add field ${fieldName}:`, e.message);
}
}
return {
success: true,
formGridUid: validated.formGridUid,
addedFields,
message: `Added ${addedFields.length} fields to form`
};
}
// =============================================================================
// PAGE EDITING TOOLS
// =============================================================================
export const UpdatePageSchema = z.object({
routeId: z.string().describe('Route ID to update'),
type: z.enum(['desktop', 'mobile']).optional().default('desktop'),
title: z.string().optional().describe('New page title'),
icon: z.string().optional().describe('New icon name'),
});
export const AddBlockToPageSchema = z.object({
pageSchemaUid: z.string().describe('Page schema UID'),
blockType: z.enum(['table', 'kanban', 'calendar', 'form', 'details']).describe('Type of block to add'),
collection: z.string().describe('Collection name'),
options: z.any().optional().describe('Block-specific options'),
});
export const RemoveBlockSchema = z.object({
blockUid: z.string().describe('UID of the block to remove'),
});
export const UpdateSchemaNodeSchema = z.object({
uid: z.string().describe('Schema node UID'),
updates: z.any().describe('Properties to update (will be merged)'),
});
export const DuplicatePageSchema = z.object({
routeId: z.string().describe('Source route ID'),
type: z.enum(['desktop', 'mobile']).optional().default('desktop'),
newTitle: z.string().describe('Title for the duplicated page'),
});
export const FindComponentSchema = z.object({
pageSchemaUid: z.string().describe('Page schema UID to search in'),
componentType: z.string().describe('Component type to find (e.g., TableV2, FormV2, Kanban)'),
});
/**
* Update page properties (title, icon)
*/
export async function updatePage(client: NocoBaseClient, input: any) {
const validated = UpdatePageSchema.parse(input);
const updates: any = {};
if (validated.title) updates.title = validated.title;
if (validated.icon) updates.icon = validated.icon;
if (Object.keys(updates).length === 0) {
throw new Error('No updates provided. Specify title or icon.');
}
const response = await client.updateRoute(validated.type, validated.routeId, updates);
return {
success: true,
routeId: validated.routeId,
updated: updates,
message: `Page updated successfully`
};
}
/**
* Add a new block to an existing page
*/
export async function addBlockToPage(client: NocoBaseClient, input: any) {
const validated = AddBlockToPageSchema.parse(input);
// Get page schema to find the grid
const schemaRes = await client.getSchema(validated.pageSchemaUid, true);
const pageSchema = schemaRes.data?.data;
if (!pageSchema) {
throw new Error(`Page schema ${validated.pageSchemaUid} not found`);
}
// Find the main Grid in the page
let gridUid: string | null = null;
function findGrid(node: any): void {
if (!node || typeof node !== 'object') return;
if (node['x-component'] === 'Grid' && node['x-initializer']?.includes('addBlock')) {
gridUid = node['x-uid'];
return;
}
if (node.properties) {
for (const key in node.properties) {
findGrid(node.properties[key]);
if (gridUid) return;
}
}
}
findGrid(pageSchema);
if (!gridUid) {
throw new Error('Could not find Grid in page to add block to');
}
// Create block based on type
let blockSchema: any;
const options = validated.options || {};
switch (validated.blockType) {
case 'table':
blockSchema = Templates.createCRMTableTemplate(validated.collection, options);
break;
case 'kanban':
blockSchema = Templates.createCRMKanbanTemplate(validated.collection, options);
break;
case 'calendar':
blockSchema = Templates.createCRMCalendarTemplate(validated.collection, options);
break;
default:
throw new Error(`Block type ${validated.blockType} not yet supported for adding`);
}
// Wrap in Grid.Row > Grid.Col
const rowUid = generateUid();
const colUid = generateUid();
const wrappedBlock = {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': rowUid,
properties: {
[colUid]: {
type: 'void',
'x-component': 'Grid.Col',
'x-uid': colUid,
properties: {
[blockSchema['x-uid']]: blockSchema
}
}
}
};
// Insert into grid
await client.insertSchemaAdjacent(gridUid, 'beforeEnd', Templates.enrichedSchema(wrappedBlock));
return {
success: true,
gridUid,
blockUid: blockSchema['x-uid'],
blockType: validated.blockType,
message: `Added ${validated.blockType} block for ${validated.collection}`
};
}
/**
* Remove a block from a page
*/
export async function removeBlock(client: NocoBaseClient, input: any) {
const validated = RemoveBlockSchema.parse(input);
await client.removeSchema(validated.blockUid);
return {
success: true,
removedUid: validated.blockUid,
message: 'Block removed successfully'
};
}
/**
* Update properties of a schema node
*/
export async function updateSchemaNode(client: NocoBaseClient, input: any) {
const validated = UpdateSchemaNodeSchema.parse(input);
// NocoBase uses POST to uiSchemas:patch
const response = await client.patchSchema({
'x-uid': validated.uid,
...validated.updates
});
return {
success: true,
uid: validated.uid,
updated: validated.updates,
message: 'Schema node updated'
};
}
/**
* Duplicate an existing page
*/
export async function duplicatePage(client: NocoBaseClient, input: any) {
const validated = DuplicatePageSchema.parse(input);
// Get source route
const routeRes = await client.getRoute(validated.type, validated.routeId);
const sourceRoute = routeRes.data?.data;
if (!sourceRoute) {
throw new Error(`Route ${validated.routeId} not found`);
}
// Get source schema
const schemaRes = await client.getSchema(sourceRoute.schemaUid, true);
const sourceSchema = schemaRes.data?.data;
if (!sourceSchema) {
throw new Error('Source page schema not found');
}
// Deep clone and regenerate UIDs
function cloneWithNewUids(node: any): any {
if (!node || typeof node !== 'object') return node;
if (Array.isArray(node)) return node.map(cloneWithNewUids);
const cloned: any = {};
for (const key in node) {
if (key === 'x-uid') {
cloned[key] = generateUid();
} else if (key === 'properties') {
cloned[key] = {};
for (const propKey in node[key]) {
const newKey = propKey.match(/^[a-z0-9]+$/i) ? generateUid() : propKey;
cloned[key][newKey] = cloneWithNewUids(node[key][propKey]);
}
} else {
cloned[key] = cloneWithNewUids(node[key]);
}
}
return cloned;
}
const newSchema = cloneWithNewUids(sourceSchema);
const newPageUid = newSchema['x-uid'];
// Find tab UID for route children
let tabUid = '';
let tabName = '';
if (newSchema.properties) {
for (const key in newSchema.properties) {
const prop = newSchema.properties[key];
if (prop['x-component'] === 'Grid') {
tabUid = prop['x-uid'];
tabName = key;
break;
}
}
}
// Insert new schema
await client.insertSchema(newSchema);
// Create new route
const newRoutePayload = {
type: 'page',
title: validated.newTitle,
icon: sourceRoute.icon,
schemaUid: newPageUid,
menuSchemaUid: generateUid(),
enableTabs: false,
children: tabUid ? [{
type: 'tabs',
schemaUid: tabUid,
tabSchemaName: tabName,
hidden: true
}] : []
};
const newRouteRes = await client.createRoute(validated.type, newRoutePayload);
const newRouteId = newRouteRes.data?.data?.id;
return {
success: true,
sourceRouteId: validated.routeId,
newRouteId,
newSchemaUid: newPageUid,
url: `/admin/${newRouteId}`,
message: `Page duplicated as "${validated.newTitle}"`
};
}
/**
* Save a schema as template
*/
export async function saveAsTemplate(client: NocoBaseClient, input: any) {
const validated = PageSaveAsTemplateSchema.parse(input);
const response = await client.saveAsTemplate(validated.schemaUid, {
name: validated.name,
collectionName: validated.collectionName,
componentName: validated.componentName,
});
return {
success: true,
template: response.data?.data,
message: `Schema ${validated.schemaUid} saved as template "${validated.name}"`
};
}
/**
* Find a component in a page schema
* Returns the UID and path to the component
*/
export async function findComponentInSchema(client: NocoBaseClient, input: any) {
const validated = FindComponentSchema.parse(input);
const schemaRes = await client.getSchema(validated.pageSchemaUid, true);
const schema = schemaRes.data?.data;
if (!schema) {
throw new Error(`Schema ${validated.pageSchemaUid} not found`);
}
const results: Array<{ uid: string; path: string; decorator?: string }> = [];
function search(node: any, path: string): void {
if (!node || typeof node !== 'object') return;
if (node['x-component'] === validated.componentType) {
results.push({
uid: node['x-uid'],
path,
decorator: node['x-decorator']
});
}
if (node.properties) {
for (const key in node.properties) {
search(node.properties[key], `${path}.properties.${key}`);
}
}
}
search(schema, 'root');
return {
componentType: validated.componentType,
found: results.length,
components: results
};
}
// =============================================================================
// LEGACY COMPATIBILITY (Keep old function names working)
// =============================================================================
export const createCollectionPage = createCRMTablePage;
export const createPageWithRoute = createCRMTablePage;
export const configureTableColumns = addColumnsToTable;
export const configureFormFields = addFieldsToForm;
export const inspectPageStructure = inspectSchema;
// Deprecated - kept for backwards compatibility
export async function addBlock(client: NocoBaseClient, input: any) {
console.error('[addBlock] Deprecated. Use addBlockToPage instead.');
return addBlockToPage(client, input);
}
export async function configureKanbanCard(client: NocoBaseClient, input: any) {
console.error('[configureKanbanCard] Deprecated. Use createCRMKanbanPage with cardFields option.');
throw new Error('configureKanbanCard is deprecated. Use createCRMKanbanPage with cardFields option.');
}