Skip to content

Commit ddbed06

Browse files
Copilothotlong
andcommitted
Refactor field renderers and ObjectGrid to use Shadcn components
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a0a576e commit ddbed06

2 files changed

Lines changed: 109 additions & 66 deletions

File tree

packages/views/src/ObjectGrid.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ import React, { useEffect, useState, useCallback } from 'react';
2525
import type { ObjectGridSchema, DataSource, ListColumn, ViewData } from '@object-ui/types';
2626
import { SchemaRenderer } from '@object-ui/react';
2727
import { getCellRenderer } from './field-renderers';
28+
import { Button } from '@object-ui/components';
29+
import {
30+
DropdownMenu,
31+
DropdownMenuContent,
32+
DropdownMenuItem,
33+
DropdownMenuTrigger,
34+
} from '@object-ui/components';
35+
import { Edit, Trash2, MoreVertical } from 'lucide-react';
2836

2937
export interface ObjectGridProps {
3038
schema: ObjectGridSchema;
@@ -298,18 +306,28 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
298306
header: 'Actions',
299307
accessorKey: '_actions',
300308
cell: (_value: any, row: any) => (
301-
<div className="flex gap-2">
302-
{operations?.update && onEdit && (
303-
<button onClick={() => onEdit(row)} className="text-blue-600 hover:text-blue-800 text-sm">
304-
Edit
305-
</button>
306-
)}
307-
{operations?.delete && onDelete && (
308-
<button onClick={() => onDelete(row)} className="text-red-600 hover:text-red-800 text-sm">
309-
Delete
310-
</button>
311-
)}
312-
</div>
309+
<DropdownMenu>
310+
<DropdownMenuTrigger asChild>
311+
<Button variant="ghost" size="icon" className="h-8 w-8">
312+
<MoreVertical className="h-4 w-4" />
313+
<span className="sr-only">Open menu</span>
314+
</Button>
315+
</DropdownMenuTrigger>
316+
<DropdownMenuContent align="end">
317+
{operations?.update && onEdit && (
318+
<DropdownMenuItem onClick={() => onEdit(row)}>
319+
<Edit className="mr-2 h-4 w-4" />
320+
Edit
321+
</DropdownMenuItem>
322+
)}
323+
{operations?.delete && onDelete && (
324+
<DropdownMenuItem variant="destructive" onClick={() => onDelete(row)}>
325+
<Trash2 className="mr-2 h-4 w-4" />
326+
Delete
327+
</DropdownMenuItem>
328+
)}
329+
</DropdownMenuContent>
330+
</DropdownMenu>
313331
),
314332
sortable: false,
315333
},

packages/views/src/field-renderers.tsx

Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515

1616
import React from 'react';
1717
import type { FieldMetadata, SelectOptionMetadata } from '@object-ui/types';
18+
import { Badge } from '@object-ui/components';
19+
import { Avatar, AvatarFallback } from '@object-ui/components';
20+
import { Button } from '@object-ui/components';
21+
import { Check, X } from 'lucide-react';
1822

