@object-ui/plugin-view 3.1.5 → 3.3.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/CHANGELOG.md +34 -0
- package/README.md +21 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2716 -3140
- package/dist/index.umd.cjs +1 -10
- package/dist/{plugin-view → packages/plugin-view}/src/ObjectView.d.ts +2 -0
- package/dist/packages/plugin-view/src/config/view-config-schema.d.ts +20 -0
- package/dist/packages/plugin-view/src/config/view-config-utils.d.ts +58 -0
- package/dist/{plugin-view → packages/plugin-view}/src/index.d.ts +4 -0
- package/package.json +42 -10
- package/.turbo/turbo-build.log +0 -34
- package/src/FilterUI.tsx +0 -350
- package/src/ObjectView.tsx +0 -1109
- package/src/SharedViewLink.tsx +0 -199
- package/src/SortUI.tsx +0 -210
- package/src/ViewSwitcher.tsx +0 -379
- package/src/ViewTabBar.tsx +0 -656
- package/src/__tests__/FilterUI.test.tsx +0 -641
- package/src/__tests__/ObjectView.test.tsx +0 -705
- package/src/__tests__/SharedViewLinkPassword.test.tsx +0 -172
- package/src/__tests__/SortUI.test.tsx +0 -380
- package/src/__tests__/ViewTabBar.test.tsx +0 -710
- package/src/__tests__/config-sync-integration.test.tsx +0 -588
- package/src/__tests__/toolbar-consistency.test.tsx +0 -755
- package/src/index.tsx +0 -197
- package/tsconfig.json +0 -8
- package/vite.config.ts +0 -44
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
- /package/dist/{plugin-view → packages/plugin-view}/src/FilterUI.d.ts +0 -0
- /package/dist/{plugin-view → packages/plugin-view}/src/SharedViewLink.d.ts +0 -0
- /package/dist/{plugin-view → packages/plugin-view}/src/SortUI.d.ts +0 -0
- /package/dist/{plugin-view → packages/plugin-view}/src/ViewSwitcher.d.ts +0 -0
- /package/dist/{plugin-view → packages/plugin-view}/src/ViewTabBar.d.ts +0 -0
package/src/SharedViewLink.tsx
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
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
|
-
Badge,
|
|
14
|
-
Input,
|
|
15
|
-
Popover,
|
|
16
|
-
PopoverContent,
|
|
17
|
-
PopoverTrigger,
|
|
18
|
-
} from '@object-ui/components';
|
|
19
|
-
import { Share2, Copy, Check, Lock, Calendar } from 'lucide-react';
|
|
20
|
-
|
|
21
|
-
export interface SharedViewLinkProps {
|
|
22
|
-
/** The object name used in the share URL path */
|
|
23
|
-
objectName: string;
|
|
24
|
-
/** Optional view identifier; defaults to "default" */
|
|
25
|
-
viewId?: string;
|
|
26
|
-
/** Base URL for the shareable link (defaults to window.location.origin) */
|
|
27
|
-
baseUrl?: string;
|
|
28
|
-
/** Callback fired after a share URL is generated */
|
|
29
|
-
onShare?: (shareUrl: string, options?: { password?: string; expiresAt?: string }) => void;
|
|
30
|
-
className?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function generateToken(): string {
|
|
34
|
-
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
35
|
-
return crypto.randomUUID();
|
|
36
|
-
}
|
|
37
|
-
// Fallback for environments without crypto.randomUUID
|
|
38
|
-
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
39
|
-
const bytes = new Uint8Array(16);
|
|
40
|
-
crypto.getRandomValues(bytes);
|
|
41
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
42
|
-
}
|
|
43
|
-
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function buildShareUrl(baseUrl: string, objectName: string, viewId: string, token: string): string {
|
|
47
|
-
return `${baseUrl}/share/${objectName}/${viewId}?mode=readonly&token=${token}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const SharedViewLink: React.FC<SharedViewLinkProps> = ({
|
|
51
|
-
objectName,
|
|
52
|
-
viewId = 'default',
|
|
53
|
-
baseUrl,
|
|
54
|
-
onShare,
|
|
55
|
-
className,
|
|
56
|
-
}) => {
|
|
57
|
-
const [shareUrl, setShareUrl] = React.useState<string | null>(null);
|
|
58
|
-
const [copied, setCopied] = React.useState(false);
|
|
59
|
-
const [open, setOpen] = React.useState(false);
|
|
60
|
-
const [password, setPassword] = React.useState('');
|
|
61
|
-
const [expiresIn, setExpiresIn] = React.useState('');
|
|
62
|
-
|
|
63
|
-
const resolvedBaseUrl = baseUrl ?? (typeof window !== 'undefined' ? window.location.origin : '');
|
|
64
|
-
|
|
65
|
-
const handleGenerateLink = React.useCallback(() => {
|
|
66
|
-
const token = generateToken();
|
|
67
|
-
const url = buildShareUrl(resolvedBaseUrl, objectName, viewId, token);
|
|
68
|
-
setShareUrl(url);
|
|
69
|
-
setCopied(false);
|
|
70
|
-
const expiresAt = expiresIn ? new Date(Date.now() + parseInt(expiresIn, 10) * 86400000).toISOString() : undefined;
|
|
71
|
-
onShare?.(url, { password: password || undefined, expiresAt });
|
|
72
|
-
}, [resolvedBaseUrl, objectName, viewId, onShare, password, expiresIn]);
|
|
73
|
-
|
|
74
|
-
const handleCopy = React.useCallback(async () => {
|
|
75
|
-
if (!shareUrl) return;
|
|
76
|
-
try {
|
|
77
|
-
await navigator.clipboard.writeText(shareUrl);
|
|
78
|
-
setCopied(true);
|
|
79
|
-
setTimeout(() => setCopied(false), 2000);
|
|
80
|
-
} catch {
|
|
81
|
-
// Fallback for environments without clipboard API
|
|
82
|
-
const textarea = document.createElement('textarea');
|
|
83
|
-
textarea.value = shareUrl;
|
|
84
|
-
document.body.appendChild(textarea);
|
|
85
|
-
textarea.select();
|
|
86
|
-
document.execCommand('copy');
|
|
87
|
-
document.body.removeChild(textarea);
|
|
88
|
-
setCopied(true);
|
|
89
|
-
setTimeout(() => setCopied(false), 2000);
|
|
90
|
-
}
|
|
91
|
-
}, [shareUrl]);
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
95
|
-
<PopoverTrigger asChild>
|
|
96
|
-
<Button variant="outline" size="sm" className={cn('gap-2', className)}>
|
|
97
|
-
<Share2 className="h-4 w-4" />
|
|
98
|
-
Share
|
|
99
|
-
</Button>
|
|
100
|
-
</PopoverTrigger>
|
|
101
|
-
<PopoverContent className="w-96 space-y-4" align="end">
|
|
102
|
-
<div className="space-y-2">
|
|
103
|
-
<div className="flex items-center justify-between">
|
|
104
|
-
<h4 className="text-sm font-medium">Share View</h4>
|
|
105
|
-
<Badge variant="secondary" className="text-xs">
|
|
106
|
-
Read-only
|
|
107
|
-
</Badge>
|
|
108
|
-
</div>
|
|
109
|
-
<p className="text-xs text-muted-foreground">
|
|
110
|
-
Generate a public link to share this view. Recipients can view data without logging in.
|
|
111
|
-
</p>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
{!shareUrl ? (
|
|
115
|
-
<div className="space-y-3">
|
|
116
|
-
{/* Password protection */}
|
|
117
|
-
<div className="space-y-1.5">
|
|
118
|
-
<label className="flex items-center gap-1.5 text-xs font-medium text-foreground">
|
|
119
|
-
<Lock className="h-3.5 w-3.5" />
|
|
120
|
-
Password protection (optional)
|
|
121
|
-
</label>
|
|
122
|
-
<Input
|
|
123
|
-
type="password"
|
|
124
|
-
value={password}
|
|
125
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
126
|
-
placeholder="Enter password..."
|
|
127
|
-
className="h-8 text-xs"
|
|
128
|
-
/>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
{/* Expiration */}
|
|
132
|
-
<div className="space-y-1.5">
|
|
133
|
-
<label className="flex items-center gap-1.5 text-xs font-medium text-foreground">
|
|
134
|
-
<Calendar className="h-3.5 w-3.5" />
|
|
135
|
-
Expires after (optional)
|
|
136
|
-
</label>
|
|
137
|
-
<select
|
|
138
|
-
value={expiresIn}
|
|
139
|
-
onChange={(e) => setExpiresIn(e.target.value)}
|
|
140
|
-
className="w-full h-8 text-xs rounded-md border border-input bg-background px-3"
|
|
141
|
-
>
|
|
142
|
-
<option value="">Never</option>
|
|
143
|
-
<option value="1">1 day</option>
|
|
144
|
-
<option value="7">7 days</option>
|
|
145
|
-
<option value="30">30 days</option>
|
|
146
|
-
<option value="90">90 days</option>
|
|
147
|
-
</select>
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
<Button onClick={handleGenerateLink} className="w-full gap-2" size="sm">
|
|
151
|
-
<Share2 className="h-4 w-4" />
|
|
152
|
-
Generate Link
|
|
153
|
-
</Button>
|
|
154
|
-
</div>
|
|
155
|
-
) : (
|
|
156
|
-
<>
|
|
157
|
-
<div className="flex items-center gap-2">
|
|
158
|
-
<Input
|
|
159
|
-
value={shareUrl}
|
|
160
|
-
readOnly
|
|
161
|
-
className="h-8 text-xs"
|
|
162
|
-
onClick={(e) => (e.target as HTMLInputElement).select()}
|
|
163
|
-
/>
|
|
164
|
-
<Button
|
|
165
|
-
variant="outline"
|
|
166
|
-
size="sm"
|
|
167
|
-
onClick={handleCopy}
|
|
168
|
-
className="shrink-0 gap-1"
|
|
169
|
-
>
|
|
170
|
-
{copied ? (
|
|
171
|
-
<Check className="h-4 w-4 text-green-500" />
|
|
172
|
-
) : (
|
|
173
|
-
<Copy className="h-4 w-4" />
|
|
174
|
-
)}
|
|
175
|
-
</Button>
|
|
176
|
-
</div>
|
|
177
|
-
{/* Share options indicators */}
|
|
178
|
-
{(password || expiresIn) && (
|
|
179
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
180
|
-
{password && (
|
|
181
|
-
<Badge variant="outline" className="text-xs gap-1">
|
|
182
|
-
<Lock className="h-3 w-3" />
|
|
183
|
-
Password protected
|
|
184
|
-
</Badge>
|
|
185
|
-
)}
|
|
186
|
-
{expiresIn && (
|
|
187
|
-
<Badge variant="outline" className="text-xs gap-1">
|
|
188
|
-
<Calendar className="h-3 w-3" />
|
|
189
|
-
Expires in {expiresIn} day{expiresIn !== '1' ? 's' : ''}
|
|
190
|
-
</Badge>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
194
|
-
</>
|
|
195
|
-
)}
|
|
196
|
-
</PopoverContent>
|
|
197
|
-
</Popover>
|
|
198
|
-
);
|
|
199
|
-
};
|
package/src/SortUI.tsx
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
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
|
-
Select,
|
|
14
|
-
SelectContent,
|
|
15
|
-
SelectItem,
|
|
16
|
-
SelectTrigger,
|
|
17
|
-
SelectValue,
|
|
18
|
-
SortBuilder,
|
|
19
|
-
} from '@object-ui/components';
|
|
20
|
-
import { cva } from 'class-variance-authority';
|
|
21
|
-
import { ArrowDown, ArrowUp } from 'lucide-react';
|
|
22
|
-
import type { SortItem } from '@object-ui/components';
|
|
23
|
-
import type { SortUISchema } from '@object-ui/types';
|
|
24
|
-
|
|
25
|
-
export type SortUIProps = {
|
|
26
|
-
schema: SortUISchema;
|
|
27
|
-
className?: string;
|
|
28
|
-
onChange?: (sort: SortUISchema['sort']) => void;
|
|
29
|
-
[key: string]: any;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
type SortEntry = {
|
|
33
|
-
field: string;
|
|
34
|
-
direction: 'asc' | 'desc';
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const sortContainerVariants = cva('', {
|
|
38
|
-
variants: {
|
|
39
|
-
variant: {
|
|
40
|
-
buttons: 'flex flex-wrap gap-2',
|
|
41
|
-
dropdown: 'flex flex-wrap items-center gap-3',
|
|
42
|
-
builder: 'space-y-3',
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
defaultVariants: {
|
|
46
|
-
variant: 'dropdown',
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const toSortEntries = (sort?: SortUISchema['sort']): SortEntry[] => {
|
|
51
|
-
if (!sort) return [];
|
|
52
|
-
return sort.map(item => ({
|
|
53
|
-
field: item.field,
|
|
54
|
-
direction: item.direction,
|
|
55
|
-
}));
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const toSortItems = (sort: SortEntry[]): SortItem[] => {
|
|
59
|
-
return sort.map(item => ({
|
|
60
|
-
id: `${item.field}-${item.direction}`,
|
|
61
|
-
field: item.field,
|
|
62
|
-
order: item.direction,
|
|
63
|
-
}));
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const toSortEntriesFromItems = (items: SortItem[]): SortEntry[] => {
|
|
67
|
-
return items
|
|
68
|
-
.filter(item => item.field)
|
|
69
|
-
.map(item => ({
|
|
70
|
-
field: item.field,
|
|
71
|
-
direction: item.order,
|
|
72
|
-
}));
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export const SortUI: React.FC<SortUIProps> = ({
|
|
76
|
-
schema,
|
|
77
|
-
className,
|
|
78
|
-
onChange,
|
|
79
|
-
}) => {
|
|
80
|
-
const [sortState, setSortState] = React.useState<SortEntry[]>(() => toSortEntries(schema.sort));
|
|
81
|
-
const [builderItems, setBuilderItems] = React.useState<SortItem[]>(() => toSortItems(toSortEntries(schema.sort)));
|
|
82
|
-
|
|
83
|
-
React.useEffect(() => {
|
|
84
|
-
const entries = toSortEntries(schema.sort);
|
|
85
|
-
setSortState(entries);
|
|
86
|
-
setBuilderItems(toSortItems(entries));
|
|
87
|
-
}, [schema.sort]);
|
|
88
|
-
|
|
89
|
-
const notifyChange = React.useCallback((nextSort: SortEntry[]) => {
|
|
90
|
-
setSortState(nextSort);
|
|
91
|
-
onChange?.(nextSort);
|
|
92
|
-
|
|
93
|
-
if (schema.onChange && typeof window !== 'undefined') {
|
|
94
|
-
window.dispatchEvent(
|
|
95
|
-
new CustomEvent(schema.onChange, {
|
|
96
|
-
detail: { sort: nextSort },
|
|
97
|
-
})
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
}, [onChange, schema.onChange]);
|
|
101
|
-
|
|
102
|
-
const handleToggle = React.useCallback((field: string) => {
|
|
103
|
-
const existing = sortState.find(item => item.field === field);
|
|
104
|
-
const multiple = Boolean(schema.multiple);
|
|
105
|
-
|
|
106
|
-
if (!existing) {
|
|
107
|
-
const next = multiple
|
|
108
|
-
? [...sortState, { field, direction: 'asc' as const }]
|
|
109
|
-
: [{ field, direction: 'asc' as const }];
|
|
110
|
-
notifyChange(next);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (existing.direction === 'asc') {
|
|
115
|
-
const next = sortState.map(item =>
|
|
116
|
-
item.field === field ? { ...item, direction: 'desc' as const } : item
|
|
117
|
-
);
|
|
118
|
-
notifyChange(next);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const next = sortState.filter(item => item.field !== field);
|
|
123
|
-
notifyChange(next);
|
|
124
|
-
}, [notifyChange, schema.multiple, sortState]);
|
|
125
|
-
|
|
126
|
-
const variant = schema.variant || 'dropdown';
|
|
127
|
-
|
|
128
|
-
if (variant === 'buttons') {
|
|
129
|
-
return (
|
|
130
|
-
<div className={cn(sortContainerVariants({ variant: 'buttons' }), className)}>
|
|
131
|
-
{schema.fields.map(field => {
|
|
132
|
-
const current = sortState.find(item => item.field === field.field);
|
|
133
|
-
const Icon = current?.direction === 'asc' ? ArrowUp : ArrowDown;
|
|
134
|
-
return (
|
|
135
|
-
<Button
|
|
136
|
-
key={field.field}
|
|
137
|
-
type="button"
|
|
138
|
-
variant={current ? 'secondary' : 'outline'}
|
|
139
|
-
size="sm"
|
|
140
|
-
onClick={() => handleToggle(field.field)}
|
|
141
|
-
className="gap-2"
|
|
142
|
-
>
|
|
143
|
-
<span>{field.label || field.field}</span>
|
|
144
|
-
{current && <Icon className="h-3.5 w-3.5" />}
|
|
145
|
-
</Button>
|
|
146
|
-
);
|
|
147
|
-
})}
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (schema.multiple) {
|
|
153
|
-
return (
|
|
154
|
-
<div className={cn(sortContainerVariants({ variant: 'builder' }), className)}>
|
|
155
|
-
<SortBuilder
|
|
156
|
-
fields={schema.fields.map(field => ({ value: field.field, label: field.label || field.field }))}
|
|
157
|
-
value={builderItems}
|
|
158
|
-
onChange={(items) => {
|
|
159
|
-
setBuilderItems(items);
|
|
160
|
-
notifyChange(toSortEntriesFromItems(items));
|
|
161
|
-
}}
|
|
162
|
-
/>
|
|
163
|
-
</div>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const singleSort = sortState[0];
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<div className={cn(sortContainerVariants({ variant: 'dropdown' }), className)}>
|
|
171
|
-
<Select
|
|
172
|
-
value={singleSort?.field || ''}
|
|
173
|
-
onValueChange={(value) => {
|
|
174
|
-
if (!value) {
|
|
175
|
-
notifyChange([]);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
notifyChange([{ field: value, direction: singleSort?.direction || 'asc' }]);
|
|
179
|
-
}}
|
|
180
|
-
>
|
|
181
|
-
<SelectTrigger className="w-56">
|
|
182
|
-
<SelectValue placeholder="Select field" />
|
|
183
|
-
</SelectTrigger>
|
|
184
|
-
<SelectContent>
|
|
185
|
-
{schema.fields.map(field => (
|
|
186
|
-
<SelectItem key={field.field} value={field.field}>
|
|
187
|
-
{field.label || field.field}
|
|
188
|
-
</SelectItem>
|
|
189
|
-
))}
|
|
190
|
-
</SelectContent>
|
|
191
|
-
</Select>
|
|
192
|
-
|
|
193
|
-
<Select
|
|
194
|
-
value={singleSort?.direction || 'asc'}
|
|
195
|
-
onValueChange={(value) => {
|
|
196
|
-
if (!singleSort?.field) return;
|
|
197
|
-
notifyChange([{ field: singleSort.field, direction: value as 'asc' | 'desc' }]);
|
|
198
|
-
}}
|
|
199
|
-
>
|
|
200
|
-
<SelectTrigger className="w-36">
|
|
201
|
-
<SelectValue />
|
|
202
|
-
</SelectTrigger>
|
|
203
|
-
<SelectContent>
|
|
204
|
-
<SelectItem value="asc">Ascending</SelectItem>
|
|
205
|
-
<SelectItem value="desc">Descending</SelectItem>
|
|
206
|
-
</SelectContent>
|
|
207
|
-
</Select>
|
|
208
|
-
</div>
|
|
209
|
-
);
|
|
210
|
-
};
|