feat(ai): add_data_table_row_v0 — desktop tabular row (79th tool)
This commit is contained in:
parent
4442d8fe26
commit
d24b587b6c
|
|
@ -49,6 +49,7 @@ import {
|
|||
buildCookieBanner,
|
||||
buildSidebarNav,
|
||||
buildAvatarGroup,
|
||||
buildDataTableRow,
|
||||
buildKbd,
|
||||
buildLink,
|
||||
buildListRow,
|
||||
|
|
@ -221,6 +222,7 @@ const SERVER_BUILDERS: Record<string, BuilderFn> = {
|
|||
add_cookie_banner_v0: (a) => buildCookieBanner(a as Parameters<typeof buildCookieBanner>[0]),
|
||||
add_sidebar_nav_v0: (a) => buildSidebarNav(a as Parameters<typeof buildSidebarNav>[0]),
|
||||
add_avatar_group_v0: (a) => buildAvatarGroup(a as Parameters<typeof buildAvatarGroup>[0]),
|
||||
add_data_table_row_v0: (a) => buildDataTableRow(a as Parameters<typeof buildDataTableRow>[0]),
|
||||
};
|
||||
|
||||
interface ExecToolBody {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
buildCookieBanner,
|
||||
buildSidebarNav,
|
||||
buildAvatarGroup,
|
||||
buildDataTableRow,
|
||||
buildDatePicker,
|
||||
buildDivider,
|
||||
buildEmptyChart,
|
||||
|
|
@ -558,6 +559,19 @@ const CASES: BuilderCase[] = [
|
|||
},
|
||||
build: (a) => buildAvatarGroup(a as unknown as Parameters<typeof buildAvatarGroup>[0]),
|
||||
},
|
||||
{
|
||||
toolName: 'add_data_table_row_v0',
|
||||
args: {
|
||||
columns: [
|
||||
{ content: 'Sarah' },
|
||||
{ content: 'sarah@acme.com' },
|
||||
{ content: 'Active' },
|
||||
{ content: '$1,240' },
|
||||
],
|
||||
selected: true,
|
||||
},
|
||||
build: (a) => buildDataTableRow(a as unknown as Parameters<typeof buildDataTableRow>[0]),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ import {
|
|||
buildCookieBanner,
|
||||
buildSidebarNav,
|
||||
buildAvatarGroup,
|
||||
buildDataTableRow,
|
||||
buildKbd,
|
||||
buildLink,
|
||||
buildListRow,
|
||||
|
|
@ -146,6 +147,7 @@ import {
|
|||
type CookieBannerParams,
|
||||
type SidebarNavParams,
|
||||
type AvatarGroupParams,
|
||||
type DataTableRowParams,
|
||||
type KbdParams,
|
||||
type LinkParams,
|
||||
type ListRowParams,
|
||||
|
|
@ -344,6 +346,7 @@ export const ELEMENT_SHIMS: Record<string, ElementShim> = {
|
|||
add_cookie_banner_v0: wrap<CookieBannerParams>(buildCookieBanner),
|
||||
add_sidebar_nav_v0: wrap<SidebarNavParams>(buildSidebarNav),
|
||||
add_avatar_group_v0: wrap<AvatarGroupParams>(buildAvatarGroup),
|
||||
add_data_table_row_v0: wrap<DataTableRowParams>(buildDataTableRow),
|
||||
};
|
||||
|
||||
export function getElementShim(name: string): ElementShim | undefined {
|
||||
|
|
|
|||
|
|
@ -232,7 +232,11 @@ Presence / collaboration:
|
|||
|
||||
70. Stacked avatar group (team / online users / "+N more" tile, white-ringed circles in a tight horizontal row) → `add_avatar_group_v0`
|
||||
|
||||
71. None match → fall through to `batch_design`
|
||||
Tabular data:
|
||||
|
||||
71. Desktop data-table row (N column-aligned cells in a single horizontal row, header / body / selected variants) → `add_data_table_row_v0`
|
||||
|
||||
72. None match → fall through to `batch_design`
|
||||
|
||||
**Disambiguation**: if you need a ROW of 3 metrics that should NOT scroll (e.g. a stats strip inside a card), use `add_stat_grid_v0`, NOT `add_metric_row_v0`. The grid uses `fill_container` per cell so it never overflows; the metric row uses fixed-px cells + scroll wrapper.
|
||||
|
||||
|
|
@ -316,6 +320,7 @@ PREFER an element tool when the spec says any of:
|
|||
- "cookie banner", "cookie consent", "GDPR banner", "CCPA banner", "privacy notice", "cookie disclosure", "cookie 提示", "隐私同意条" → `add_cookie_banner_v0` (set `show_settings_link: true` for fine-grained GDPR consent UX). Caller positions sticky-bottom; the tool emits the banner card itself.
|
||||
- "sidebar", "side nav", "sidebar nav", "left rail", "dashboard nav rail", "admin sidebar", "settings sidebar", "docs sidebar", "vertical nav", "侧边栏", "侧边导航", "左侧导航" → `add_sidebar_nav_v0` (desktop persistent rail; pass `title` for a brand row above the items, mark current page with `active: true` on the item). Different from `add_bottom_nav_v0` (mobile bottom tabs) and `add_top_nav_bar_v0` (mobile top header).
|
||||
- "stacked avatars", "avatar group", "avatar stack", "team avatars", "5 contributors", "online users", "+N more", "viewers row", "presence indicator", "团队成员", "在线用户", "头像组" → `add_avatar_group_v0` (renders up to `max_visible` ringed avatar circles + a "+N" overflow tile; pen-core flex doesn't allow negative gap so the white ring + 4px gap is the affordance, not literal overlap). Different from `add_avatar_v0` which is a single tile.
|
||||
- "data table row", "table row", "table header row", "customer row", "order row", "report row", "transaction row", "users table", "数据表行", "表格行", "表头行" → `add_data_table_row_v0` (desktop pattern; pass `header: true` for the column-header row, `selected: true` to tint a hover/selected body row). For row separators stack `add_divider_v0` between rows. Different from `add_list_row_v0` which is the iOS / mobile leading-icon list cell.
|
||||
|
||||
STILL use batch_design when:
|
||||
|
||||
|
|
@ -606,6 +611,33 @@ add_avatar_group_v0({
|
|||
max_visible: 4,
|
||||
}) // renders 4 ringed circles + "+3" overflow tile
|
||||
add_avatar_group_v0({ items: [{ initial: "A" }, { initial: "B" }, { initial: "C" }], size: 24 }) // compact 3-up
|
||||
|
||||
add_data_table_row_v0({
|
||||
header: true,
|
||||
columns: [
|
||||
{ content: "Customer" },
|
||||
{ content: "Email" },
|
||||
{ content: "Status" },
|
||||
{ content: "Total" },
|
||||
],
|
||||
})
|
||||
add_data_table_row_v0({
|
||||
columns: [
|
||||
{ content: "Sarah Lee" },
|
||||
{ content: "sarah@acme.com" },
|
||||
{ content: "Active" },
|
||||
{ content: "$1,240" },
|
||||
],
|
||||
})
|
||||
add_data_table_row_v0({
|
||||
selected: true,
|
||||
columns: [
|
||||
{ content: "Alex Park" },
|
||||
{ content: "alex@acme.com" },
|
||||
{ content: "Pending" },
|
||||
{ content: "$680" },
|
||||
],
|
||||
}) // tinted hover/selected row
|
||||
```
|
||||
|
||||
## Composition pattern
|
||||
|
|
|
|||
87
packages/pen-core/src/element-builders/data-table-row.ts
Normal file
87
packages/pen-core/src/element-builders/data-table-row.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import type { ElementTree } from './helpers.js';
|
||||
|
||||
export interface DataTableRowColumn {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface DataTableRowParams {
|
||||
columns: DataTableRowColumn[];
|
||||
/** Render as the table header row (smaller, slate-500, slightly bolder). */
|
||||
header?: boolean;
|
||||
/** Mark as the currently selected/hover row (slate-50 fill). */
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop / dashboard data-table row — N evenly-fill_container cells
|
||||
* laid out horizontally with a 16px gap (no vertical dividers, matches
|
||||
* modern Linear / Stripe styling). Distinct from `add_list_row_v0`,
|
||||
* which is the iOS / mobile leading-icon + title-stack + trailing-
|
||||
* chevron pattern.
|
||||
*
|
||||
* Header rows use 12/600 slate-500 typography and a 40px row height.
|
||||
* Body rows use 14/400 slate-900 typography and a 48px row height,
|
||||
* optionally tinted slate-50 when `selected` is true.
|
||||
*
|
||||
* For row separators stack `add_divider_v0` between successive rows;
|
||||
* the tool does not emit its own bottom border.
|
||||
*/
|
||||
export function buildDataTableRow(params: DataTableRowParams): ElementTree {
|
||||
const isHeader = params.header === true;
|
||||
const isSelected = !isHeader && params.selected === true;
|
||||
const cellTextSize = isHeader ? 12 : 14;
|
||||
const cellTextWeight = isHeader ? 600 : 400;
|
||||
const cellTextColor = isHeader ? '#64748B' : '#0F172A';
|
||||
const rowHeight = isHeader ? 40 : 48;
|
||||
|
||||
const node: ElementTree = {
|
||||
type: 'frame',
|
||||
name: isHeader ? 'Data Table Header Row' : 'Data Table Row',
|
||||
role: isHeader ? 'data-table-header-row' : 'data-table-row',
|
||||
width: 'fill_container',
|
||||
height: rowHeight,
|
||||
layout: 'horizontal',
|
||||
padding: [0, 16],
|
||||
gap: 16,
|
||||
alignItems: 'center',
|
||||
children: params.columns.map((col, i) =>
|
||||
buildCell(col, i, isHeader, cellTextSize, cellTextWeight, cellTextColor),
|
||||
),
|
||||
};
|
||||
|
||||
if (isSelected) {
|
||||
node.fill = [{ type: 'solid', color: '#F8FAFC' }];
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function buildCell(
|
||||
col: DataTableRowColumn,
|
||||
index: number,
|
||||
isHeader: boolean,
|
||||
size: number,
|
||||
weight: number,
|
||||
color: string,
|
||||
): ElementTree {
|
||||
return {
|
||||
type: 'frame',
|
||||
name: `Cell ${index + 1}`,
|
||||
role: isHeader ? 'data-table-header-cell' : 'data-table-cell',
|
||||
width: 'fill_container',
|
||||
height: 'fit_content',
|
||||
layout: 'horizontal',
|
||||
alignItems: 'center',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'Cell Text',
|
||||
role: isHeader ? 'data-table-header-text' : 'data-table-cell-text',
|
||||
content: col.content,
|
||||
fontSize: size,
|
||||
fontWeight: weight,
|
||||
fill: [{ type: 'solid', color }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -19,6 +19,11 @@ export { buildDivider, type DividerOrientation, type DividerParams } from './div
|
|||
export { buildBadge, type BadgeParams } from './badge.js';
|
||||
export { buildAvatar, type AvatarParams } from './avatar.js';
|
||||
export { buildAvatarGroup, type AvatarGroupItem, type AvatarGroupParams } from './avatar-group.js';
|
||||
export {
|
||||
buildDataTableRow,
|
||||
type DataTableRowColumn,
|
||||
type DataTableRowParams,
|
||||
} from './data-table-row.js';
|
||||
export { buildIconButton, type IconButtonParams } from './icon-button.js';
|
||||
export { buildIconLabel, type IconLabelParams } from './icon-label.js';
|
||||
export { buildStatGrid, type StatGridItem, type StatGridParams } from './stat-grid.js';
|
||||
|
|
|
|||
|
|
@ -265,6 +265,7 @@ export {
|
|||
buildCookieBanner,
|
||||
buildSidebarNav,
|
||||
buildAvatarGroup,
|
||||
buildDataTableRow,
|
||||
cjkFontFamily,
|
||||
detectCjkScript,
|
||||
type ElementTree,
|
||||
|
|
@ -371,4 +372,6 @@ export {
|
|||
type SidebarNavParams,
|
||||
type AvatarGroupItem,
|
||||
type AvatarGroupParams,
|
||||
type DataTableRowColumn,
|
||||
type DataTableRowParams,
|
||||
} from './element-builders/index.js';
|
||||
|
|
|
|||
141
packages/pen-mcp/src/__tests__/add-data-table-row-v0.test.ts
Normal file
141
packages/pen-mcp/src/__tests__/add-data-table-row-v0.test.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/**
|
||||
* Unit tests for add_data_table_row_v0 — desktop tabular row.
|
||||
* Distinct surface from `add_list_row_v0` (iOS / mobile list cell).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { writeFile, unlink, readFile, mkdir } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { DESIGN_TOOL_DEFINITIONS, DESIGN_TOOL_NAMES } from '../routes/design-routes';
|
||||
import { handleAddDataTableRowV0 } from '../tools/add-data-table-row-v0';
|
||||
import { invalidateCache } from '../document-manager';
|
||||
|
||||
const TMP_DIR = join(tmpdir(), 'openpencil-add-data-table-row-v0-tests');
|
||||
const EMPTY_DOC = JSON.stringify({ version: '1.0.0', children: [] });
|
||||
|
||||
async function fresh(name: string): Promise<string> {
|
||||
const fp = join(TMP_DIR, name);
|
||||
await writeFile(fp, EMPTY_DOC, 'utf-8');
|
||||
return fp;
|
||||
}
|
||||
|
||||
async function readDoc(fp: string): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(await readFile(fp, 'utf-8'));
|
||||
}
|
||||
|
||||
function getRoot(doc: Record<string, unknown>): Record<string, unknown> {
|
||||
const pages = doc['pages'] as Array<{ children?: Record<string, unknown>[] }> | undefined;
|
||||
const pageChildren = pages?.[0]?.children;
|
||||
const topChildren = doc['children'] as Record<string, unknown>[] | undefined;
|
||||
const root = pageChildren?.[0] ?? topChildren?.[0];
|
||||
if (!root) throw new Error('expected root');
|
||||
return root;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await mkdir(TMP_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
for (const f of ['body.op', 'header.op', 'selected.op', 'parent.op']) {
|
||||
try {
|
||||
const fp = join(TMP_DIR, f);
|
||||
invalidateCache(fp);
|
||||
await unlink(fp);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
describe('add_data_table_row_v0 — registration', () => {
|
||||
it('is registered in DESIGN_TOOL_DEFINITIONS + NAMES', () => {
|
||||
expect(DESIGN_TOOL_DEFINITIONS.map((t) => t.name)).toContain('add_data_table_row_v0');
|
||||
expect(DESIGN_TOOL_NAMES.has('add_data_table_row_v0')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add_data_table_row_v0 — structure', () => {
|
||||
it('emits body row: data-table-row, height=48, padding=[0,16], gap=16, no fill, slate-900 14/400 cells', async () => {
|
||||
const fp = await fresh('body.op');
|
||||
await handleAddDataTableRowV0({
|
||||
filePath: fp,
|
||||
columns: [{ content: 'Sarah' }, { content: 'sarah@acme.com' }, { content: 'Active' }],
|
||||
});
|
||||
const row = getRoot(await readDoc(fp));
|
||||
expect(row.role).toBe('data-table-row');
|
||||
expect(row.height).toBe(48);
|
||||
expect(row.padding).toEqual([0, 16]);
|
||||
expect(row.gap).toBe(16);
|
||||
expect(row.layout).toBe('horizontal');
|
||||
expect(row.fill).toBeUndefined();
|
||||
const cells = row.children as Record<string, unknown>[];
|
||||
expect(cells.length).toBe(3);
|
||||
for (const cell of cells) {
|
||||
expect(cell.role).toBe('data-table-cell');
|
||||
expect(cell.width).toBe('fill_container');
|
||||
const text = (cell.children as Record<string, unknown>[])[0];
|
||||
expect(text.role).toBe('data-table-cell-text');
|
||||
expect(text.fontSize).toBe(14);
|
||||
expect(text.fontWeight).toBe(400);
|
||||
expect((text.fill as Array<{ color: string }>)[0].color).toBe('#0F172A');
|
||||
}
|
||||
});
|
||||
|
||||
it('emits header row: data-table-header-row, height=40, slate-500 12/600 cells', async () => {
|
||||
const fp = await fresh('header.op');
|
||||
await handleAddDataTableRowV0({
|
||||
filePath: fp,
|
||||
header: true,
|
||||
columns: [{ content: 'Customer' }, { content: 'Status' }],
|
||||
});
|
||||
const row = getRoot(await readDoc(fp));
|
||||
expect(row.role).toBe('data-table-header-row');
|
||||
expect(row.height).toBe(40);
|
||||
expect(row.fill).toBeUndefined();
|
||||
const cells = row.children as Record<string, unknown>[];
|
||||
for (const cell of cells) {
|
||||
expect(cell.role).toBe('data-table-header-cell');
|
||||
const text = (cell.children as Record<string, unknown>[])[0];
|
||||
expect(text.role).toBe('data-table-header-text');
|
||||
expect(text.fontSize).toBe(12);
|
||||
expect(text.fontWeight).toBe(600);
|
||||
expect((text.fill as Array<{ color: string }>)[0].color).toBe('#64748B');
|
||||
}
|
||||
});
|
||||
|
||||
it('selected body row is tinted slate-50; selected on header is ignored', async () => {
|
||||
const fp = await fresh('selected.op');
|
||||
await handleAddDataTableRowV0({
|
||||
filePath: fp,
|
||||
selected: true,
|
||||
columns: [{ content: 'Alex' }, { content: 'Pending' }],
|
||||
});
|
||||
const tinted = getRoot(await readDoc(fp));
|
||||
expect((tinted.fill as Array<{ color: string }>)[0].color).toBe('#F8FAFC');
|
||||
|
||||
invalidateCache(fp);
|
||||
await writeFile(fp, EMPTY_DOC, 'utf-8');
|
||||
await handleAddDataTableRowV0({
|
||||
filePath: fp,
|
||||
header: true,
|
||||
selected: true,
|
||||
columns: [{ content: 'Customer' }],
|
||||
});
|
||||
const headerRow = getRoot(await readDoc(fp));
|
||||
expect(headerRow.fill).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws on bogus parent_id AND leaves file untouched (side-effect invariant)', async () => {
|
||||
const fp = await fresh('parent.op');
|
||||
const before = await readFile(fp, 'utf-8');
|
||||
await expect(
|
||||
handleAddDataTableRowV0({
|
||||
filePath: fp,
|
||||
columns: [{ content: 'X' }],
|
||||
parent_id: 'bogus-parent',
|
||||
}),
|
||||
).rejects.toThrow(/parent_id.*not found/);
|
||||
const after = await readFile(fp, 'utf-8');
|
||||
expect(after).toBe(before);
|
||||
});
|
||||
});
|
||||
|
|
@ -87,6 +87,7 @@ const ELEMENT_TOOL_NAMES = [
|
|||
'add_cookie_banner_v0',
|
||||
'add_sidebar_nav_v0',
|
||||
'add_avatar_group_v0',
|
||||
'add_data_table_row_v0',
|
||||
];
|
||||
|
||||
describe('element tools — v0-MUST contract', () => {
|
||||
|
|
|
|||
|
|
@ -479,6 +479,52 @@ export const ELEMENT_TOOL_DEFINITIONS_EXT_2 = [
|
|||
required: ['label'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_data_table_row_v0',
|
||||
description:
|
||||
'Desktop / dashboard data-table row — N evenly-spaced cells laid out horizontally (no ' +
|
||||
'vertical dividers, modern Linear / Stripe styling). Set `header: true` for the column-' +
|
||||
'header row (smaller, bolder, slate-500). Set `selected: true` on a body row to tint it ' +
|
||||
'slate-50. For row separators stack `add_divider_v0` between successive rows. Distinct ' +
|
||||
'from add_list_row_v0 (iOS / mobile leading-icon list cell). Use for "table row", "data ' +
|
||||
'row", "customer row", "order row", "table header", "数据表行", "表格行". ' +
|
||||
'schemaVersion 1.0',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
schemaVersion: schemaVersionProp,
|
||||
filePath: filePathProp,
|
||||
columns: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Cells in column order. Each cell renders as a fill_container frame so all columns ' +
|
||||
'share remaining width evenly.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Cell text (e.g. "Sarah Lee", "$1,240", "Active").',
|
||||
},
|
||||
},
|
||||
required: ['content'],
|
||||
},
|
||||
},
|
||||
header: {
|
||||
type: 'boolean',
|
||||
description: 'Render as the table header row (12/600 slate-500, 40px tall).',
|
||||
},
|
||||
selected: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Tint a body row slate-50 to mark hover / selected state. Ignored on header.',
|
||||
},
|
||||
parent_id: parentIdProp,
|
||||
pageId: pageIdProp,
|
||||
},
|
||||
required: ['columns'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'add_avatar_group_v0',
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ import { handleAddInputWithActionV0 } from '../tools/add-input-with-action-v0';
|
|||
import { handleAddCookieBannerV0 } from '../tools/add-cookie-banner-v0';
|
||||
import { handleAddSidebarNavV0 } from '../tools/add-sidebar-nav-v0';
|
||||
import { handleAddAvatarGroupV0 } from '../tools/add-avatar-group-v0';
|
||||
import { handleAddDataTableRowV0 } from '../tools/add-data-table-row-v0';
|
||||
import { recordElementToolCall } from '../metrics/element-tool-metrics';
|
||||
import { ELEMENT_TOOL_DEFINITIONS_BASE } from './element-tool-defs-base';
|
||||
import { ELEMENT_TOOL_DEFINITIONS_EXT } from './element-tool-defs-ext';
|
||||
|
|
@ -281,6 +282,8 @@ async function dispatchElementToolCall(name: string, a: any): Promise<string> {
|
|||
return JSON.stringify(await handleAddSidebarNavV0(a), null, 2);
|
||||
case 'add_avatar_group_v0':
|
||||
return JSON.stringify(await handleAddAvatarGroupV0(a), null, 2);
|
||||
case 'add_data_table_row_v0':
|
||||
return JSON.stringify(await handleAddDataTableRowV0(a), null, 2);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
30
packages/pen-mcp/src/tools/add-data-table-row-v0.ts
Normal file
30
packages/pen-mcp/src/tools/add-data-table-row-v0.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import {
|
||||
assignIdsRecursively,
|
||||
buildDataTableRow,
|
||||
type DataTableRowParams,
|
||||
} from '@zseven-w/pen-core';
|
||||
import type { handleBatchDesign } from './batch-design';
|
||||
import { ensureParentExists, insertElementTree } from './element-tool-helpers';
|
||||
|
||||
export interface AddDataTableRowV0Params extends DataTableRowParams {
|
||||
parent_id?: string;
|
||||
filePath?: string;
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
export type { DataTableRowColumn as AddDataTableRowV0Column } from '@zseven-w/pen-core';
|
||||
|
||||
/**
|
||||
* Desktop / dashboard data-table row. Tree build delegated to
|
||||
* `@zseven-w/pen-core`'s `buildDataTableRow`. Distinct from
|
||||
* `add_list_row_v0` (mobile / iOS list cell) — use this for tabular
|
||||
* customer / order / report data with column-aligned cells.
|
||||
*/
|
||||
export async function handleAddDataTableRowV0(
|
||||
params: AddDataTableRowV0Params,
|
||||
): Promise<Awaited<ReturnType<typeof handleBatchDesign>>> {
|
||||
await ensureParentExists(params);
|
||||
const row = buildDataTableRow(params);
|
||||
assignIdsRecursively(row);
|
||||
return insertElementTree({ binding: 'dataTableRow', tree: row, ...params });
|
||||
}
|
||||
Loading…
Reference in a new issue