-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Column pinned/summary/link/action — 列级高级特性实现 #782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e7be682
36db280
a440f1d
20e8bbe
324bc12
85be0c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
||
|
|
||
| const generateColumns = useCallback(() => { | ||
| // Map field type to column header icon (Airtable-style) | ||
| const getTypeIcon = (fieldType: string | null): React.ReactNode => { | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
|
@@ -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({ | ||
|
|
@@ -522,8 +532,8 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({ | |
| }); | ||
| }} | ||
| > | ||
| {displayContent} | ||
| </button> | ||
| {formatActionLabel(col.action!)} | ||
| </Button> | ||
|
Comment on lines
522
to
+536
|
||
| ); | ||
| }; | ||
| } else if (CellRenderer) { | ||
|
|
@@ -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 }), | ||
| }; | ||
| }); | ||
| } | ||
|
|
@@ -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) { | ||
|
|
@@ -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, | ||
|
|
@@ -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, | ||
|
|
@@ -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"> | ||
|
|
@@ -1215,7 +1268,10 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({ | |
| ))} | ||
| </div> | ||
| ) : ( | ||
| <SchemaRenderer schema={dataTableSchema} /> | ||
| <> | ||
| <SchemaRenderer schema={dataTableSchema} /> | ||
| {summaryFooter} | ||
| </> | ||
| ); | ||
|
Comment on lines
1253
to
1275
|
||
|
|
||
| // For split mode, wrap the grid in the ResizablePanelGroup | ||
|
|
||
There was a problem hiding this comment.
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"