@object-ui/plugin-view 3.3.0 → 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.
@@ -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
- };