@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.
@@ -7,8 +7,32 @@
7
7
  */
8
8
 
9
9
  import * as React from 'react';
10
- import { cn, Button, Skeleton } from '@object-ui/components';
11
- import { ArrowLeft, Edit, Trash2, MoreHorizontal } from 'lucide-react';
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
- // TODO: Fetch from API
59
- // This would integrate with the data provider
60
- setTimeout(() => {
61
- setLoading(false);
62
- }, 500);
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.backUrl]);
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
- // TODO: Implement inline edit mode
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
- }, [onDelete, schema.deleteConfirmation]);
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
- <div className={cn('space-y-6', className)}>
108
- {/* Header */}
109
- <div className="flex items-center justify-between">
110
- <div className="flex items-center gap-4">
111
- {(schema.showBack ?? true) && (
112
- <Button variant="ghost" size="icon" onClick={handleBack}>
113
- <ArrowLeft className="h-4 w-4" />
114
- </Button>
115
- )}
116
- <div>
117
- <h1 className="text-2xl font-bold">{schema.title || 'Details'}</h1>
118
- {schema.objectName && (
119
- <p className="text-sm text-muted-foreground mt-1">
120
- {schema.objectName} #{schema.resourceId}
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
- <div className="flex items-center gap-2">
127
- {schema.actions?.map((action, index) => (
128
- <SchemaRenderer key={index} schema={action} data={data} />
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
- {schema.showEdit && (
132
- <Button variant="outline" onClick={handleEdit}>
133
- <Edit className="h-4 w-4 mr-2" />
134
- Edit
135
- </Button>
136
- )}
137
-
138
- {schema.showDelete && (
139
- <Button variant="destructive" onClick={handleDelete}>
140
- <Trash2 className="h-4 w-4 mr-2" />
141
- Delete
142
- </Button>
143
- )}
144
-
145
- <Button variant="ghost" size="icon">
146
- <MoreHorizontal className="h-4 w-4" />
147
- </Button>
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
- </div>
360
+ </div>
361
+ </TooltipProvider>
211
362
  );
212
363
  };
@@ -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
- // TODO: Fetch data from API
39
- // This would integrate with the data provider
40
- setLoading(false);
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
+ });