Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,8 +688,8 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
- [x] `quickFilters` structure reconciliation: Auto-normalizes spec `{ field, operator, value }` format into ObjectUI `{ id, label, filters[] }` format. Both formats supported simultaneously.
- [x] `conditionalFormatting` expression reconciliation: Supports spec `{ condition, style }` format alongside ObjectUI field/operator/value rules. `condition` is treated as alias for `expression`, `style` object merged into CSS properties.
- [x] `exportOptions` schema reconciliation: Accepts both spec `string[]` format (e.g., `['csv', 'xlsx']`) and ObjectUI object format `{ formats, maxRecords, includeHeaders, fileNamePrefix }`.
- [x] Column `pinned`: `pinned` property added to ListViewSchema column type. Bridge passes through to ObjectGrid which supports `frozenColumns`.
- [x] Column `summary`: `summary` property added to ListViewSchema column type. Bridge passes through for aggregation rendering.
- [x] Column `pinned`: `pinned` property added to ListViewSchema column type. Bridge passes through to ObjectGrid which supports `frozenColumns`. ObjectGrid reorders columns (left-pinned first, right-pinned last with sticky CSS). Zod schema updated with `pinned` field. `useColumnSummary` hook created.
- [x] Column `summary`: `summary` property added to ListViewSchema column type. Bridge passes through for aggregation rendering. ObjectGrid renders summary footer with count/sum/avg/min/max aggregations via `useColumnSummary` hook. Zod schema updated with `summary` field.
Comment on lines +691 to +692
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title contains Chinese text "列级高级特性实现" which violates the English-only codebase requirement. All user-facing text, titles, documentation, and comments must be written in English to ensure global accessibility and consistency. Please update the PR title to use only English text, for example: "feat: Column pinned/summary/link/action — Column-Level Advanced Features Implementation"

Copilot uses AI. Check for mistakes.
- [x] Column `link`: ObjectGrid renders click-to-navigate buttons on link-type columns with `navigation.handleClick`. Primary field auto-linked.
- [x] Column `action`: ObjectGrid renders action dispatch buttons via `executeAction` on action-type columns.

