@object-ui/plugin-detail 3.0.3 โ†’ 3.1.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.
Files changed (131) hide show
  1. package/.turbo/turbo-build.log +45 -8
  2. package/dist/AddressField-C07oUOY6.js +96 -0
  3. package/dist/AutoNumberField-BxnFqllo.js +8 -0
  4. package/dist/AvatarField-VThNABzo.js +82 -0
  5. package/dist/BooleanField-CGHKBzAi.js +37 -0
  6. package/dist/CodeField-Co_muhRR.js +21 -0
  7. package/dist/ColorField-DLid_tFz.js +42 -0
  8. package/dist/CurrencyField-Bw-LqANM.js +43 -0
  9. package/dist/DateField-BNHAzMB2.js +21 -0
  10. package/dist/DateTimeField-DjAyn_DQ.js +28 -0
  11. package/dist/EmailField-xoNcSppb.js +31 -0
  12. package/dist/FileField-DbNJwjU2.js +133 -0
  13. package/dist/FormulaField-CJkkwIK8.js +9 -0
  14. package/dist/GeolocationField-C1AnS6VV.js +123 -0
  15. package/dist/GridField-DATAHIKf.js +30 -0
  16. package/dist/ImageField-CEKJpyJp.js +90 -0
  17. package/dist/LocationField-jDWXjlpx.js +31 -0
  18. package/dist/LookupField-DQ08L9UQ.js +96 -0
  19. package/dist/MasterDetailField-Dbk529Ea.js +108 -0
  20. package/dist/NumberField-BVroN9aV.js +26 -0
  21. package/dist/ObjectField-CT3l_IHW.js +48 -0
  22. package/dist/PasswordField-DweVLEE0.js +38 -0
  23. package/dist/PercentField-ZpWUK97K.js +63 -0
  24. package/dist/PhoneField-mw-9fqZ_.js +31 -0
  25. package/dist/QRCodeField-Cbb9ck59.js +77 -0
  26. package/dist/RatingField-CSqgLS6t.js +47 -0
  27. package/dist/RichTextField-BpfBOd99.js +38 -0
  28. package/dist/SelectField-B9Ei-5jl.js +26 -0
  29. package/dist/SignatureField-DgGpHnQ8.js +85 -0
  30. package/dist/SliderField-C6HvOHd8.js +30 -0
  31. package/dist/SummaryField-ugYPYxjP.js +9 -0
  32. package/dist/TextAreaField-BK3RgzY3.js +39 -0
  33. package/dist/TextField-Bvzx3atT.js +32 -0
  34. package/dist/TimeField-Cuz9-Uai.js +21 -0
  35. package/dist/UrlField-B6XHTV73.js +33 -0
  36. package/dist/UserField-ooTul2d6.js +49 -0
  37. package/dist/VectorField-CKg9jdGa.js +25 -0
  38. package/dist/index-CnlyRfY_.js +59461 -0
  39. package/dist/index.js +30 -55026
  40. package/dist/index.umd.cjs +41 -30
  41. package/dist/plugin-detail.css +1 -1
  42. package/dist/src/ActivityTimeline.d.ts +20 -0
  43. package/dist/src/ActivityTimeline.d.ts.map +1 -0
  44. package/dist/src/CommentAttachment.d.ts +25 -0
  45. package/dist/src/CommentAttachment.d.ts.map +1 -0
  46. package/dist/src/CommentInput.d.ts +24 -0
  47. package/dist/src/CommentInput.d.ts.map +1 -0
  48. package/dist/src/DetailSection.d.ts +6 -0
  49. package/dist/src/DetailSection.d.ts.map +1 -1
  50. package/dist/src/DetailView.d.ts +4 -0
  51. package/dist/src/DetailView.d.ts.map +1 -1
  52. package/dist/src/DetailView.stories.d.ts +8 -0
  53. package/dist/src/DetailView.stories.d.ts.map +1 -1
  54. package/dist/src/DiffView.d.ts +24 -0
  55. package/dist/src/DiffView.d.ts.map +1 -0
  56. package/dist/src/FieldChangeItem.d.ts +21 -0
  57. package/dist/src/FieldChangeItem.d.ts.map +1 -0
  58. package/dist/src/InlineCreateRelated.d.ts +32 -0
  59. package/dist/src/InlineCreateRelated.d.ts.map +1 -0
  60. package/dist/src/MentionAutocomplete.d.ts +43 -0
  61. package/dist/src/MentionAutocomplete.d.ts.map +1 -0
  62. package/dist/src/PointInTimeRestore.d.ts +28 -0
  63. package/dist/src/PointInTimeRestore.d.ts.map +1 -0
  64. package/dist/src/ReactionPicker.d.ts +25 -0
  65. package/dist/src/ReactionPicker.d.ts.map +1 -0
  66. package/dist/src/RecordActivityTimeline.d.ts +49 -0
  67. package/dist/src/RecordActivityTimeline.d.ts.map +1 -0
  68. package/dist/src/RecordChatterPanel.d.ts +48 -0
  69. package/dist/src/RecordChatterPanel.d.ts.map +1 -0
  70. package/dist/src/RecordComments.d.ts +20 -0
  71. package/dist/src/RecordComments.d.ts.map +1 -0
  72. package/dist/src/RecordNavigationEnhanced.d.ts +18 -0
  73. package/dist/src/RecordNavigationEnhanced.d.ts.map +1 -0
  74. package/dist/src/RelatedList.d.ts +4 -0
  75. package/dist/src/RelatedList.d.ts.map +1 -1
  76. package/dist/src/RelationshipGraph.d.ts +23 -0
  77. package/dist/src/RelationshipGraph.d.ts.map +1 -0
  78. package/dist/src/RichTextCommentInput.d.ts +24 -0
  79. package/dist/src/RichTextCommentInput.d.ts.map +1 -0
  80. package/dist/src/SubscriptionToggle.d.ts +22 -0
  81. package/dist/src/SubscriptionToggle.d.ts.map +1 -0
  82. package/dist/src/ThreadedReplies.d.ts +26 -0
  83. package/dist/src/ThreadedReplies.d.ts.map +1 -0
  84. package/dist/src/autoLayout.d.ts +34 -0
  85. package/dist/src/autoLayout.d.ts.map +1 -0
  86. package/dist/src/index.d.ts +36 -0
  87. package/dist/src/index.d.ts.map +1 -1
  88. package/dist/src/useDetailTranslation.d.ts +34 -0
  89. package/dist/src/useDetailTranslation.d.ts.map +1 -0
  90. package/package.json +8 -7
  91. package/src/ActivityTimeline.tsx +184 -0
  92. package/src/CommentAttachment.tsx +192 -0
  93. package/src/CommentInput.tsx +81 -0
  94. package/src/DetailSection.tsx +74 -9
  95. package/src/DetailView.stories.tsx +76 -0
  96. package/src/DetailView.tsx +270 -27
  97. package/src/DiffView.tsx +231 -0
  98. package/src/FieldChangeItem.tsx +46 -0
  99. package/src/InlineCreateRelated.tsx +291 -0
  100. package/src/MentionAutocomplete.tsx +123 -0
  101. package/src/PointInTimeRestore.tsx +261 -0
  102. package/src/ReactionPicker.tsx +106 -0
  103. package/src/RecordActivityTimeline.tsx +429 -0
  104. package/src/RecordChatterPanel.tsx +202 -0
  105. package/src/RecordComments.tsx +215 -0
  106. package/src/RecordNavigationEnhanced.tsx +211 -0
  107. package/src/RelatedList.tsx +37 -8
  108. package/src/RelationshipGraph.tsx +286 -0
  109. package/src/RichTextCommentInput.tsx +348 -0
  110. package/src/SubscriptionToggle.tsx +60 -0
  111. package/src/ThreadedReplies.tsx +161 -0
  112. package/src/__tests__/ActivityTimeline.test.tsx +119 -0
  113. package/src/__tests__/ActivityTimelineFiltering.test.tsx +143 -0
  114. package/src/__tests__/CommentInput.test.tsx +57 -0
  115. package/src/__tests__/DetailSection.test.tsx +320 -0
  116. package/src/__tests__/DetailView.test.tsx +415 -1
  117. package/src/__tests__/FieldChangeItem.test.tsx +119 -0
  118. package/src/__tests__/MentionAutocomplete.test.tsx +97 -0
  119. package/src/__tests__/ReactionPicker.test.tsx +113 -0
  120. package/src/__tests__/RecordActivityTimeline.test.tsx +395 -0
  121. package/src/__tests__/RecordChatterPanel.test.tsx +227 -0
  122. package/src/__tests__/RecordComments.test.tsx +96 -0
  123. package/src/__tests__/RecordCommentsPinSearch.test.tsx +133 -0
  124. package/src/__tests__/RelatedList.test.tsx +66 -0
  125. package/src/__tests__/SubscriptionToggle.test.tsx +84 -0
  126. package/src/__tests__/ThreadedReplies.test.tsx +212 -0
  127. package/src/__tests__/autoLayout.test.ts +184 -0
  128. package/src/__tests__/phase12-features.test.tsx +583 -0
  129. package/src/autoLayout.ts +111 -0
  130. package/src/index.tsx +46 -0
  131. package/src/useDetailTranslation.ts +103 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import * as React from 'react';
