Skip to content

Commit 78bbfd0

Browse files
authored
Merge pull request #784 from objectstack-ai/copilot/add-row-actions-and-bulk-actions
2 parents a3471e4 + 30f4f2d commit 78bbfd0

File tree

6 files changed

+616
-45
lines changed

6 files changed

+616
-45
lines changed

packages/plugin-grid/src/ObjectGrid.tsx

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ import { getCellRenderer, formatCurrency, formatCompactCurrency, formatDate, for
2828
import {
2929
Badge, Button, NavigationOverlay,
3030
Popover, PopoverContent, PopoverTrigger,
31-
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
3231
} from '@object-ui/components';
3332
import { usePullToRefresh } from '@object-ui/mobile';
34-
import { Edit, Trash2, MoreVertical, ChevronRight, ChevronDown, Download, Rows2, Rows3, Rows4, AlignJustify, Type, Hash, Calendar, CheckSquare, User, Tag, Clock } from 'lucide-react';
33+
import { ChevronRight, ChevronDown, Download, Rows2, Rows3, Rows4, AlignJustify, Type, Hash, Calendar, CheckSquare, User, Tag, Clock } from 'lucide-react';
3534
import { useRowColor } from './useRowColor';
3635
import { useGroupedData } from './useGroupedData';
3736
import { GroupRow } from './GroupRow';
3837
import { useColumnSummary } from './useColumnSummary';
38+
import { RowActionMenu, formatActionLabel } from './components/RowActionMenu';
39+
import { BulkActionBar } from './components/BulkActionBar';
3940

4041
export interface ObjectGridProps {
4142
schema: ObjectGridSchema;
@@ -52,14 +53,6 @@ export interface ObjectGridProps {
5253
onAddRecord?: () => void;
5354
}
5455

55-
/**
56-
* Format an action identifier string into a human-readable label.
57-
* e.g., 'send_email' → 'Send Email'
58-
*/
59-
function formatActionLabel(action: string): string {
60-
return action.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
61-
}
62-
6356
/**
6457
* Helper to get data configuration from schema
6558
* Handles both new ViewData format and legacy inline data
@@ -137,6 +130,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
137130
const [refreshKey, setRefreshKey] = useState(0);
138131
const [showExport, setShowExport] = useState(false);
139132
const [rowHeightMode, setRowHeightMode] = useState<'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'>(schema.rowHeight ?? 'medium');
133+
const [selectedRows, setSelectedRows] = useState<any[]>([]);
140134

141135
// Column state persistence (order and widths)
142136
const columnStorageKey = React.useMemo(() => {
@@ -756,37 +750,15 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
756750
header: 'Actions',
757751
accessorKey: '_actions',
758752
cell: (_value: any, row: any) => (
759-
<DropdownMenu>
760-
<DropdownMenuTrigger asChild>
761-
<Button variant="ghost" size="icon" className="h-8 w-8 min-h-[44px] min-w-[44px] sm:min-h-0 sm:min-w-0" data-testid="row-action-trigger">
762-
<MoreVertical className="h-4 w-4" />
763-
<span className="sr-only">Open menu</span>
764-
</Button>
765-
</DropdownMenuTrigger>
766-
<DropdownMenuContent align="end">
767-
{operations?.update && onEdit && (
768-
<DropdownMenuItem onClick={() => onEdit(row)}>
769-
<Edit className="mr-2 h-4 w-4" />
770-
Edit
771-
</DropdownMenuItem>
772-
)}
773-
{operations?.delete && onDelete && (
774-
<DropdownMenuItem onClick={() => onDelete(row)}>
775-
<Trash2 className="mr-2 h-4 w-4" />
776-
Delete
777-
</DropdownMenuItem>
778-
)}
779-
{schema.rowActions?.map(action => (
780-
<DropdownMenuItem
781-
key={action}
782-
onClick={() => executeAction({ type: action, params: { record: row } })}
783-
data-testid={`row-action-${action}`}
784-
>
785-
{formatActionLabel(action)}
786-
</DropdownMenuItem>
787-
))}
788-
</DropdownMenuContent>
789-
</DropdownMenu>
753+
<RowActionMenu
754+
row={row}
755+
rowActions={schema.rowActions}
756+
canEdit={!!(operations?.update && onEdit)}
757+
canDelete={!!(operations?.delete && onDelete)}
758+
onEdit={onEdit}
759+
onDelete={onDelete}
760+
onAction={(action, r) => executeAction({ type: action, params: { record: r } })}
761+
/>
790762
),
791763
sortable: false,
792764
},
@@ -869,7 +841,10 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
869841
onAddRecord: onAddRecord,
870842
rowClassName: schema.rowColor ? (row: any, _idx: number) => getRowClassName(row) : undefined,
871843
frozenColumns: effectiveFrozenColumns,
872-
onSelectionChange: onRowSelect,
844+
onSelectionChange: (rows: any[]) => {
845+
setSelectedRows(rows);
846+
onRowSelect?.(rows);
847+
},
873848
onRowClick: navigation.handleClick,
874849
onCellChange: onCellChange,
875850
onRowSave: onRowSave,
@@ -1250,6 +1225,9 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
12501225
</div>
12511226
) : null;
12521227

1228+
// Bulk actions — support both batchActions (ObjectUI) and bulkActions (spec) names
1229+
const effectiveBulkActions = schema.batchActions ?? (schema as any).bulkActions;
1230+
12531231
// Render grid content: grouped (multiple tables with headers) or flat (single table)
12541232
const gridContent = isGrouped ? (
12551233
<div className="space-y-2">
@@ -1280,7 +1258,18 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
12801258
<NavigationOverlay
12811259
{...navigation}
12821260
title={detailTitle}
1283-
mainContent={<>{gridToolbar}{gridContent}</>}
1261+
mainContent={
1262+
<>
1263+
{gridToolbar}
1264+
{gridContent}
1265+
<BulkActionBar
1266+
selectedRows={selectedRows}
1267+
actions={effectiveBulkActions ?? []}
1268+
onAction={(action, rows) => executeAction({ type: action, params: { records: rows } })}
1269+
onClearSelection={() => setSelectedRows([])}
1270+
/>
1271+
</>
1272+
}
12841273
>
12851274
{(record) => renderRecordDetail(record)}
12861275
</NavigationOverlay>
@@ -1299,6 +1288,12 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
12991288
)}
13001289
{gridToolbar}
13011290
{gridContent}
1291+
<BulkActionBar
1292+
selectedRows={selectedRows}
1293+
actions={effectiveBulkActions ?? []}
1294+
onAction={(action, rows) => executeAction({ type: action, params: { records: rows } })}
1295+
onClearSelection={() => setSelectedRows([])}
1296+
/>
13021297
{navigation.isOverlay && (
13031298
<NavigationOverlay
13041299
{...navigation}

0 commit comments

Comments
 (0)