Expand Down
84 changes: 70 additions & 14 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { Edit, Trash2, MoreVertical, ChevronRight, ChevronDown, Download, Rows2,
import { useRowColor } from './useRowColor';
import { useGroupedData } from './useGroupedData';
import { GroupRow } from './GroupRow';
import { useColumnSummary } from './useColumnSummary';

export interface ObjectGridProps {
schema: ObjectGridSchema;
Expand Down Expand Up @@ -352,6 +353,16 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
// --- Grouping support ---
const { groups, isGrouped, toggleGroup } = useGroupedData(schema.grouping, data);

// --- Column summary support ---
const summaryColumns = React.useMemo(() => {
const cols = normalizeColumns(schema.columns);
if (cols && cols.length > 0 && typeof cols[0] === 'object') {
return cols as ListColumn[];
}
return undefined;
}, [schema.columns]);
const { summaries, hasSummary } = useColumnSummary(summaryColumns, data);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation passes the full data array to useColumnSummary on line 364, which means summaries are computed over all data rows, not just the currently visible/filtered rows. If the grid has pagination or filtering enabled, the summary values will include data from all pages/hidden rows, which may be misleading to users who expect to see summaries only for the visible data. Consider passing the filtered/paginated data subset instead, or document this behavior clearly in the component documentation.

Copilot uses AI. Check for mistakes.

const generateColumns = useCallback(() => {
// Map field type to column header icon (Airtable-style)
const getTypeIcon = (fieldType: string | null): React.ReactNode => {
Expand Down Expand Up @@ -474,7 +485,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
<button
type="button"
className="text-primary font-medium underline-offset-4 hover:underline cursor-pointer bg-transparent border-none p-0 text-left font-inherit"
data-testid={isPrimaryField ? 'primary-field-link' : undefined}
data-testid={isPrimaryField ? 'primary-field-link' : 'link-cell'}
onClick={(e) => {
e.stopPropagation();
navigation.handleClick(row);
Expand All @@ -494,7 +505,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
<button
type="button"
className="text-primary font-medium underline-offset-4 hover:underline cursor-pointer bg-transparent border-none p-0 text-left font-inherit"
data-testid={isPrimaryField ? 'primary-field-link' : undefined}
data-testid={isPrimaryField ? 'primary-field-link' : 'link-cell'}
onClick={(e) => {
e.stopPropagation();
navigation.handleClick(row);
Expand All @@ -505,15 +516,14 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
);
};
} else if (col.action) {
// Action column: clicking executes the registered action
// Action column: render as action button
cellRenderer = (value: any, row: any) => {
const displayContent = CellRenderer
? <CellRenderer value={value} field={{ name: col.field, type: inferredType || 'text' } as any} />
: (value != null && value !== '' ? String(value) : <span className="text-muted-foreground/50 text-xs italic">—</span>);
return (
<button
type="button"
className="text-primary underline-offset-4 hover:underline cursor-pointer bg-transparent border-none p-0 text-left font-inherit"
<Button
variant="outline"
size="sm"
className="h-7 text-xs"
data-testid="action-cell"
onClick={(e) => {
e.stopPropagation();
executeAction({
Expand All @@ -522,8 +532,8 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
});
}}
>
{displayContent}
</button>
{formatActionLabel(col.action!)}
</Button>
Comment on lines 522 to +536
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action button on line 522-536 lacks an accessible label for screen readers. While the visible text shows the formatted action label, there's no aria-label or similar attribute to indicate the button's purpose in the context of the row. Consider adding an aria-label that includes context, for example: aria-label={${formatActionLabel(col.action!)} for ${row.name || row._id}} to provide screen reader users with more information about which row the action will be performed on.

Copilot uses AI. Check for mistakes.
);
};
} else if (CellRenderer) {
Expand Down Expand Up @@ -580,6 +590,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
...(col.resizable !== undefined && { resizable: col.resizable }),
...(col.wrap !== undefined && { wrap: col.wrap }),
...(cellRenderer && { cell: cellRenderer }),
...(col.pinned && { pinned: col.pinned }),
};
});
}
Expand Down Expand Up @@ -781,6 +792,30 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
},
] : persistedColumns;

// --- Pinned column reordering ---
// Reorder: pinned:'left' first, unpinned middle, pinned:'right' last
const pinnedLeftCols = columnsWithActions.filter((c: any) => c.pinned === 'left');
const pinnedRightCols = columnsWithActions.filter((c: any) => c.pinned === 'right');
const unpinnedCols = columnsWithActions.filter((c: any) => !c.pinned);
const hasPinnedColumns = pinnedLeftCols.length > 0 || pinnedRightCols.length > 0;
const rightPinnedClasses = 'sticky right-0 z-10 bg-background border-l border-border';
const orderedColumns = hasPinnedColumns
? [
...pinnedLeftCols,
...unpinnedCols,
...pinnedRightCols.map((col: any) => ({
...col,
className: [col.className, rightPinnedClasses].filter(Boolean).join(' '),
cellClassName: [col.cellClassName, rightPinnedClasses].filter(Boolean).join(' '),
})),
]
: columnsWithActions;

// Calculate frozenColumns: if pinned columns exist, use left-pinned count; otherwise use schema default
const effectiveFrozenColumns = hasPinnedColumns
? pinnedLeftCols.length
: (schema.frozenColumns ?? 1);

// Determine selection mode (support both new and legacy formats)
let selectionMode: 'none' | 'single' | 'multiple' | boolean = false;
if (schema.selection?.type) {
Expand All @@ -807,7 +842,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
const dataTableSchema: any = {
type: 'data-table',
caption: schema.label || schema.title,
columns: columnsWithActions,
columns: orderedColumns,
data,
pagination: paginationEnabled,
pageSize: pageSize,
Expand All @@ -833,7 +868,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
showAddRow: !!operations?.create,
onAddRecord: onAddRecord,
rowClassName: schema.rowColor ? (row: any, _idx: number) => getRowClassName(row) : undefined,
frozenColumns: schema.frozenColumns ?? 1,
frozenColumns: effectiveFrozenColumns,
onSelectionChange: onRowSelect,
onRowClick: navigation.handleClick,
onCellChange: onCellChange,
Expand Down Expand Up @@ -1197,6 +1232,24 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
);
};

// Summary footer row
const summaryFooter = hasSummary ? (
<div className="border-t bg-muted/30 px-2 py-1.5" data-testid="column-summary-footer">
<div className="flex gap-4 text-xs text-muted-foreground font-medium">
{orderedColumns
.filter((col: any) => summaries.has(col.accessorKey))
.map((col: any) => {
const summary = summaries.get(col.accessorKey)!;
return (
<span key={col.accessorKey} data-testid={`summary-${col.accessorKey}`}>
{col.header}: {summary.label}
</span>
);
})}
</div>
</div>
) : null;

// Render grid content: grouped (multiple tables with headers) or flat (single table)
const gridContent = isGrouped ? (
<div className="space-y-2">
Expand All @@ -1215,7 +1268,10 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
))}
</div>
) : (
<SchemaRenderer schema={dataTableSchema} />
<>
<SchemaRenderer schema={dataTableSchema} />
{summaryFooter}
</>
);
Comment on lines 1253 to 1275
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The summary footer is only rendered when the grid is not grouped (line 1270-1275). When grouping is enabled, the summary aggregations are not displayed at all. This creates an inconsistent user experience where summary configurations on columns are silently ignored when grouping is active. Consider either: (1) rendering the summary footer after all groups in the grouped view, or (2) documenting this limitation in the PR description and adding a comment in the code explaining why summaries are not shown in grouped mode.

Copilot uses AI. Check for mistakes.

// For split mode, wrap the grid in the ResizablePanelGroup
Expand Down
Loading