@mdguggenbichler/slugbase-core 0.0.28 → 0.0.30
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/backend/dist/routes/users.d.ts.map +1 -1
- package/backend/dist/routes/users.js +15 -1
- package/backend/dist/routes/users.js.map +1 -1
- package/backend/dist/utils/tenant.d.ts.map +1 -1
- package/backend/dist/utils/tenant.js +1 -4
- package/backend/dist/utils/tenant.js.map +1 -1
- package/frontend/src/components/AppSidebar.tsx +1 -1
- package/frontend/src/components/UserDropdown.tsx +1 -1
- package/frontend/src/components/sharing/ShareResourceDialog.tsx +153 -137
- package/frontend/src/components/ui/Button.tsx +20 -13
- package/frontend/src/components/ui/SharingField.tsx +4 -5
- package/frontend/src/locales/en.json +4 -1
- package/frontend/src/pages/Bookmarks.tsx +11 -9
- package/frontend/src/pages/Folders.tsx +1 -3
- package/frontend/src/pages/Profile.tsx +1 -1
- package/frontend/src/pages/Tags.tsx +1 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAuJxB,eAAe,MAAM,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { queryOne, execute } from '../db/index.js';
|
|
2
|
+
import { query, queryOne, execute } from '../db/index.js';
|
|
3
3
|
import { requireAuth } from '../middleware/auth.js';
|
|
4
4
|
import { validateEmail, normalizeEmail, validateLength, sanitizeString } from '../utils/validation.js';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
@@ -7,6 +7,20 @@ import crypto from 'crypto';
|
|
|
7
7
|
import { sendEmailVerificationEmail } from '../utils/email.js';
|
|
8
8
|
const router = Router();
|
|
9
9
|
router.use(requireAuth());
|
|
10
|
+
// List users for sharing (same tenant/org). No admin required. Used by sharing modal.
|
|
11
|
+
// Core: users table has no tenant_id; returns all users except current (self-hosted single-tenant).
|
|
12
|
+
router.get('/for-sharing', async (req, res) => {
|
|
13
|
+
const authReq = req;
|
|
14
|
+
try {
|
|
15
|
+
const userId = authReq.user.id;
|
|
16
|
+
const rows = await query('SELECT id, name, email FROM users WHERE id != ? ORDER BY name ASC, email ASC', [userId]);
|
|
17
|
+
const list = Array.isArray(rows) ? rows : (rows ? [rows] : []);
|
|
18
|
+
res.json(list);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
res.status(500).json({ error: error.message });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
10
24
|
// Get current user profile
|
|
11
25
|
router.get('/me', async (req, res) => {
|
|
12
26
|
const authReq = req;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/utils/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/utils/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAGhD"}
|
|
@@ -4,9 +4,6 @@ export function getDefaultTenantId() {
|
|
|
4
4
|
}
|
|
5
5
|
export function getTenantId(req) {
|
|
6
6
|
const tenantId = req.tenantId;
|
|
7
|
-
|
|
8
|
-
throw new Error('Missing tenantId on request');
|
|
9
|
-
}
|
|
10
|
-
return tenantId;
|
|
7
|
+
return tenantId ?? DEFAULT_TENANT_ID;
|
|
11
8
|
}
|
|
12
9
|
//# sourceMappingURL=tenant.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.js","sourceRoot":"","sources":["../../src/utils/tenant.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C,MAAM,UAAU,kBAAkB;IAChC,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,QAAQ,GAAI,GAAuC,CAAC,QAAQ,CAAC;IACnE,
|
|
1
|
+
{"version":3,"file":"tenant.js","sourceRoot":"","sources":["../../src/utils/tenant.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C,MAAM,UAAU,kBAAkB;IAChC,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,QAAQ,GAAI,GAAuC,CAAC,QAAQ,CAAC;IACnE,OAAO,QAAQ,IAAI,iBAAiB,CAAC;AACvC,CAAC"}
|
|
@@ -69,7 +69,7 @@ export default function AppSidebar({ user, version = null }: AppSidebarProps) {
|
|
|
69
69
|
pathname === rootActivePath + '/' ||
|
|
70
70
|
pathname === (pathBaseForActive || '/');
|
|
71
71
|
|
|
72
|
-
const showAdmin = user?.is_admin;
|
|
72
|
+
const showAdmin = !!(user?.is_admin);
|
|
73
73
|
const [adminOpen, setAdminOpen] = useState(() => {
|
|
74
74
|
if (typeof window === 'undefined') return true;
|
|
75
75
|
const stored = localStorage.getItem(SIDEBAR_ADMIN_OPEN_KEY);
|
|
@@ -31,7 +31,7 @@ export default function UserDropdown({ user }: UserDropdownProps) {
|
|
|
31
31
|
const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
|
|
32
32
|
const { logout } = useAuth();
|
|
33
33
|
|
|
34
|
-
const showAdmin = user?.is_admin;
|
|
34
|
+
const showAdmin = !!(user?.is_admin);
|
|
35
35
|
|
|
36
36
|
if (!user) return null;
|
|
37
37
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { useAuth } from '../../contexts/AuthContext';
|
|
4
3
|
import api from '../../api/client';
|
|
5
4
|
import {
|
|
6
5
|
Dialog,
|
|
@@ -12,19 +11,10 @@ import {
|
|
|
12
11
|
import { Separator } from '../ui/separator';
|
|
13
12
|
import { Input } from '../ui/input';
|
|
14
13
|
import { ScrollArea } from '../ui/scroll-area';
|
|
15
|
-
import { Popover, PopoverContent, PopoverAnchor, PopoverTrigger } from '../ui/popover';
|
|
16
|
-
import {
|
|
17
|
-
Command,
|
|
18
|
-
CommandEmpty,
|
|
19
|
-
CommandGroup,
|
|
20
|
-
CommandInput,
|
|
21
|
-
CommandItem,
|
|
22
|
-
CommandList,
|
|
23
|
-
} from '../ui/command';
|
|
24
14
|
import Button from '../ui/Button';
|
|
25
15
|
import Tooltip from '../ui/Tooltip';
|
|
26
16
|
import { useToast } from '../ui/Toast';
|
|
27
|
-
import {
|
|
17
|
+
import { User, Users, X } from 'lucide-react';
|
|
28
18
|
import { cn } from '@/lib/utils';
|
|
29
19
|
|
|
30
20
|
interface SharedUser {
|
|
@@ -56,7 +46,6 @@ export default function ShareResourceDialog({
|
|
|
56
46
|
onSuccess,
|
|
57
47
|
}: ShareResourceDialogProps) {
|
|
58
48
|
const { t } = useTranslation();
|
|
59
|
-
const { user } = useAuth();
|
|
60
49
|
const { showToast } = useToast();
|
|
61
50
|
const [loading, setLoading] = useState(true);
|
|
62
51
|
const [saving, setSaving] = useState(false);
|
|
@@ -67,17 +56,16 @@ export default function ShareResourceDialog({
|
|
|
67
56
|
const [allUsers, setAllUsers] = useState<SharedUser[]>([]);
|
|
68
57
|
const [teams, setTeams] = useState<SharedTeam[]>([]);
|
|
69
58
|
const [emailInput, setEmailInput] = useState('');
|
|
70
|
-
const [
|
|
71
|
-
const [teamsPopoverOpen, setTeamsPopoverOpen] = useState(false);
|
|
59
|
+
const [peopleDropdownOpen, setPeopleDropdownOpen] = useState(false);
|
|
72
60
|
const [activeTab, setActiveTab] = useState<'people' | 'teams'>('people');
|
|
73
61
|
|
|
62
|
+
const MIN_CHARS_FOR_USER_DROPDOWN = 3;
|
|
63
|
+
|
|
74
64
|
const allowShareToTeams = teams.length > 0;
|
|
75
65
|
const allowShareToUsers = true;
|
|
76
66
|
|
|
77
|
-
const
|
|
78
|
-
if (!resourceId
|
|
79
|
-
setLoading(true);
|
|
80
|
-
setError(null);
|
|
67
|
+
const refreshResourceOnly = useCallback(async () => {
|
|
68
|
+
if (!resourceId) return;
|
|
81
69
|
try {
|
|
82
70
|
const endpoint = resourceType === 'bookmark' ? `/bookmarks/${resourceId}` : `/folders/${resourceId}`;
|
|
83
71
|
const res = await api.get(endpoint);
|
|
@@ -85,35 +73,56 @@ export default function ShareResourceDialog({
|
|
|
85
73
|
setSharedUsers(data.shared_users ?? []);
|
|
86
74
|
setSharedTeams(data.shared_teams ?? []);
|
|
87
75
|
setResourceData(resourceType === 'folder' ? { name: data.name, icon: data.icon } : null);
|
|
88
|
-
} catch
|
|
89
|
-
|
|
90
|
-
setError(err.response?.data?.error || t('common.error'));
|
|
91
|
-
showToast(t('common.error'), 'error');
|
|
92
|
-
} finally {
|
|
93
|
-
setLoading(false);
|
|
76
|
+
} catch {
|
|
77
|
+
// ignore
|
|
94
78
|
}
|
|
95
|
-
}, [resourceId, resourceType
|
|
79
|
+
}, [resourceId, resourceType]);
|
|
96
80
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
81
|
+
const loadAll = useCallback(async () => {
|
|
82
|
+
if (!resourceId || !isOpen) return;
|
|
83
|
+
setLoading(true);
|
|
84
|
+
setError(null);
|
|
85
|
+
const endpoint = resourceType === 'bookmark' ? `/bookmarks/${resourceId}` : `/folders/${resourceId}`;
|
|
86
|
+
|
|
87
|
+
const [resourceResult, usersResult, teamsResult] = await Promise.allSettled([
|
|
88
|
+
api.get(endpoint),
|
|
89
|
+
(async () => {
|
|
90
|
+
try {
|
|
91
|
+
const res = await api.get('/users/for-sharing');
|
|
92
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
93
|
+
} catch (e: any) {
|
|
94
|
+
try {
|
|
95
|
+
const adminRes = await api.get('/admin/users');
|
|
96
|
+
const list = Array.isArray(adminRes.data) ? adminRes.data : [];
|
|
97
|
+
return list.filter((u: SharedUser) => u.id && u.email);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
})(),
|
|
103
|
+
api.get('/teams').then((r) => r.data).catch(() => []),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
if (resourceResult.status === 'fulfilled') {
|
|
107
|
+
const data = resourceResult.value.data;
|
|
108
|
+
setSharedUsers(data.shared_users ?? []);
|
|
109
|
+
setSharedTeams(data.shared_teams ?? []);
|
|
110
|
+
setResourceData(resourceType === 'folder' ? { name: data.name, icon: data.icon } : null);
|
|
111
|
+
} else {
|
|
112
|
+
setError(t('common.error'));
|
|
113
|
+
showToast(t('common.error'), 'error');
|
|
108
114
|
}
|
|
109
|
-
|
|
115
|
+
|
|
116
|
+
setAllUsers(usersResult.status === 'fulfilled' ? usersResult.value : []);
|
|
117
|
+
const teamsData = teamsResult.status === 'fulfilled' ? teamsResult.value : [];
|
|
118
|
+
setTeams(Array.isArray(teamsData) ? teamsData : teamsData != null ? [teamsData] : []);
|
|
119
|
+
|
|
120
|
+
setLoading(false);
|
|
121
|
+
}, [resourceId, resourceType, isOpen, t, showToast]);
|
|
110
122
|
|
|
111
123
|
useEffect(() => {
|
|
112
|
-
if (isOpen)
|
|
113
|
-
|
|
114
|
-
fetchUsersAndTeams();
|
|
115
|
-
}
|
|
116
|
-
}, [isOpen, fetchResource, fetchUsersAndTeams]);
|
|
124
|
+
if (isOpen) loadAll();
|
|
125
|
+
}, [isOpen, loadAll]);
|
|
117
126
|
|
|
118
127
|
async function updateShares(userIds: string[], teamIds: string[], shareAllTeams: boolean) {
|
|
119
128
|
setSaving(true);
|
|
@@ -130,16 +139,17 @@ export default function ShareResourceDialog({
|
|
|
130
139
|
payload.share_all_teams = shareAllTeams;
|
|
131
140
|
}
|
|
132
141
|
await api.put(endpoint, payload);
|
|
133
|
-
await
|
|
142
|
+
await refreshResourceOnly();
|
|
134
143
|
onSuccess();
|
|
135
144
|
showToast(t('common.success'), 'success');
|
|
136
145
|
} catch (err: any) {
|
|
137
146
|
const msg = err.response?.data?.error || t('common.error');
|
|
138
147
|
setError(msg);
|
|
139
148
|
showToast(msg, 'error');
|
|
140
|
-
} finally {
|
|
141
149
|
setSaving(false);
|
|
150
|
+
throw err;
|
|
142
151
|
}
|
|
152
|
+
setSaving(false);
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
function handleRemoveUser(userId: string) {
|
|
@@ -153,11 +163,19 @@ export default function ShareResourceDialog({
|
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
function handleAddUser(userId: string) {
|
|
166
|
+
if (sharedUsers.some((u) => u.id === userId)) return;
|
|
167
|
+
const userToAdd = allUsers.find((u) => u.id === userId);
|
|
156
168
|
const newUserIds = [...sharedUsers.map((u) => u.id), userId];
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
if (userToAdd) {
|
|
170
|
+
setSharedUsers((prev) => [...prev, userToAdd]);
|
|
171
|
+
}
|
|
172
|
+
setPeopleDropdownOpen(false);
|
|
160
173
|
setEmailInput('');
|
|
174
|
+
updateShares(newUserIds, sharedTeams.map((t) => t.id), false).catch(() => {
|
|
175
|
+
if (userToAdd) {
|
|
176
|
+
setSharedUsers((prev) => prev.filter((u) => u.id !== userId));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
function handleAddUserByEmail() {
|
|
@@ -172,17 +190,30 @@ export default function ShareResourceDialog({
|
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
function handleAddTeam(teamId: string) {
|
|
193
|
+
if (sharedTeams.some((t) => t.id === teamId)) return;
|
|
194
|
+
const teamToAdd = teams.find((t) => t.id === teamId);
|
|
175
195
|
const newTeamIds = [...sharedTeams.map((t) => t.id), teamId];
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
if (teamToAdd) {
|
|
197
|
+
setSharedTeams((prev) => [...prev, teamToAdd]);
|
|
198
|
+
}
|
|
199
|
+
updateShares(sharedUsers.map((u) => u.id), newTeamIds, false).catch(() => {
|
|
200
|
+
if (teamToAdd) {
|
|
201
|
+
setSharedTeams((prev) => prev.filter((t) => t.id !== teamId));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
179
204
|
}
|
|
180
205
|
|
|
206
|
+
const searchQuery = emailInput.trim().toLowerCase();
|
|
181
207
|
const filteredUsers = allUsers.filter((u) => {
|
|
182
|
-
if (!
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
if (!searchQuery) return false;
|
|
209
|
+
return (
|
|
210
|
+
u.email.toLowerCase().includes(searchQuery) ||
|
|
211
|
+
(u.name && u.name.toLowerCase().includes(searchQuery))
|
|
212
|
+
);
|
|
185
213
|
});
|
|
214
|
+
const usersAvailableToAdd = filteredUsers.filter((u) => !sharedUsers.some((su) => su.id === u.id));
|
|
215
|
+
const showUserDropdown =
|
|
216
|
+
searchQuery.length >= MIN_CHARS_FOR_USER_DROPDOWN && usersAvailableToAdd.length > 0;
|
|
186
217
|
|
|
187
218
|
const filteredTeams = teams.filter((t) => !sharedTeams.some((st) => st.id === t.id));
|
|
188
219
|
|
|
@@ -275,6 +306,11 @@ export default function ShareResourceDialog({
|
|
|
275
306
|
|
|
276
307
|
<div>
|
|
277
308
|
<h4 className="text-sm font-medium mb-3">{t('sharing.addAccess')}</h4>
|
|
309
|
+
{!loading && allUsers.length === 0 && teams.length === 0 && (
|
|
310
|
+
<p className="text-xs text-amber-600 dark:text-amber-500 mb-2" role="status">
|
|
311
|
+
{t('sharing.noUsersOrTeams')}
|
|
312
|
+
</p>
|
|
313
|
+
)}
|
|
278
314
|
{allowShareToTeams && allowShareToUsers ? (
|
|
279
315
|
<div className="flex gap-2 border-b mb-3">
|
|
280
316
|
<button
|
|
@@ -306,105 +342,85 @@ export default function ShareResourceDialog({
|
|
|
306
342
|
|
|
307
343
|
{allowShareToUsers && (activeTab === 'people' || !allowShareToTeams) && (
|
|
308
344
|
<div className="space-y-2">
|
|
309
|
-
<div className="
|
|
345
|
+
<div className="relative">
|
|
310
346
|
<Input
|
|
311
347
|
placeholder={t('admin.searchUsers')}
|
|
312
348
|
value={emailInput}
|
|
313
|
-
onChange={(e) =>
|
|
349
|
+
onChange={(e) => {
|
|
350
|
+
setEmailInput(e.target.value);
|
|
351
|
+
setPeopleDropdownOpen(true);
|
|
352
|
+
}}
|
|
353
|
+
onFocus={() => {
|
|
354
|
+
if (searchQuery.length >= MIN_CHARS_FOR_USER_DROPDOWN)
|
|
355
|
+
setPeopleDropdownOpen(true);
|
|
356
|
+
}}
|
|
357
|
+
onBlur={() => {
|
|
358
|
+
setTimeout(() => setPeopleDropdownOpen(false), 150);
|
|
359
|
+
}}
|
|
314
360
|
onKeyDown={(e) => {
|
|
315
361
|
if (e.key === 'Enter') {
|
|
316
362
|
e.preventDefault();
|
|
317
363
|
handleAddUserByEmail();
|
|
318
364
|
}
|
|
319
365
|
}}
|
|
320
|
-
className="
|
|
366
|
+
className="w-full"
|
|
367
|
+
autoComplete="off"
|
|
321
368
|
/>
|
|
322
|
-
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
onValueChange={setEmailInput}
|
|
347
|
-
/>
|
|
348
|
-
<CommandList>
|
|
349
|
-
<CommandEmpty>{t('common.noResults')}</CommandEmpty>
|
|
350
|
-
<CommandGroup>
|
|
351
|
-
{filteredUsers
|
|
352
|
-
.filter((u) => !sharedUsers.some((su) => su.id === u.id))
|
|
353
|
-
.map((u) => (
|
|
354
|
-
<CommandItem
|
|
355
|
-
key={u.id}
|
|
356
|
-
onSelect={() => handleAddUser(u.id)}
|
|
357
|
-
className="flex flex-col items-start gap-0.5"
|
|
358
|
-
>
|
|
359
|
-
<span className="font-medium">{u.name || u.email}</span>
|
|
360
|
-
{u.name && u.email && (
|
|
361
|
-
<span className="text-xs text-muted-foreground">{u.email}</span>
|
|
362
|
-
)}
|
|
363
|
-
</CommandItem>
|
|
364
|
-
))}
|
|
365
|
-
</CommandGroup>
|
|
366
|
-
</CommandList>
|
|
367
|
-
</Command>
|
|
368
|
-
</PopoverContent>
|
|
369
|
-
</Popover>
|
|
369
|
+
{peopleDropdownOpen && showUserDropdown && (
|
|
370
|
+
<div
|
|
371
|
+
className="absolute top-full left-0 right-0 z-10 mt-1 max-h-48 overflow-auto rounded-md border bg-popover shadow-md"
|
|
372
|
+
role="listbox"
|
|
373
|
+
>
|
|
374
|
+
{usersAvailableToAdd.map((u) => (
|
|
375
|
+
<button
|
|
376
|
+
key={u.id}
|
|
377
|
+
type="button"
|
|
378
|
+
role="option"
|
|
379
|
+
className="flex w-full flex-col items-start gap-0.5 px-3 py-2 text-left text-sm hover:bg-accent focus:bg-accent focus:outline-none"
|
|
380
|
+
onMouseDown={(e) => {
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
handleAddUser(u.id);
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
<span className="font-medium">{u.name || u.email}</span>
|
|
386
|
+
{u.name && u.email && (
|
|
387
|
+
<span className="text-xs text-muted-foreground">{u.email}</span>
|
|
388
|
+
)}
|
|
389
|
+
</button>
|
|
390
|
+
))}
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
370
393
|
</div>
|
|
371
|
-
<p className="text-xs text-muted-foreground">
|
|
394
|
+
<p className="text-xs text-muted-foreground">
|
|
395
|
+
{t('sharing.typeToSearchUsers', { count: MIN_CHARS_FOR_USER_DROPDOWN })}
|
|
396
|
+
</p>
|
|
372
397
|
</div>
|
|
373
398
|
)}
|
|
374
399
|
|
|
375
400
|
{allowShareToTeams && (activeTab === 'teams' || !allowShareToUsers) && (
|
|
376
|
-
<
|
|
377
|
-
<
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
>
|
|
400
|
-
{team.name}
|
|
401
|
-
</CommandItem>
|
|
402
|
-
))}
|
|
403
|
-
</CommandGroup>
|
|
404
|
-
</CommandList>
|
|
405
|
-
</Command>
|
|
406
|
-
</PopoverContent>
|
|
407
|
-
</Popover>
|
|
401
|
+
<div className="space-y-2">
|
|
402
|
+
<p className="text-xs text-muted-foreground">{t('sharing.selectTeamToAdd')}</p>
|
|
403
|
+
{filteredTeams.length === 0 ? (
|
|
404
|
+
<p className="text-sm text-muted-foreground py-2">{t('common.noResults')}</p>
|
|
405
|
+
) : (
|
|
406
|
+
<ScrollArea className="max-h-48 rounded-md border">
|
|
407
|
+
<div className="p-1 space-y-0.5">
|
|
408
|
+
{filteredTeams.map((team) => (
|
|
409
|
+
<button
|
|
410
|
+
key={team.id}
|
|
411
|
+
type="button"
|
|
412
|
+
disabled={saving}
|
|
413
|
+
className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm hover:bg-accent focus:bg-accent focus:outline-none disabled:opacity-50"
|
|
414
|
+
onClick={() => handleAddTeam(team.id)}
|
|
415
|
+
>
|
|
416
|
+
<Users className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
417
|
+
<span>{team.name}</span>
|
|
418
|
+
</button>
|
|
419
|
+
))}
|
|
420
|
+
</div>
|
|
421
|
+
</ScrollArea>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
408
424
|
)}
|
|
409
425
|
</div>
|
|
410
426
|
</div>
|
|
@@ -29,18 +29,21 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
29
29
|
|
|
30
30
|
const defaultIconClass = 'h-4 w-4';
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
33
|
+
{
|
|
34
|
+
variant = 'primary',
|
|
35
|
+
size = 'md',
|
|
36
|
+
icon: Icon,
|
|
37
|
+
iconPosition = 'left',
|
|
38
|
+
iconClassName = defaultIconClass,
|
|
39
|
+
loading = false,
|
|
40
|
+
children,
|
|
41
|
+
className = '',
|
|
42
|
+
disabled,
|
|
43
|
+
...props
|
|
44
|
+
},
|
|
45
|
+
ref
|
|
46
|
+
) {
|
|
44
47
|
const iconClass = iconClassName || defaultIconClass;
|
|
45
48
|
const content = loading ? (
|
|
46
49
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
@@ -54,6 +57,7 @@ export default function Button({
|
|
|
54
57
|
|
|
55
58
|
return (
|
|
56
59
|
<ShadcnButton
|
|
60
|
+
ref={ref}
|
|
57
61
|
className={cn(className)}
|
|
58
62
|
variant={variantMap[variant]}
|
|
59
63
|
size={sizeMap[size]}
|
|
@@ -63,6 +67,9 @@ export default function Button({
|
|
|
63
67
|
{content}
|
|
64
68
|
</ShadcnButton>
|
|
65
69
|
);
|
|
66
|
-
}
|
|
70
|
+
});
|
|
71
|
+
Button.displayName = 'Button';
|
|
72
|
+
|
|
73
|
+
export default Button;
|
|
67
74
|
|
|
68
75
|
export { buttonVariants };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { useAuth } from '../../contexts/AuthContext';
|
|
4
3
|
import api from '../../api/client';
|
|
5
4
|
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
|
6
5
|
import { Switch } from './switch';
|
|
@@ -48,7 +47,6 @@ export function SharingField({
|
|
|
48
47
|
disabled = false,
|
|
49
48
|
}: SharingFieldProps) {
|
|
50
49
|
const { t } = useTranslation();
|
|
51
|
-
const { user } = useAuth();
|
|
52
50
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
|
53
51
|
const [allUsers, setAllUsers] = useState<UserType[]>([]);
|
|
54
52
|
const [userSearchQuery, setUserSearchQuery] = useState('');
|
|
@@ -62,11 +60,12 @@ export function SharingField({
|
|
|
62
60
|
|
|
63
61
|
async function loadUsers() {
|
|
64
62
|
try {
|
|
65
|
-
|
|
63
|
+
// Use non-admin endpoint so any user can add people when sharing (same org/tenant).
|
|
64
|
+
const response = await api.get('/users/for-sharing');
|
|
66
65
|
const users = Array.isArray(response.data) ? response.data : [];
|
|
67
|
-
setAllUsers(users
|
|
66
|
+
setAllUsers(users);
|
|
68
67
|
} catch (error) {
|
|
69
|
-
console.error('Failed to load users:', error);
|
|
68
|
+
console.error('Failed to load users for sharing:', error);
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
|
|
@@ -387,7 +387,10 @@
|
|
|
387
387
|
"notSharedYet": "Not shared yet",
|
|
388
388
|
"emailNotAssociated": "This email is not associated with a SlugBase user yet.",
|
|
389
389
|
"removeAccess": "Remove access",
|
|
390
|
-
"add": "Add"
|
|
390
|
+
"add": "Add",
|
|
391
|
+
"noUsersOrTeams": "No users or teams could be loaded. Check the Network tab for /api/users/for-sharing and /api/teams.",
|
|
392
|
+
"typeToSearchUsers": "Type at least {{count}} characters to see matching users.",
|
|
393
|
+
"selectTeamToAdd": "Select a team to share with."
|
|
391
394
|
},
|
|
392
395
|
"plan": {
|
|
393
396
|
"limitBookmarks": "You've reached the Free plan limit ({{limit}} bookmarks). Upgrade to add more.",
|
|
@@ -180,7 +180,7 @@ export default function Bookmarks() {
|
|
|
180
180
|
|
|
181
181
|
async function loadData() {
|
|
182
182
|
try {
|
|
183
|
-
const [
|
|
183
|
+
const [bookmarksSettled, foldersSettled, tagsSettled, teamsSettled] = await Promise.allSettled([
|
|
184
184
|
api.get('/bookmarks', {
|
|
185
185
|
params: {
|
|
186
186
|
folder_id: selectedFolder || undefined,
|
|
@@ -197,13 +197,15 @@ export default function Bookmarks() {
|
|
|
197
197
|
api.get('/tags'),
|
|
198
198
|
api.get('/teams'),
|
|
199
199
|
]);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
if (bookmarksSettled.status === 'fulfilled') {
|
|
201
|
+
const payload = bookmarksSettled.value.data;
|
|
202
|
+
const items = payload.items ?? [];
|
|
203
|
+
setTotal(payload.total ?? 0);
|
|
204
|
+
setBookmarks(items);
|
|
205
|
+
}
|
|
206
|
+
if (foldersSettled.status === 'fulfilled') setFolders(foldersSettled.value.data ?? []);
|
|
207
|
+
if (tagsSettled.status === 'fulfilled') setTags(tagsSettled.value.data ?? []);
|
|
208
|
+
if (teamsSettled.status === 'fulfilled') setTeams(Array.isArray(teamsSettled.value.data) ? teamsSettled.value.data : []);
|
|
207
209
|
} catch (error) {
|
|
208
210
|
console.error('Failed to load data:', error);
|
|
209
211
|
} finally {
|
|
@@ -498,7 +500,7 @@ export default function Bookmarks() {
|
|
|
498
500
|
return (
|
|
499
501
|
<div className="space-y-6 pb-24">
|
|
500
502
|
{/* Sticky controls bar: header + filters/toolbar - stays visible when scrolling */}
|
|
501
|
-
<div className="sticky top-0 z-40 space-y-4 pb-4 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 pt-0 -mt-8 bg-background
|
|
503
|
+
<div className="sticky top-0 z-40 space-y-4 pb-4 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 pt-0 -mt-8 bg-background shadow-sm">
|
|
502
504
|
<PageHeader
|
|
503
505
|
className="pt-4"
|
|
504
506
|
title={`${t('bookmarks.title')} (${total})`}
|
|
@@ -197,7 +197,7 @@ export default function Folders() {
|
|
|
197
197
|
return (
|
|
198
198
|
<div className="space-y-6 pb-24">
|
|
199
199
|
{/* Sticky controls bar: header + toolbar - stays visible when scrolling */}
|
|
200
|
-
<div className="sticky top-0 z-40 space-y-4 pb-4 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 pt-0 -mt-8 bg-background
|
|
200
|
+
<div className="sticky top-0 z-40 space-y-4 pb-4 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 pt-0 -mt-8 bg-background shadow-sm">
|
|
201
201
|
<PageHeader
|
|
202
202
|
className="pt-4"
|
|
203
203
|
title={`${t('folders.title')} (${totalFolders})`}
|
|
@@ -368,8 +368,6 @@ export default function Folders() {
|
|
|
368
368
|
)}
|
|
369
369
|
</div>
|
|
370
370
|
</div>
|
|
371
|
-
{/* TODO: Add bookmark_count when backend supports it */}
|
|
372
|
-
<p className="text-xs text-muted-foreground">—</p>
|
|
373
371
|
</div>
|
|
374
372
|
</Link>
|
|
375
373
|
{folder.folder_type === 'own' && (
|
|
@@ -226,7 +226,7 @@ export default function Profile() {
|
|
|
226
226
|
<span>
|
|
227
227
|
{t('profile.signedInAs')}: <span className="text-foreground">{user.email}</span>
|
|
228
228
|
</span>
|
|
229
|
-
{user.is_admin && (
|
|
229
|
+
{!!user.is_admin && (
|
|
230
230
|
<Badge variant="secondary" className="text-xs font-normal">
|
|
231
231
|
{t('profile.admin')}
|
|
232
232
|
</Badge>
|
|
@@ -172,7 +172,7 @@ export default function Tags() {
|
|
|
172
172
|
return (
|
|
173
173
|
<div className="space-y-6 pb-24">
|
|
174
174
|
{/* Sticky controls bar: header + toolbar - stays visible when scrolling */}
|
|
175
|
-
<div className="sticky top-0 z-40 space-y-4 pb-4 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 pt-0 -mt-8 bg-background
|
|
175
|
+
<div className="sticky top-0 z-40 space-y-4 pb-4 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 pt-0 -mt-8 bg-background shadow-sm">
|
|
176
176
|
<PageHeader
|
|
177
177
|
className="pt-4"
|
|
178
178
|
title={`${t('tags.title')} (${totalTags})`}
|
|
@@ -287,8 +287,6 @@ export default function Tags() {
|
|
|
287
287
|
</h3>
|
|
288
288
|
</div>
|
|
289
289
|
</div>
|
|
290
|
-
{/* TODO: Add bookmark_count when backend supports it */}
|
|
291
|
-
<p className="text-xs text-muted-foreground">—</p>
|
|
292
290
|
</div>
|
|
293
291
|
</Link>
|
|
294
292
|
<div className={`flex gap-1.5 pt-2.5 mt-auto shrink-0 border-t border-border ${compactMode ? 'pt-2' : ''}`}>
|