10
+ import {
11
+ cn,
12
+ Button,
13
+ Card,
14
+ CardHeader,
15
+ CardTitle,
16
+ CardContent,
17
+ } from '@object-ui/components';
18
+ import { History, RotateCcw, Eye, ChevronRight } from 'lucide-react';
19
+
20
+ export interface RevisionEntry {
21
+ id: string;
22
+ timestamp: string;
23
+ user: string;
24
+ changes: { field: string; oldValue: any; newValue: any }[];
25
+ /** Full record snapshot at this revision point. */
26
+ snapshot?: Record<string, any>;
27
+ }
28
+
29
+ export interface PointInTimeRestoreProps {
30
+ recordId: string;
31
+ revisions: RevisionEntry[];
32
+ onRestore?: (revisionId: string, snapshot: Record<string, any>) => void | Promise<void>;
33
+ className?: string;
34
+ }
35
+
36
+ function formatTimestamp(timestamp: string): string {
37
+ try {
38
+ const date = new Date(timestamp);
39
+ const now = new Date();
40
+ const diffMs = now.getTime() - date.getTime();
41
+ const diffMins = Math.floor(diffMs / 60000);
42
+
43
+ if (diffMins < 1) return 'just now';
44
+ if (diffMins < 60) return `${diffMins}m ago`;
45
+ const diffHours = Math.floor(diffMins / 60);
46
+ if (diffHours < 24) return `${diffHours}h ago`;
47
+ return date.toLocaleString();
48
+ } catch {
49
+ return timestamp;
50
+ }
51
+ }
52
+
53
+ export const PointInTimeRestore: React.FC<PointInTimeRestoreProps> = ({
54
+ recordId: _recordId,
55
+ revisions,
56
+ onRestore,
57
+ className,
58
+ }) => {
59
+ const [selectedRevisionId, setSelectedRevisionId] = React.useState<string | null>(null);
60
+ const [isConfirming, setIsConfirming] = React.useState(false);
61
+ const [isRestoring, setIsRestoring] = React.useState(false);
62
+
63
+ const selectedRevision = React.useMemo(
64
+ () => revisions.find((r) => r.id === selectedRevisionId) ?? null,
65
+ [revisions, selectedRevisionId],
66
+ );
67
+
68
+ const handleRestore = React.useCallback(async () => {
69
+ if (!selectedRevision || !onRestore) return;
70
+
71
+ if (!isConfirming) {
72
+ setIsConfirming(true);
73
+ return;
74
+ }
75
+
76
+ setIsRestoring(true);
77
+ try {
78
+ const snapshot = selectedRevision.snapshot ?? {};
79
+ await onRestore(selectedRevision.id, snapshot);
80
+ setIsConfirming(false);
81
+ setSelectedRevisionId(null);
82
+ } finally {
83
+ setIsRestoring(false);
84
+ }
85
+ }, [selectedRevision, onRestore, isConfirming]);
86
+
87
+ const handleCancelConfirm = React.useCallback(() => {
88
+ setIsConfirming(false);
89
+ }, []);
90
+
91
+ return (
92
+ <Card className={cn('', className)}>
93
+ <CardHeader className="pb-3">
94
+ <CardTitle className="flex items-center gap-2 text-base">
95
+ <History className="h-4 w-4" />
96
+ Revision History
97
+ <span className="text-sm font-normal text-muted-foreground">
98
+ ({revisions.length})
99
+ </span>
100
+ </CardTitle>
101
+ </CardHeader>
102
+ <CardContent>
103
+ {revisions.length === 0 ? (
104
+ <p className="text-sm text-muted-foreground text-center py-4">
105
+ No revisions recorded
106
+ </p>
107
+ ) : (
108
+ <div className="flex flex-col lg:flex-row gap-4">
109
+ {/* Timeline */}
110
+ <div className="flex-1 min-w-0">
111
+ <div className="relative">
112
+ <div className="absolute left-3 top-2 bottom-2 w-px bg-border" />
113
+ <div className="space-y-1">
114
+ {revisions.map((revision) => {
115
+ const isSelected = revision.id === selectedRevisionId;
116
+ return (
117
+ <button
118
+ key={revision.id}
119
+ type="button"
120
+ className={cn(
121
+ 'w-full text-left flex items-start gap-3 py-2 px-2 rounded-md transition-colors relative',
122
+ isSelected
123
+ ? 'bg-accent'
124
+ : 'hover:bg-accent/50',
125
+ )}
126
+ onClick={() => {
127
+ setSelectedRevisionId(revision.id);
128
+ setIsConfirming(false);
129
+ }}
130
+ >
131
+ {/* Timeline dot */}
132
+ <div
133
+ className={cn(
134
+ 'shrink-0 h-6 w-6 rounded-full border-2 flex items-center justify-center z-10 mt-0.5',
135
+ isSelected
136
+ ? 'border-primary bg-primary'
137
+ : 'border-border bg-background',
138
+ )}
139
+ >
140
+ {isSelected && (
141
+ <ChevronRight className="h-3 w-3 text-primary-foreground" />
142
+ )}
143
+ </div>
144
+ {/* Content */}
145
+ <div className="flex-1 min-w-0">
146
+ <div className="flex items-center gap-2">
147
+ <span className="text-sm font-medium">{revision.user}</span>
148
+ <span className="text-xs text-muted-foreground">
149
+ {formatTimestamp(revision.timestamp)}
150
+ </span>
151
+ </div>
152
+ <p className="text-xs text-muted-foreground mt-0.5">
153
+ {revision.changes.length} field{revision.changes.length !== 1 ? 's' : ''}{' '}
154
+ changed
155
+ </p>
156
+ </div>
157
+ </button>
158
+ );
159
+ })}
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ {/* Preview panel */}
165
+ {selectedRevision && (
166
+ <div className="lg:w-80 border rounded-md p-3 space-y-3">
167
+ <div className="flex items-center gap-2 text-sm font-medium">
168
+ <Eye className="h-4 w-4 text-muted-foreground" />
169
+ Revision Preview
170
+ </div>
171
+
172
+ {/* Field changes */}
173
+ <div className="space-y-2">
174
+ {selectedRevision.changes.map((change, i) => (
175
+ <div key={i} className="text-xs">
176
+ <span className="font-medium text-muted-foreground">
177
+ {change.field}
178
+ </span>
179
+ <div className="flex items-center gap-1.5 mt-0.5">
180
+ <span className="line-through text-red-600 dark:text-red-400 truncate max-w-[120px]">
181
+ {change.oldValue != null ? String(change.oldValue) : '(empty)'}
182
+ </span>
183
+ <ChevronRight className="h-3 w-3 text-muted-foreground shrink-0" />
184
+ <span className="text-green-600 dark:text-green-400 truncate max-w-[120px]">
185
+ {change.newValue != null ? String(change.newValue) : '(empty)'}
186
+ </span>
187
+ </div>
188
+ </div>
189
+ ))}
190
+ </div>
191
+
192
+ {/* Snapshot values */}
193
+ {selectedRevision.snapshot && (
194
+ <div className="border-t pt-2 space-y-1">
195
+ <p className="text-xs font-medium text-muted-foreground">
196
+ Record state at this point
197
+ </p>
198
+ <div className="max-h-40 overflow-y-auto space-y-1">
199
+ {Object.entries(selectedRevision.snapshot).map(([key, val]) => (
200
+ <div key={key} className="flex justify-between text-xs gap-2">
201
+ <span className="text-muted-foreground truncate">{key}</span>
202
+ <span className="font-mono truncate max-w-[140px]">
203
+ {val != null ? String(val) : 'โ€“'}
204
+ </span>
205
+ </div>
206
+ ))}
207
+ </div>
208
+ </div>
209
+ )}
210
+
211
+ {/* Restore button */}
212
+ {onRestore && (
213
+ <div className="pt-1 space-y-2">
214
+ {isConfirming ? (
215
+ <>
216
+ <p className="text-xs text-amber-600 dark:text-amber-400">
217
+ This will restore the record to its state at{' '}
218
+ {formatTimestamp(selectedRevision.timestamp)}. Continue?
219
+ </p>
220
+ <div className="flex gap-2">
221
+ <Button
222
+ variant="destructive"
223
+ size="sm"
224
+ className="gap-1.5 flex-1"
225
+ onClick={handleRestore}
226
+ disabled={isRestoring}
227
+ >
228
+ <RotateCcw className="h-3.5 w-3.5" />
229
+ {isRestoring ? 'Restoringโ€ฆ' : 'Confirm Restore'}
230
+ </Button>
231
+ <Button
232
+ variant="ghost"
233
+ size="sm"
234
+ onClick={handleCancelConfirm}
235
+ disabled={isRestoring}
236
+ >
237
+ Cancel
238
+ </Button>
239
+ </div>
240
+ </>
241
+ ) : (
242
+ <Button
243
+ variant="outline"
244
+ size="sm"
245
+ className="w-full gap-1.5"
246
+ onClick={handleRestore}
247
+ >
248
+ <RotateCcw className="h-3.5 w-3.5" />
249
+ Restore to this point
250
+ </Button>
251
+ )}
252
+ </div>
253
+ )}
254
+ </div>
255
+ )}
256
+ </div>
257
+ )}
258
+ </CardContent>
259
+ </Card>
260
+ );
261
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import * as React from 'react';
10
+ import { cn, Button } from '@object-ui/components';
11
+ import { SmilePlus } from 'lucide-react';
12
+ import type { Reaction } from '@object-ui/types';
13
+
14
+ const DEFAULT_EMOJI_OPTIONS = ['๐Ÿ‘', 'โค๏ธ', '๐ŸŽ‰', '๐Ÿ˜‚', '๐Ÿ˜ฎ', '๐Ÿ˜ข'];
15
+
16
+ export interface ReactionPickerProps {
17
+ /** Existing reactions on the feed item */
18
+ reactions: Reaction[];
19
+ /** Called when user adds or removes a reaction */
20
+ onToggleReaction?: (emoji: string) => void | Promise<void>;
21
+ /** Available emoji options */
22
+ emojiOptions?: string[];
23
+ className?: string;
24
+ }
25
+
26
+ /**
27
+ * ReactionPicker โ€” Emoji reaction selector and display.
28
+ * Shows existing reactions with counts, and a picker to add/remove.
29
+ * Aligned with @objectstack/spec ReactionSchema.
30
+ */
31
+ export const ReactionPicker: React.FC<ReactionPickerProps> = ({
32
+ reactions,
33
+ onToggleReaction,
34
+ emojiOptions = DEFAULT_EMOJI_OPTIONS,
35
+ className,
36
+ }) => {
37
+ const [showPicker, setShowPicker] = React.useState(false);
38
+
39
+ const handleReaction = React.useCallback(
40
+ (emoji: string) => {
41
+ onToggleReaction?.(emoji);
42
+ setShowPicker(false);
43
+ },
44
+ [onToggleReaction],
45
+ );
46
+
47
+ return (
48
+ <div className={cn('flex items-center gap-1 flex-wrap', className)}>
49
+ {/* Existing reactions */}
50
+ {reactions.map((reaction) => (
51
+ <button
52
+ key={reaction.emoji}
53
+ type="button"
54
+ className={cn(
55
+ 'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs border transition-colors',
56
+ reaction.reacted
57
+ ? 'bg-primary/10 border-primary/30 text-primary'
58
+ : 'bg-muted border-border text-muted-foreground hover:bg-muted/80',
59
+ )}
60
+ onClick={() => handleReaction(reaction.emoji)}
61
+ disabled={!onToggleReaction}
62
+ aria-label={`${reaction.emoji} ${reaction.count} reaction${reaction.count !== 1 ? 's' : ''}`}
63
+ >
64
+ <span>{reaction.emoji}</span>
65
+ <span>{reaction.count}</span>
66
+ </button>
67
+ ))}
68
+
69
+ {/* Add reaction button */}
70
+ {onToggleReaction && (
71
+ <div className="relative">
72
+ <Button
73
+ variant="ghost"
74
+ size="icon"
75
+ className="h-6 w-6"
76
+ onClick={() => setShowPicker(!showPicker)}
77
+ aria-label="Add reaction"
78
+ >
79
+ <SmilePlus className="h-3.5 w-3.5" />
80
+ </Button>
81
+
82
+ {showPicker && (
83
+ <div
84
+ className="absolute bottom-full mb-1 left-0 bg-popover border rounded-md shadow-md z-50 p-1.5 flex gap-1"
85
+ role="listbox"
86
+ aria-label="Emoji picker"
87
+ >
88
+ {emojiOptions.map((emoji) => (
89
+ <button
90
+ key={emoji}
91
+ type="button"
92
+ className="hover:bg-accent rounded p-1 text-base transition-colors"
93
+ onClick={() => handleReaction(emoji)}
94
+ role="option"
95
+ aria-selected={reactions.some(r => r.emoji === emoji && r.reacted)}
96
+ >
97
+ {emoji}
98
+ </button>
99
+ ))}
100
+ </div>
101
+ )}
102
+ </div>
103
+ )}
104
+ </div>
105
+ );
106
+ };