@object-ui/plugin-detail 0.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +35 -0
- package/dist/index.js +22617 -20846
- package/dist/index.umd.cjs +35 -26
- package/dist/plugin-detail.css +1 -1
- package/dist/src/DetailSection.d.ts.map +1 -1
- package/dist/src/DetailView.d.ts.map +1 -1
- package/dist/src/DetailView.stories.d.ts +37 -0
- package/dist/src/DetailView.stories.d.ts.map +1 -0
- package/dist/src/RelatedList.d.ts +2 -0
- package/dist/src/RelatedList.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/DetailSection.tsx +113 -22
- package/src/DetailView.stories.tsx +258 -0
- package/src/DetailView.tsx +206 -55
- package/src/RelatedList.tsx +30 -5
- package/src/index.tsx +13 -0
package/src/DetailView.tsx
CHANGED
|
@@ -7,8 +7,32 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import * as React from 'react';
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
cn,
|
|
12
|
+
Button,
|
|
13
|
+
Skeleton,
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuItem,
|
|
17
|
+
DropdownMenuSeparator,
|
|
18
|
+
DropdownMenuTrigger,
|
|
19
|
+
Tooltip,
|
|
20
|
+
TooltipContent,
|
|
21
|
+
TooltipProvider,
|
|
22
|
+
TooltipTrigger,
|
|
23
|
+
} from '@object-ui/components';
|
|
24
|
+
import {
|
|
25
|
+
ArrowLeft,
|
|
26
|
+
Edit,
|
|
27
|
+
Trash2,
|
|
28
|
+
MoreHorizontal,
|
|
29
|
+
Share2,
|
|
30
|
+
Copy,
|
|
31
|
+
Download,
|
|
32
|
+
History,
|
|
33
|
+
Star,
|
|
34
|
+
StarOff,
|
|
35
|
+
} from 'lucide-react';
|
|
12
36
|
import { DetailSection } from './DetailSection';
|
|
13
37
|
import { DetailTabs } from './DetailTabs';
|
|
14
38
|
import { RelatedList } from './RelatedList';
|
|
@@ -34,6 +58,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
34
58
|
}) => {
|
|
35
59
|
const [data, setData] = React.useState<any>(schema.data);
|
|
36
60
|
const [loading, setLoading] = React.useState(!schema.data && !!((schema.api && schema.resourceId) || (dataSource && schema.objectName && schema.resourceId)));
|
|
61
|
+
const [isFavorite, setIsFavorite] = React.useState(false);
|
|
37
62
|
|
|
38
63
|
// Fetch data if API or DataSource provided
|
|
39
64
|
React.useEffect(() => {
|
|
@@ -55,43 +80,93 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
55
80
|
});
|
|
56
81
|
} else if (schema.api && schema.resourceId) {
|
|
57
82
|
setLoading(true);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
fetch(`${schema.api}/${schema.resourceId}`)
|
|
84
|
+
.then(res => res.json())
|
|
85
|
+
.then(result => {
|
|
86
|
+
setData(result?.data || result);
|
|
87
|
+
})
|
|
88
|
+
.catch(err => {
|
|
89
|
+
console.error('Failed to fetch detail data:', err);
|
|
90
|
+
})
|
|
91
|
+
.finally(() => setLoading(false));
|
|
63
92
|
}
|
|
64
93
|
}, [schema.api, schema.resourceId]);
|
|
65
94
|
|
|
66
95
|
const handleBack = React.useCallback(() => {
|
|
67
96
|
if (onBack) {
|
|
68
97
|
onBack();
|
|
98
|
+
} else if (schema.onNavigate) {
|
|
99
|
+
// SPA-aware navigation
|
|
100
|
+
const backUrl = schema.backUrl || (schema.objectName ? `/${schema.objectName}` : '/');
|
|
101
|
+
schema.onNavigate(backUrl, { replace: true });
|
|
69
102
|
} else if (schema.backUrl) {
|
|
70
103
|
window.location.href = schema.backUrl;
|
|
71
104
|
} else {
|
|
72
105
|
window.history.back();
|
|
73
106
|
}
|
|
74
|
-
}, [onBack, schema
|
|
107
|
+
}, [onBack, schema]);
|
|
75
108
|
|
|
76
109
|
const handleEdit = React.useCallback(() => {
|
|
77
110
|
if (onEdit) {
|
|
78
111
|
onEdit();
|
|
112
|
+
} else if (schema.onNavigate && schema.editUrl) {
|
|
113
|
+
// SPA-aware navigation
|
|
114
|
+
schema.onNavigate(schema.editUrl);
|
|
115
|
+
} else if (schema.onNavigate && schema.objectName && schema.resourceId) {
|
|
116
|
+
// Build edit URL from object + resource
|
|
117
|
+
schema.onNavigate(`/${schema.objectName}/${schema.resourceId}/edit`);
|
|
79
118
|
} else if (schema.editUrl) {
|
|
80
119
|
window.location.href = schema.editUrl;
|
|
81
120
|
}
|
|
82
|
-
|
|
83
|
-
// else {
|
|
84
|
-
// setEditMode(true);
|
|
85
|
-
// }
|
|
86
|
-
}, [onEdit, schema.editUrl]);
|
|
121
|
+
}, [onEdit, schema]);
|
|
87
122
|
|
|
88
123
|
const handleDelete = React.useCallback(() => {
|
|
89
|
-
// TODO: Replace with proper confirmation dialog component
|
|
90
124
|
const confirmMessage = schema.deleteConfirmation || 'Are you sure you want to delete this record?';
|
|
125
|
+
// Use window.confirm as fallback — the ActionProvider's onConfirm handler
|
|
126
|
+
// will intercept this if wired up via the action system.
|
|
91
127
|
if (window.confirm(confirmMessage)) {
|
|
92
128
|
onDelete?.();
|
|
129
|
+
// Navigate back after deletion if onNavigate available
|
|
130
|
+
if (schema.onNavigate && schema.objectName) {
|
|
131
|
+
schema.onNavigate(`/${schema.objectName}`, { replace: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}, [onDelete, schema]);
|
|
135
|
+
|
|
136
|
+
const handleShare = React.useCallback(() => {
|
|
137
|
+
// Share functionality - could trigger share dialog or copy link
|
|
138
|
+
if (navigator.share && schema.objectName && schema.resourceId) {
|
|
139
|
+
navigator.share({
|
|
140
|
+
title: schema.title || 'Record Details',
|
|
141
|
+
text: `${schema.objectName} #${schema.resourceId}`,
|
|
142
|
+
url: window.location.href,
|
|
143
|
+
}).catch((err) => console.log('Share failed:', err));
|
|
144
|
+
} else {
|
|
145
|
+
// Fallback: copy link to clipboard
|
|
146
|
+
navigator.clipboard.writeText(window.location.href).then(() => {
|
|
147
|
+
console.log('Link copied to clipboard');
|
|
148
|
+
});
|
|
93
149
|
}
|
|
94
|
-
}, [
|
|
150
|
+
}, [schema]);
|
|
151
|
+
|
|
152
|
+
const handleDuplicate = React.useCallback(() => {
|
|
153
|
+
// Duplicate functionality - could navigate to create page with prefilled data
|
|
154
|
+
console.log('Duplicate record:', data);
|
|
155
|
+
}, [data]);
|
|
156
|
+
|
|
157
|
+
const handleExport = React.useCallback(() => {
|
|
158
|
+
// Export functionality - could download as JSON, PDF, etc.
|
|
159
|
+
console.log('Export record:', data);
|
|
160
|
+
}, [data]);
|
|
161
|
+
|
|
162
|
+
const handleViewHistory = React.useCallback(() => {
|
|
163
|
+
// View history functionality
|
|
164
|
+
console.log('View history for record:', schema.resourceId);
|
|
165
|
+
}, [schema]);
|
|
166
|
+
|
|
167
|
+
const handleToggleFavorite = React.useCallback(() => {
|
|
168
|
+
setIsFavorite(!isFavorite);
|
|
169
|
+
}, [isFavorite]);
|
|
95
170
|
|
|
96
171
|
if (loading || schema.loading) {
|
|
97
172
|
return (
|
|
@@ -104,49 +179,123 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
104
179
|
}
|
|
105
180
|
|
|
106
181
|
return (
|
|
107
|
-
<
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
<div className="flex items-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
</p>
|
|
182
|
+
<TooltipProvider>
|
|
183
|
+
<div className={cn('space-y-6', className)}>
|
|
184
|
+
{/* Header - Airtable-inspired layout */}
|
|
185
|
+
<div className="flex flex-col sm:flex-row items-start justify-between gap-3 sm:gap-4 pb-4 border-b">
|
|
186
|
+
<div className="flex items-start gap-2 sm:gap-3 flex-1 min-w-0">
|
|
187
|
+
{(schema.showBack ?? true) && (
|
|
188
|
+
<Tooltip>
|
|
189
|
+
<TooltipTrigger asChild>
|
|
190
|
+
<Button variant="ghost" size="icon" onClick={handleBack} className="shrink-0 mt-1">
|
|
191
|
+
<ArrowLeft className="h-4 w-4" />
|
|
192
|
+
</Button>
|
|
193
|
+
</TooltipTrigger>
|
|
194
|
+
<TooltipContent>Back</TooltipContent>
|
|
195
|
+
</Tooltip>
|
|
122
196
|
)}
|
|
197
|
+
<div className="flex-1 min-w-0">
|
|
198
|
+
<div className="flex items-center gap-2">
|
|
199
|
+
<h1 className="text-xl sm:text-2xl font-bold truncate">{schema.title || 'Details'}</h1>
|
|
200
|
+
<Tooltip>
|
|
201
|
+
<TooltipTrigger asChild>
|
|
202
|
+
<Button
|
|
203
|
+
variant="ghost"
|
|
204
|
+
size="icon"
|
|
205
|
+
className="h-6 w-6 shrink-0"
|
|
206
|
+
onClick={handleToggleFavorite}
|
|
207
|
+
>
|
|
208
|
+
{isFavorite ? (
|
|
209
|
+
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
|
|
210
|
+
) : (
|
|
211
|
+
<StarOff className="h-4 w-4 text-muted-foreground" />
|
|
212
|
+
)}
|
|
213
|
+
</Button>
|
|
214
|
+
</TooltipTrigger>
|
|
215
|
+
<TooltipContent>
|
|
216
|
+
{isFavorite ? 'Remove from favorites' : 'Add to favorites'}
|
|
217
|
+
</TooltipContent>
|
|
218
|
+
</Tooltip>
|
|
219
|
+
</div>
|
|
220
|
+
{schema.objectName && (
|
|
221
|
+
<p className="text-sm text-muted-foreground mt-1 flex items-center gap-1.5">
|
|
222
|
+
<span className="font-medium">{schema.objectName}</span>
|
|
223
|
+
<span className="text-muted-foreground/60">•</span>
|
|
224
|
+
<span>#{schema.resourceId}</span>
|
|
225
|
+
</p>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
123
228
|
</div>
|
|
124
|
-
</div>
|
|
125
229
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
230
|
+
<div className="flex flex-wrap items-center gap-1.5 shrink-0 w-full sm:w-auto">
|
|
231
|
+
{schema.actions?.map((action, index) => (
|
|
232
|
+
<SchemaRenderer key={index} schema={action} data={data} />
|
|
233
|
+
))}
|
|
234
|
+
|
|
235
|
+
{/* Share Button */}
|
|
236
|
+
<Tooltip>
|
|
237
|
+
<TooltipTrigger asChild>
|
|
238
|
+
<Button variant="outline" size="icon" onClick={handleShare}>
|
|
239
|
+
<Share2 className="h-4 w-4" />
|
|
240
|
+
</Button>
|
|
241
|
+
</TooltipTrigger>
|
|
242
|
+
<TooltipContent>Share</TooltipContent>
|
|
243
|
+
</Tooltip>
|
|
244
|
+
|
|
245
|
+
{/* Edit Button */}
|
|
246
|
+
{schema.showEdit && (
|
|
247
|
+
<Tooltip>
|
|
248
|
+
<TooltipTrigger asChild>
|
|
249
|
+
<Button variant="default" onClick={handleEdit} className="gap-2">
|
|
250
|
+
<Edit className="h-4 w-4" />
|
|
251
|
+
<span className="hidden sm:inline">Edit</span>
|
|
252
|
+
</Button>
|
|
253
|
+
</TooltipTrigger>
|
|
254
|
+
<TooltipContent>Edit record</TooltipContent>
|
|
255
|
+
</Tooltip>
|
|
256
|
+
)}
|
|
130
257
|
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
258
|
+
{/* More Actions Menu */}
|
|
259
|
+
<DropdownMenu>
|
|
260
|
+
<Tooltip>
|
|
261
|
+
<TooltipTrigger asChild>
|
|
262
|
+
<DropdownMenuTrigger asChild>
|
|
263
|
+
<Button variant="ghost" size="icon">
|
|
264
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
265
|
+
</Button>
|
|
266
|
+
</DropdownMenuTrigger>
|
|
267
|
+
</TooltipTrigger>
|
|
268
|
+
<TooltipContent>More actions</TooltipContent>
|
|
269
|
+
</Tooltip>
|
|
270
|
+
<DropdownMenuContent align="end" className="w-[calc(100vw-2rem)] sm:w-48 max-h-[60vh] overflow-y-auto">
|
|
271
|
+
<DropdownMenuItem onClick={handleDuplicate}>
|
|
272
|
+
<Copy className="h-4 w-4 mr-2" />
|
|
273
|
+
Duplicate
|
|
274
|
+
</DropdownMenuItem>
|
|
275
|
+
<DropdownMenuItem onClick={handleExport}>
|
|
276
|
+
<Download className="h-4 w-4 mr-2" />
|
|
277
|
+
Export
|
|
278
|
+
</DropdownMenuItem>
|
|
279
|
+
<DropdownMenuItem onClick={handleViewHistory}>
|
|
280
|
+
<History className="h-4 w-4 mr-2" />
|
|
281
|
+
View history
|
|
282
|
+
</DropdownMenuItem>
|
|
283
|
+
{schema.showDelete && (
|
|
284
|
+
<>
|
|
285
|
+
<DropdownMenuSeparator />
|
|
286
|
+
<DropdownMenuItem
|
|
287
|
+
onClick={handleDelete}
|
|
288
|
+
className="text-destructive focus:text-destructive"
|
|
289
|
+
>
|
|
290
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
291
|
+
Delete
|
|
292
|
+
</DropdownMenuItem>
|
|
293
|
+
</>
|
|
294
|
+
)}
|
|
295
|
+
</DropdownMenuContent>
|
|
296
|
+
</DropdownMenu>
|
|
297
|
+
</div>
|
|
148
298
|
</div>
|
|
149
|
-
</div>
|
|
150
299
|
|
|
151
300
|
{/* Custom Header */}
|
|
152
301
|
{schema.header && (
|
|
@@ -157,7 +306,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
157
306
|
|
|
158
307
|
{/* Sections */}
|
|
159
308
|
{schema.sections && schema.sections.length > 0 && (
|
|
160
|
-
<div className="space-y-4">
|
|
309
|
+
<div className="space-y-3 sm:space-y-4">
|
|
161
310
|
{schema.sections.map((section, index) => (
|
|
162
311
|
<DetailSection
|
|
163
312
|
key={index}
|
|
@@ -196,6 +345,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
196
345
|
api={related.api}
|
|
197
346
|
data={related.data}
|
|
198
347
|
columns={related.columns as any}
|
|
348
|
+
dataSource={dataSource}
|
|
199
349
|
/>
|
|
200
350
|
))}
|
|
201
351
|
</div>
|
|
@@ -207,6 +357,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
207
357
|
<SchemaRenderer schema={schema.footer} data={data} />
|
|
208
358
|
</div>
|
|
209
359
|
)}
|
|
210
|
-
|
|
360
|
+
</div>
|
|
361
|
+
</TooltipProvider>
|
|
211
362
|
);
|
|
212
363
|
};
|
package/src/RelatedList.tsx
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as React from 'react';
|
|
10
10
|
import { Card, CardHeader, CardTitle, CardContent } from '@object-ui/components';
|
|
11
11
|
import { SchemaRenderer } from '@object-ui/react';
|
|
12
|
+
import type { DataSource } from '@object-ui/types';
|
|
12
13
|
|
|
13
14
|
export interface RelatedListProps {
|
|
14
15
|
title: string;
|
|
@@ -18,6 +19,7 @@ export interface RelatedListProps {
|
|
|
18
19
|
schema?: any;
|
|
19
20
|
columns?: any[];
|
|
20
21
|
className?: string;
|
|
22
|
+
dataSource?: DataSource;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export const RelatedList: React.FC<RelatedListProps> = ({
|
|
@@ -28,18 +30,41 @@ export const RelatedList: React.FC<RelatedListProps> = ({
|
|
|
28
30
|
schema,
|
|
29
31
|
columns,
|
|
30
32
|
className,
|
|
33
|
+
dataSource,
|
|
31
34
|
}) => {
|
|
32
|
-
const [relatedData] = React.useState(data);
|
|
35
|
+
const [relatedData, setRelatedData] = React.useState(data);
|
|
33
36
|
const [loading, setLoading] = React.useState(false);
|
|
34
37
|
|
|
35
38
|
React.useEffect(() => {
|
|
36
39
|
if (api && !data.length) {
|
|
37
40
|
setLoading(true);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
if (dataSource) {
|
|
42
|
+
dataSource.find(api).then((result) => {
|
|
43
|
+
const items = Array.isArray(result)
|
|
44
|
+
? result
|
|
45
|
+
: Array.isArray((result as any)?.data)
|
|
46
|
+
? (result as any).data
|
|
47
|
+
: [];
|
|
48
|
+
setRelatedData(items);
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}).catch((err) => {
|
|
51
|
+
console.error('Failed to fetch related data:', err);
|
|
52
|
+
setLoading(false);
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
fetch(api)
|
|
56
|
+
.then(res => res.json())
|
|
57
|
+
.then(result => {
|
|
58
|
+
const items = Array.isArray(result) ? result : (result?.data || []);
|
|
59
|
+
setRelatedData(items);
|
|
60
|
+
})
|
|
61
|
+
.catch(err => {
|
|
62
|
+
console.error('Failed to fetch related data:', err);
|
|
63
|
+
})
|
|
64
|
+
.finally(() => setLoading(false));
|
|
65
|
+
}
|
|
41
66
|
}
|
|
42
|
-
}, [api, data]);
|
|
67
|
+
}, [api, data, dataSource]);
|
|
43
68
|
|
|
44
69
|
const viewSchema = React.useMemo(() => {
|
|
45
70
|
if (schema) return schema;
|
package/src/index.tsx
CHANGED
|
@@ -84,3 +84,16 @@ ComponentRegistry.register('related-list', RelatedList, {
|
|
|
84
84
|
{ name: 'columns', type: 'array', label: 'Columns' },
|
|
85
85
|
],
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
// Alias for generic view
|
|
89
|
+
ComponentRegistry.register('detail', DetailView, {
|
|
90
|
+
namespace: 'view',
|
|
91
|
+
category: 'view',
|
|
92
|
+
label: 'Detail',
|
|
93
|
+
icon: 'FileText',
|
|
94
|
+
inputs: [
|
|
95
|
+
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
96
|
+
{ name: 'recordId', type: 'string', label: 'Record ID' },
|
|
97
|
+
{ name: 'fields', type: 'array', label: 'Fields' },
|
|
98
|
+
]
|
|
99
|
+
});
|