Skip to content

Commit fd30bbe

Browse files
Copilothotlong
andcommitted
feat: implement P1 spec compliance features (quickFilters, hiddenFields, fieldOrder, exportOptions, densityMode, inline editing, marker clustering, combo charts, column persistence)
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 38f7996 commit fd30bbe

File tree

9 files changed

+680
-50
lines changed

9 files changed

+680
-50
lines changed

packages/plugin-charts/src/AdvancedChartImpl.tsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ const TW_COLORS: Record<string, string> = {
7070
const resolveColor = (color: string) => TW_COLORS[color] || color;
7171

7272
export interface AdvancedChartImplProps {
73-
chartType?: 'bar' | 'line' | 'area' | 'pie' | 'donut' | 'radar' | 'scatter';
73+
chartType?: 'bar' | 'line' | 'area' | 'pie' | 'donut' | 'radar' | 'scatter' | 'combo';
7474
data?: Array<Record<string, any>>;
7575
config?: ChartConfig;
7676
xAxisKey?: string;
77-
series?: Array<{ dataKey: string }>;
77+
series?: Array<{ dataKey: string; chartType?: 'bar' | 'line' | 'area' }>;
7878
className?: string;
7979
}
8080

@@ -231,13 +231,46 @@ export default function AdvancedChartImpl({
231231
);
232232
}
233233

234+
// Combo chart (mixed bar + line on same chart)
235+
if (chartType === 'combo') {
236+
return (
237+
<ChartContainer config={config} className={className}>
238+
<BarChart data={data}>
239+
<CartesianGrid vertical={false} />
240+
<XAxis
241+
dataKey={xAxisKey}
242+
tickLine={false}
243+
tickMargin={10}
244+
axisLine={false}
245+
interval={isMobile ? Math.ceil(data.length / 5) : 0}
246+
tickFormatter={(value) => (value && typeof value === 'string') ? value.slice(0, 3) : value}
247+
/>
248+
<YAxis yAxisId="left" tickLine={false} axisLine={false} />
249+
<YAxis yAxisId="right" orientation="right" tickLine={false} axisLine={false} />
250+
<ChartTooltip content={<ChartTooltipContent />} />
251+
<ChartLegend
252+
content={<ChartLegendContent />}
253+
{...(isMobile && { verticalAlign: "bottom", wrapperStyle: { fontSize: '11px', paddingTop: '8px' } })}
254+
/>
255+
{series.map((s: any, index: number) => {
256+
const color = resolveColor(config[s.dataKey]?.color || DEFAULT_CHART_COLOR);
257+
const seriesType = s.chartType || (index === 0 ? 'bar' : 'line');
258+
const yAxisId = seriesType === 'bar' ? 'left' : 'right';
259+
260+
if (seriesType === 'line') {
261+
return <Line key={s.dataKey} yAxisId={yAxisId} type="monotone" dataKey={s.dataKey} stroke={color} strokeWidth={2} dot={false} />;
262+
}
263+
if (seriesType === 'area') {
264+
return <Area key={s.dataKey} yAxisId={yAxisId} type="monotone" dataKey={s.dataKey} fill={color} stroke={color} fillOpacity={0.4} />;
265+
}
266+
return <Bar key={s.dataKey} yAxisId={yAxisId} dataKey={s.dataKey} fill={color} radius={4} />;
267+
})}
268+
</BarChart>
269+
</ChartContainer>
270+
);
271+
}
272+
234273
return (
235-
<ChartContainer config={config} className={className}>
236-
<ChartComponent data={data}>
237-
<CartesianGrid vertical={false} />
238-
<XAxis
239-
dataKey={xAxisKey}
240-
tickLine={false}
241274
tickMargin={10}
242275
axisLine={false}
243276
interval={isMobile ? Math.ceil(data.length / 5) : 0}

packages/plugin-charts/src/ChartRenderer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export interface ChartRendererProps {
4343
type: string;
4444
id?: string;
4545
className?: string;
46-
chartType?: 'bar' | 'line' | 'area';
46+
chartType?: 'bar' | 'line' | 'area' | 'pie' | 'donut' | 'radar' | 'scatter' | 'combo';
4747
data?: Array<Record<string, any>>;
4848
config?: Record<string, any>;
4949
xAxisKey?: string;

packages/plugin-detail/src/DetailSection.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,18 @@ export interface DetailSectionProps {
3131
section: DetailViewSectionType;
3232
data?: any;
3333
className?: string;
34+
/** Whether inline editing is active */
35+
isEditing?: boolean;
36+
/** Callback when a field value changes during inline editing */
37+
onFieldChange?: (field: string, value: any) => void;
3438
}
3539

3640
export const DetailSection: React.FC<DetailSectionProps> = ({
3741
section,
3842
data,
3943
className,
44+
isEditing = false,
45+
onFieldChange,
4046
}) => {
4147
const [isCollapsed, setIsCollapsed] = React.useState(section.defaultCollapsed ?? false);
4248
const [copiedField, setCopiedField] = React.useState<string | null>(null);
@@ -75,6 +81,16 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
7581
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
7682
{field.label || field.name}
7783
</div>
84+
{isEditing && !field.readonly ? (
85+
<div className="min-h-[44px] sm:min-h-0">
86+
<input
87+
type={field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'}
88+
className="w-full px-2 py-1.5 text-sm border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-ring"
89+
value={value != null ? String(value) : ''}
90+
onChange={(e) => onFieldChange?.(field.name, e.target.value)}
91+
/>
92+
</div>
93+
) : (
7894
<div
7995
className={cn(
8096
"flex items-start justify-between gap-2 min-h-[44px] sm:min-h-0 rounded-md",
@@ -120,6 +136,7 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
120136
</TooltipProvider>
121137
)}
122138
</div>
139+
)}
123140
</div>
124141
);
125142
};

packages/plugin-detail/src/DetailView.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
History,
3333
Star,
3434
StarOff,
35+
Check,
3536
} from 'lucide-react';
3637
import { DetailSection } from './DetailSection';
3738
import { DetailTabs } from './DetailTabs';
@@ -46,6 +47,10 @@ export interface DetailViewProps {
4647
onEdit?: () => void;
4748
onDelete?: () => void;
4849
onBack?: () => void;
50+
/** Enable inline editing toggle for detail fields */
51+
inlineEdit?: boolean;
52+
/** Callback when a field value is saved inline */
53+
onFieldSave?: (field: string, value: any, record: any) => void | Promise<void>;
4954
}
5055

5156
export const DetailView: React.FC<DetailViewProps> = ({
@@ -55,10 +60,14 @@ export const DetailView: React.FC<DetailViewProps> = ({
5560
onEdit,
5661
onDelete,
5762
onBack,
63+
inlineEdit = false,
64+
onFieldSave,
5865
}) => {
5966
const [data, setData] = React.useState<any>(schema.data);
6067
const [loading, setLoading] = React.useState(!schema.data && !!((schema.api && schema.resourceId) || (dataSource && schema.objectName && schema.resourceId)));
6168
const [isFavorite, setIsFavorite] = React.useState(false);
69+
const [isInlineEditing, setIsInlineEditing] = React.useState(false);
70+
const [editedValues, setEditedValues] = React.useState<Record<string, any>>({});
6271

6372
// Fetch data if API or DataSource provided
6473
React.useEffect(() => {
@@ -168,6 +177,26 @@ export const DetailView: React.FC<DetailViewProps> = ({
168177
setIsFavorite(!isFavorite);
169178
}, [isFavorite]);
170179

180+
const handleInlineEditToggle = React.useCallback(() => {
181+
if (isInlineEditing) {
182+
// Save changes
183+
const changes = Object.entries(editedValues);
184+
if (changes.length > 0) {
185+
const updatedData = { ...data, ...editedValues };
186+
setData(updatedData);
187+
changes.forEach(([field, value]) => {
188+
onFieldSave?.(field, value, updatedData);
189+
});
190+
}
191+
setEditedValues({});
192+
}
193+
setIsInlineEditing(!isInlineEditing);
194+
}, [isInlineEditing, editedValues, data, onFieldSave]);
195+
196+
const handleInlineFieldChange = React.useCallback((field: string, value: any) => {
197+
setEditedValues(prev => ({ ...prev, [field]: value }));
198+
}, []);
199+
171200
if (loading || schema.loading) {
172201
return (
173202
<div className={cn('space-y-4', className)}>
@@ -232,6 +261,35 @@ export const DetailView: React.FC<DetailViewProps> = ({
232261
<SchemaRenderer key={index} schema={action} data={data} />
233262
))}
234263

264+
{/* Inline Edit Toggle */}
265+
{inlineEdit && (
266+
<Tooltip>
267+
<TooltipTrigger asChild>
268+
<Button
269+
variant={isInlineEditing ? 'default' : 'outline'}
270+
size="sm"
271+
onClick={handleInlineEditToggle}
272+
className="gap-2"
273+
>
274+
{isInlineEditing ? (
275+
<>
276+
<Check className="h-4 w-4" />
277+
<span className="hidden sm:inline">Save</span>
278+
</>
279+
) : (
280+
<>
281+
<Edit className="h-4 w-4" />
282+
<span className="hidden sm:inline">Edit inline</span>
283+
</>
284+
)}
285+
</Button>
286+
</TooltipTrigger>
287+
<TooltipContent>
288+
{isInlineEditing ? 'Save changes' : 'Edit fields inline'}
289+
</TooltipContent>
290+
</Tooltip>
291+
)}
292+
235293
{/* Share Button */}
236294
<Tooltip>
237295
<TooltipTrigger asChild>
@@ -311,7 +369,9 @@ export const DetailView: React.FC<DetailViewProps> = ({
311369
<DetailSection
312370
key={index}
313371
section={section}
314-
data={data}
372+
data={{ ...data, ...editedValues }}
373+
isEditing={isInlineEditing}
374+
onFieldChange={handleInlineFieldChange}
315375
/>
316376
))}
317377
</div>
@@ -324,7 +384,9 @@ export const DetailView: React.FC<DetailViewProps> = ({
324384
fields: schema.fields,
325385
columns: schema.columns || 2,
326386
}}
327-
data={data}
387+
data={{ ...data, ...editedValues }}
388+
isEditing={isInlineEditing}
389+
onFieldChange={handleInlineFieldChange}
328390
/>
329391
)}
330392

packages/plugin-gantt/src/GanttView.tsx

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,12 @@ export interface GanttViewProps {
5858
startDate?: Date
5959
endDate?: Date
6060
onTaskClick?: (task: GanttTask) => void
61+
onTaskUpdate?: (task: GanttTask, changes: Partial<Pick<GanttTask, 'title' | 'start' | 'end' | 'progress'>>) => void
6162
onViewChange?: (view: GanttViewMode) => void
6263
onAddClick?: () => void
6364
className?: string
65+
/** Enable inline editing of task fields */
66+
inlineEdit?: boolean
6467
}
6568

6669
export function GanttView({
@@ -69,15 +72,19 @@ export function GanttView({
6972
startDate,
7073
endDate,
7174
onTaskClick,
75+
onTaskUpdate,
7276
onViewChange,
7377
onAddClick,
74-
className
78+
className,
79+
inlineEdit = false,
7580
}: GanttViewProps) {
7681
const [currentDate, setCurrentDate] = React.useState(new Date());
7782
const [rowHeight, setRowHeight] = React.useState(
7883
typeof window !== 'undefined' && window.innerWidth < 640 ? 32 : 40
7984
);
8085
const [columnWidth, setColumnWidth] = React.useState(getResponsiveColumnWidth());
86+
const [editingTask, setEditingTask] = React.useState<string | number | null>(null);
87+
const [editValues, setEditValues] = React.useState<Record<string, string>>({});
8188

8289
React.useEffect(() => {
8390
const handleResize = () => {
@@ -251,28 +258,85 @@ export function GanttView({
251258
ref={listRef}
252259
style={{ width: taskListWidth, minWidth: taskListWidth }}
253260
>
254-
{tasks.map((task) => (
261+
{tasks.map((task) => {
262+
const isEditing = inlineEdit && editingTask === task.id;
263+
return (
255264
<div
256265
key={task.id}
257266
className="flex items-center border-b px-2 sm:px-4 hover:bg-accent/50 cursor-pointer transition-colors touch-manipulation"
258267
style={{ height: rowHeight }}
259-
onClick={() => onTaskClick?.(task)}
268+
onClick={() => !isEditing && onTaskClick?.(task)}
269+
onDoubleClick={() => {
270+
if (inlineEdit && onTaskUpdate) {
271+
setEditingTask(task.id);
272+
setEditValues({
273+
title: task.title,
274+
start: task.start.toISOString().split('T')[0],
275+
end: task.end.toISOString().split('T')[0],
276+
progress: String(task.progress),
277+
});
278+
}
279+
}}
260280
>
261281
<div className="flex-1 truncate font-medium text-xs sm:text-sm flex items-center gap-2">
262282
<div
263283
className="w-2 h-2 rounded-full shrink-0"
264284
style={{ backgroundColor: task.color || '#3b82f6' }}
265285
/>
266-
{task.title}
286+
{isEditing ? (
287+
<input
288+
className="border rounded px-1 py-0.5 text-xs w-full bg-background"
289+
value={editValues.title || ''}
290+
onChange={(e) => setEditValues(prev => ({ ...prev, title: e.target.value }))}
291+
onKeyDown={(e) => {
292+
if (e.key === 'Enter') {
293+
onTaskUpdate?.(task, {
294+
title: editValues.title,
295+
start: new Date(editValues.start),
296+
end: new Date(editValues.end),
297+
progress: Number(editValues.progress) || 0,
298+
});
299+
setEditingTask(null);
300+
} else if (e.key === 'Escape') {
301+
setEditingTask(null);
302+
}
303+
}}
304+
onClick={(e) => e.stopPropagation()}
305+
autoFocus
306+
/>
307+
) : (
308+
task.title
309+
)}
267310
</div>
268311
<div className="w-16 sm:w-20 text-right text-xs text-muted-foreground hidden sm:block">
269-
{task.start.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })}
312+
{isEditing ? (
313+
<input
314+
type="date"
315+
className="border rounded px-1 py-0.5 text-xs w-full bg-background"
316+
value={editValues.start || ''}
317+
onChange={(e) => setEditValues(prev => ({ ...prev, start: e.target.value }))}
318+
onClick={(e) => e.stopPropagation()}
319+
/>
320+
) : (
321+
task.start.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })
322+
)}
270323
</div>
271324
<div className="w-16 sm:w-20 text-right text-xs text-muted-foreground hidden sm:block">
272-
{task.end.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })}
325+
{isEditing ? (
326+
<input
327+
type="date"
328+
className="border rounded px-1 py-0.5 text-xs w-full bg-background"
329+
value={editValues.end || ''}
330+
onChange={(e) => setEditValues(prev => ({ ...prev, end: e.target.value }))}
331+
onClick={(e) => e.stopPropagation()}
332+
/>
333+
) : (
334+
task.end.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })
335+
)}
273336
</div>
274337
</div>
275-
))}
338+
);
339+
})}
276340
</div>
277341

278342
{/* Right Side: Timeline */}

0 commit comments

Comments
 (0)