@object-ui/plugin-detail 3.0.3 โ 3.1.1
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 +45 -8
- package/CHANGELOG.md +11 -0
- package/dist/AddressField-B1iVr404.js +96 -0
- package/dist/AutoNumberField-BxnFqllo.js +8 -0
- package/dist/AvatarField-Duw4xOLZ.js +82 -0
- package/dist/BooleanField-CZ4axVeq.js +37 -0
- package/dist/CodeField-BSz-mk2v.js +21 -0
- package/dist/ColorField-B522ad8m.js +42 -0
- package/dist/CurrencyField-Cwr3_pow.js +43 -0
- package/dist/DateField-DCo6dxud.js +21 -0
- package/dist/DateTimeField-BWfBuANO.js +28 -0
- package/dist/EmailField-CpwbdVCU.js +31 -0
- package/dist/FileField-DVAUAJ8e.js +133 -0
- package/dist/FormulaField-CJkkwIK8.js +9 -0
- package/dist/GeolocationField-DNCKitgo.js +123 -0
- package/dist/GridField-DSblZNfp.js +30 -0
- package/dist/ImageField-DBAlnMon.js +90 -0
- package/dist/LocationField-DsHsXA6R.js +31 -0
- package/dist/LookupField-CsT0QQz2.js +96 -0
- package/dist/MasterDetailField-Db8b7Gqs.js +108 -0
- package/dist/NumberField-0IGp7lcA.js +26 -0
- package/dist/ObjectField-BLApgJtS.js +48 -0
- package/dist/PasswordField-pHKyNlmo.js +38 -0
- package/dist/PercentField-CwgKmlIb.js +63 -0
- package/dist/PhoneField-lKtbYOdN.js +31 -0
- package/dist/QRCodeField-BTTasT3w.js +77 -0
- package/dist/RatingField-De2X-l44.js +47 -0
- package/dist/RichTextField-B5QnvUOr.js +38 -0
- package/dist/SelectField-C9AZRHWu.js +26 -0
- package/dist/SignatureField-BgcEmYzd.js +85 -0
- package/dist/SliderField-BzrttVOY.js +30 -0
- package/dist/SummaryField-ugYPYxjP.js +9 -0
- package/dist/TextAreaField-DSE_CaU6.js +39 -0
- package/dist/TextField-DFQ4T9PR.js +32 -0
- package/dist/TimeField-F0cfmsps.js +21 -0
- package/dist/UrlField-DLXrFIH-.js +33 -0
- package/dist/UserField-PXMmxJY9.js +49 -0
- package/dist/VectorField-CKg9jdGa.js +25 -0
- package/dist/index-qQ1C-yUR.js +59976 -0
- package/dist/index.js +32 -55026
- package/dist/index.umd.cjs +41 -30
- package/dist/plugin-detail.css +1 -1
- package/dist/src/ActivityTimeline.d.ts +20 -0
- package/dist/src/ActivityTimeline.d.ts.map +1 -0
- package/dist/src/CommentAttachment.d.ts +25 -0
- package/dist/src/CommentAttachment.d.ts.map +1 -0
- package/dist/src/CommentInput.d.ts +24 -0
- package/dist/src/CommentInput.d.ts.map +1 -0
- package/dist/src/DetailSection.d.ts +8 -0
- package/dist/src/DetailSection.d.ts.map +1 -1
- package/dist/src/DetailView.d.ts +4 -0
- package/dist/src/DetailView.d.ts.map +1 -1
- package/dist/src/DetailView.stories.d.ts +8 -0
- package/dist/src/DetailView.stories.d.ts.map +1 -1
- package/dist/src/DiffView.d.ts +24 -0
- package/dist/src/DiffView.d.ts.map +1 -0
- package/dist/src/FieldChangeItem.d.ts +21 -0
- package/dist/src/FieldChangeItem.d.ts.map +1 -0
- package/dist/src/HeaderHighlight.d.ts +18 -0
- package/dist/src/HeaderHighlight.d.ts.map +1 -0
- package/dist/src/InlineCreateRelated.d.ts +32 -0
- package/dist/src/InlineCreateRelated.d.ts.map +1 -0
- package/dist/src/MentionAutocomplete.d.ts +43 -0
- package/dist/src/MentionAutocomplete.d.ts.map +1 -0
- package/dist/src/PointInTimeRestore.d.ts +28 -0
- package/dist/src/PointInTimeRestore.d.ts.map +1 -0
- package/dist/src/ReactionPicker.d.ts +25 -0
- package/dist/src/ReactionPicker.d.ts.map +1 -0
- package/dist/src/RecordActivityTimeline.d.ts +49 -0
- package/dist/src/RecordActivityTimeline.d.ts.map +1 -0
- package/dist/src/RecordChatterPanel.d.ts +48 -0
- package/dist/src/RecordChatterPanel.d.ts.map +1 -0
- package/dist/src/RecordComments.d.ts +20 -0
- package/dist/src/RecordComments.d.ts.map +1 -0
- package/dist/src/RecordNavigationEnhanced.d.ts +18 -0
- package/dist/src/RecordNavigationEnhanced.d.ts.map +1 -0
- package/dist/src/RelatedList.d.ts +20 -0
- package/dist/src/RelatedList.d.ts.map +1 -1
- package/dist/src/RelationshipGraph.d.ts +23 -0
- package/dist/src/RelationshipGraph.d.ts.map +1 -0
- package/dist/src/RichTextCommentInput.d.ts +24 -0
- package/dist/src/RichTextCommentInput.d.ts.map +1 -0
- package/dist/src/SectionGroup.d.ts +21 -0
- package/dist/src/SectionGroup.d.ts.map +1 -0
- package/dist/src/SubscriptionToggle.d.ts +22 -0
- package/dist/src/SubscriptionToggle.d.ts.map +1 -0
- package/dist/src/ThreadedReplies.d.ts +26 -0
- package/dist/src/ThreadedReplies.d.ts.map +1 -0
- package/dist/src/autoLayout.d.ts +34 -0
- package/dist/src/autoLayout.d.ts.map +1 -0
- package/dist/src/index.d.ts +40 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/useDetailTranslation.d.ts +34 -0
- package/dist/src/useDetailTranslation.d.ts.map +1 -0
- package/package.json +8 -7
- package/src/ActivityTimeline.tsx +184 -0
- package/src/CommentAttachment.tsx +192 -0
- package/src/CommentInput.tsx +81 -0
- package/src/DetailSection.tsx +81 -10
- package/src/DetailView.stories.tsx +76 -0
- package/src/DetailView.tsx +519 -66
- package/src/DiffView.tsx +231 -0
- package/src/FieldChangeItem.tsx +46 -0
- package/src/HeaderHighlight.tsx +67 -0
- package/src/InlineCreateRelated.tsx +291 -0
- package/src/MentionAutocomplete.tsx +123 -0
- package/src/PointInTimeRestore.tsx +261 -0
- package/src/ReactionPicker.tsx +106 -0
- package/src/RecordActivityTimeline.tsx +429 -0
- package/src/RecordChatterPanel.tsx +202 -0
- package/src/RecordComments.tsx +215 -0
- package/src/RecordNavigationEnhanced.tsx +211 -0
- package/src/RelatedList.tsx +314 -19
- package/src/RelationshipGraph.tsx +286 -0
- package/src/RichTextCommentInput.tsx +348 -0
- package/src/SectionGroup.tsx +101 -0
- package/src/SubscriptionToggle.tsx +60 -0
- package/src/ThreadedReplies.tsx +161 -0
- package/src/__tests__/ActivityTimeline.test.tsx +119 -0
- package/src/__tests__/ActivityTimelineFiltering.test.tsx +143 -0
- package/src/__tests__/CommentInput.test.tsx +57 -0
- package/src/__tests__/DetailSection.test.tsx +320 -0
- package/src/__tests__/DetailView.test.tsx +415 -1
- package/src/__tests__/FieldChangeItem.test.tsx +119 -0
- package/src/__tests__/HeaderHighlight.test.tsx +68 -0
- package/src/__tests__/MentionAutocomplete.test.tsx +97 -0
- package/src/__tests__/ReactionPicker.test.tsx +113 -0
- package/src/__tests__/RecordActivityTimeline.test.tsx +395 -0
- package/src/__tests__/RecordChatterPanel.test.tsx +227 -0
- package/src/__tests__/RecordComments.test.tsx +96 -0
- package/src/__tests__/RecordCommentsPinSearch.test.tsx +133 -0
- package/src/__tests__/RelatedList.test.tsx +160 -0
- package/src/__tests__/SectionGroup.test.tsx +101 -0
- package/src/__tests__/SubscriptionToggle.test.tsx +84 -0
- package/src/__tests__/ThreadedReplies.test.tsx +212 -0
- package/src/__tests__/autoLayout.test.ts +184 -0
- package/src/__tests__/phase12-features.test.tsx +583 -0
- package/src/__tests__/roadmap-features.test.tsx +478 -0
- package/src/autoLayout.ts +111 -0
- package/src/index.tsx +50 -0
- package/src/useDetailTranslation.ts +114 -0
|
@@ -0,0 +1,123 @@
|
|
|
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 } from '@object-ui/components';
|
|
11
|
+
import type { Mention } from '@object-ui/types';
|
|
12
|
+
|
|
13
|
+
export interface MentionSuggestionItem {
|
|
14
|
+
/** Entity ID */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Display name */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Avatar URL */
|
|
19
|
+
avatarUrl?: string;
|
|
20
|
+
/** Entity type */
|
|
21
|
+
type: 'user' | 'team' | 'group';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MentionAutocompleteProps {
|
|
25
|
+
/** Search query (text after @) */
|
|
26
|
+
query: string;
|
|
27
|
+
/** Available suggestions */
|
|
28
|
+
suggestions: MentionSuggestionItem[];
|
|
29
|
+
/** Called when a suggestion is selected */
|
|
30
|
+
onSelect: (item: MentionSuggestionItem) => void;
|
|
31
|
+
/** Whether the dropdown is visible */
|
|
32
|
+
visible?: boolean;
|
|
33
|
+
/** Active/highlighted index */
|
|
34
|
+
activeIndex?: number;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* MentionAutocomplete โ Dropdown for @mention autocomplete.
|
|
40
|
+
* Filters suggestions by query and renders a selectable list.
|
|
41
|
+
* Produces MentionSchema data on selection.
|
|
42
|
+
*/
|
|
43
|
+
export const MentionAutocomplete: React.FC<MentionAutocompleteProps> = ({
|
|
44
|
+
query,
|
|
45
|
+
suggestions,
|
|
46
|
+
onSelect,
|
|
47
|
+
visible = true,
|
|
48
|
+
activeIndex = 0,
|
|
49
|
+
className,
|
|
50
|
+
}) => {
|
|
51
|
+
const filtered = React.useMemo(() => {
|
|
52
|
+
if (!query) return suggestions;
|
|
53
|
+
const q = query.toLowerCase();
|
|
54
|
+
return suggestions.filter(
|
|
55
|
+
(s) => s.name.toLowerCase().includes(q) || s.id.toLowerCase().includes(q),
|
|
56
|
+
);
|
|
57
|
+
}, [query, suggestions]);
|
|
58
|
+
|
|
59
|
+
if (!visible || filtered.length === 0) return null;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={cn(
|
|
64
|
+
'bg-popover border rounded-md shadow-md z-50 max-h-48 overflow-y-auto w-56',
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
role="listbox"
|
|
68
|
+
aria-label="Mention suggestions"
|
|
69
|
+
>
|
|
70
|
+
{filtered.map((item, index) => (
|
|
71
|
+
<button
|
|
72
|
+
key={item.id}
|
|
73
|
+
type="button"
|
|
74
|
+
role="option"
|
|
75
|
+
aria-selected={index === activeIndex}
|
|
76
|
+
className={cn(
|
|
77
|
+
'w-full text-left px-3 py-1.5 text-sm flex items-center gap-2 hover:bg-accent transition-colors',
|
|
78
|
+
index === activeIndex && 'bg-accent',
|
|
79
|
+
)}
|
|
80
|
+
onMouseDown={(e) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
onSelect(item);
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{item.avatarUrl ? (
|
|
86
|
+
<img
|
|
87
|
+
src={item.avatarUrl}
|
|
88
|
+
alt={item.name}
|
|
89
|
+
className="h-5 w-5 rounded-full object-cover"
|
|
90
|
+
/>
|
|
91
|
+
) : (
|
|
92
|
+
<div className="h-5 w-5 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium">
|
|
93
|
+
{item.name.charAt(0).toUpperCase()}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
<div className="flex-1 min-w-0">
|
|
97
|
+
<span className="truncate">{item.name}</span>
|
|
98
|
+
{item.type !== 'user' && (
|
|
99
|
+
<span className="ml-1 text-xs text-muted-foreground capitalize">({item.type})</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</button>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Helper to create a Mention object from a suggestion item.
|
|
110
|
+
*/
|
|
111
|
+
export function createMentionFromSuggestion(
|
|
112
|
+
item: MentionSuggestionItem,
|
|
113
|
+
offset: number,
|
|
114
|
+
length: number,
|
|
115
|
+
): Mention {
|
|
116
|
+
return {
|
|
117
|
+
type: item.type,
|
|
118
|
+
id: item.id,
|
|
119
|
+
name: item.name,
|
|
120
|
+
offset,
|
|
121
|
+
length,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -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
|
+
};
|