1923
/**
2024
* Cell renderer props
@@ -133,13 +137,15 @@ export function BooleanCellRenderer({ value }: CellRendererProps): React.ReactEl
133137
return (
134138
<div className="flex items-center">
135139
{value ? (
136-
<span className="inline-flex items-center justify-center w-5 h-5 rounded bg-green-100 text-green-600">
137-
138-
</span>
140+
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 gap-1">
141+
<Check className="size-3" />
142+
True
143+
</Badge>
139144
) : (
140-
<span className="inline-flex items-center justify-center w-5 h-5 rounded bg-gray-100 text-gray-400">
141-
142-
</span>
145+
<Badge variant="outline" className="bg-gray-50 text-gray-500 border-gray-200 gap-1">
146+
<X className="size-3" />
147+
False
148+
</Badge>
143149
)}
144150
</div>
145151
);
@@ -173,17 +179,20 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R
173179

174180
if (!value) return <span>-</span>;
175181

176-
// Color mapping for Tailwind CSS (to avoid dynamic class names)
177-
const colorClasses: Record<string, { bg: string; text: string }> = {
178-
gray: { bg: 'bg-gray-100', text: 'text-gray-800' },
179-
red: { bg: 'bg-red-100', text: 'text-red-800' },
180-
orange: { bg: 'bg-orange-100', text: 'text-orange-800' },
181-
yellow: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
182-
green: { bg: 'bg-green-100', text: 'text-green-800' },
183-
blue: { bg: 'bg-blue-100', text: 'text-blue-800' },
184-
indigo: { bg: 'bg-indigo-100', text: 'text-indigo-800' },
185-
purple: { bg: 'bg-purple-100', text: 'text-purple-800' },
186-
pink: { bg: 'bg-pink-100', text: 'text-pink-800' },
182+
// Color to Tailwind class mapping for custom Badge styling
183+
const getColorClasses = (color?: string) => {
184+
const colorMap: Record<string, string> = {
185+
gray: 'bg-gray-100 text-gray-800 border-gray-300',
186+
red: 'bg-red-100 text-red-800 border-red-300',
187+
orange: 'bg-orange-100 text-orange-800 border-orange-300',
188+
yellow: 'bg-yellow-100 text-yellow-800 border-yellow-300',
189+
green: 'bg-green-100 text-green-800 border-green-300',
190+
blue: 'bg-blue-100 text-blue-800 border-blue-300',
191+
indigo: 'bg-indigo-100 text-indigo-800 border-indigo-300',
192+
purple: 'bg-purple-100 text-purple-800 border-purple-300',
193+
pink: 'bg-pink-100 text-pink-800 border-pink-300',
194+
};
195+
return colorMap[color || 'blue'] || colorMap.blue;
187196
};
188197

189198
// Handle multiple values
@@ -193,16 +202,16 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R
193202
{value.map((val, idx) => {
194203
const option = options.find(opt => opt.value === val);
195204
const label = option?.label || val;
196-
const color = option?.color || 'gray';
197-
const classes = colorClasses[color] || colorClasses.gray;
205+
const colorClasses = getColorClasses(option?.color);
198206

199207
return (
200-
<span
208+
<Badge
201209
key={idx}
202-
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${classes.bg} ${classes.text}`}
210+
variant="outline"
211+
className={colorClasses}
203212
>
204213
{label}
205-
</span>
214+
</Badge>
206215
);
207216
})}
208217
</div>
@@ -212,15 +221,15 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R
212221
// Handle single value
213222
const option = options.find(opt => opt.value === value);
214223
const label = option?.label || value;
215-
const color = option?.color || 'blue';
216-
const classes = colorClasses[color] || colorClasses.blue;
224+
const colorClasses = getColorClasses(option?.color);
217225

218226
return (
219-
<span
220-
className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${classes.bg} ${classes.text}`}
227+
<Badge
228+
variant="outline"
229+
className={colorClasses}
221230
>
222231
{label}
223-
</span>
232+
</Badge>
224233
);
225234
}
226235

@@ -231,13 +240,18 @@ export function EmailCellRenderer({ value }: CellRendererProps): React.ReactElem
231240
if (!value) return <span>-</span>;
232241

233242
return (
234-
<a
235-
href={`mailto:${value}`}
236-
className="text-blue-600 hover:text-blue-800 underline truncate"
237-
onClick={(e) => e.stopPropagation()}
243+
<Button
244+
variant="link"
245+
className="p-0 h-auto font-normal text-blue-600 hover:text-blue-800"
246+
asChild
238247
>
239-
{value}
240-
</a>
248+
<a
249+
href={`mailto:${value}`}
250+
onClick={(e) => e.stopPropagation()}
251+
>
252+
{value}
253+
</a>
254+
</Button>
241255
);
242256
}
243257

@@ -248,15 +262,20 @@ export function UrlCellRenderer({ value }: CellRendererProps): React.ReactElemen
248262
if (!value) return <span>-</span>;
249263

250264
return (
251-
<a
252-
href={value}
253-
target="_blank"
254-
rel="noopener noreferrer"
255-
className="text-blue-600 hover:text-blue-800 underline truncate"
256-
onClick={(e) => e.stopPropagation()}
265+
<Button
266+
variant="link"
267+
className="p-0 h-auto font-normal text-blue-600 hover:text-blue-800"
268+
asChild
257269
>
258-
{value}
259-
</a>
270+
<a
271+
href={value}
272+
target="_blank"
273+
rel="noopener noreferrer"
274+
onClick={(e) => e.stopPropagation()}
275+
>
276+
{value}
277+
</a>
278+
</Button>
260279
);
261280
}
262281

@@ -313,11 +332,11 @@ export function ImageCellRenderer({ value }: CellRendererProps): React.ReactElem
313332
key={idx}
314333
src={img.url || ''}
315334
alt={img.name || `Image ${idx + 1}`}
316-
className="w-8 h-8 rounded border-2 border-white object-cover"
335+
className="size-8 rounded-md border-2 border-white object-cover"
317336
/>
318337
))}
319338
{value.length > 3 && (
320-
<div className="w-8 h-8 rounded border-2 border-white bg-gray-100 flex items-center justify-center text-xs">
339+
<div className="size-8 rounded-md border-2 border-white bg-gray-100 flex items-center justify-center text-xs font-medium text-gray-600">
321340
+{value.length - 3}
322341
</div>
323342
)}
@@ -329,7 +348,7 @@ export function ImageCellRenderer({ value }: CellRendererProps): React.ReactElem
329348
<img
330349
src={value.url || ''}
331350
alt={value.name || 'Image'}
332-
className="w-10 h-10 rounded object-cover"
351+
className="size-10 rounded-md object-cover"
333352
/>
334353
);
335354
}
@@ -391,19 +410,23 @@ export function UserCellRenderer({ value }: CellRendererProps): React.ReactEleme
391410
const initials = name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2);
392411

393412
return (
394-
<div
413+
<Avatar
395414
key={idx}
396-
className="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center text-xs font-medium border-2 border-white"
415+
className="size-8 border-2 border-white"
397416
title={name}
398417
>
399-
{initials}
400-
</div>
418+
<AvatarFallback className="bg-blue-500 text-white text-xs">
419+
{initials}
420+
</AvatarFallback>
421+
</Avatar>
401422
);
402423
})}
403424
{value.length > 3 && (
404-
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-xs border-2 border-white">
405-
+{value.length - 3}
406-
</div>
425+
<Avatar className="size-8 border-2 border-white">
426+
<AvatarFallback className="bg-gray-200 text-gray-600 text-xs">
427+
+{value.length - 3}
428+
</AvatarFallback>
429+
</Avatar>
407430
)}
408431
</div>
409432
);
@@ -414,9 +437,11 @@ export function UserCellRenderer({ value }: CellRendererProps): React.ReactEleme
414437

415438
return (
416439
<div className="flex items-center gap-2">
417-
<div className="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center text-xs font-medium">
418-
{initials}
419-
</div>
440+
<Avatar className="size-8">
441+
<AvatarFallback className="bg-blue-500 text-white text-xs">
442+
{initials}
443+
</AvatarFallback>
444+
</Avatar>
420445
<span className="truncate">{name}</span>
421446
</div>
422447
);

0 commit comments

Comments
 (0)