@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.
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAsIxB,eAAe,MAAM,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":"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"}
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,CAMhD"}
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
- if (!tenantId) {
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,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
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 { UserPlus, User, Users, X } from 'lucide-react';
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 [peoplePopoverOpen, setPeoplePopoverOpen] = useState(false);
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 fetchResource = useCallback(async () => {
78
- if (!resourceId || !isOpen) return;
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 (err: any) {
89
- console.error('Failed to fetch resource:', err);
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, isOpen, t, showToast]);
79
+ }, [resourceId, resourceType]);
96
80
 
97
- const fetchUsersAndTeams = useCallback(async () => {
98
- try {
99
- const [usersRes, teamsRes] = await Promise.all([
100
- api.get('/admin/users'),
101
- api.get('/teams'),
102
- ]);
103
- const users = Array.isArray(usersRes.data) ? usersRes.data : [];
104
- setAllUsers(users.filter((u: SharedUser) => u.id !== user?.id));
105
- setTeams(teamsRes.data ?? []);
106
- } catch (err) {
107
- console.error('Failed to fetch users/teams:', err);
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
- }, [user?.id]);
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
- fetchResource();
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 fetchResource();
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 (newUserIds.includes(userId)) return;
158
- updateShares(newUserIds, sharedTeams.map((t) => t.id), false);
159
- setPeoplePopoverOpen(false);
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 (newTeamIds.includes(teamId)) return;
177
- updateShares(sharedUsers.map((u) => u.id), newTeamIds, false);
178
- setTeamsPopoverOpen(false);
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 (!emailInput.trim()) return true;
183
- const q = emailInput.toLowerCase();
184
- return u.email.toLowerCase().includes(q) || (u.name && u.name.toLowerCase().includes(q));
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="flex gap-2">
345
+ <div className="relative">
310
346
  <Input
311
347
  placeholder={t('admin.searchUsers')}
312
348
  value={emailInput}
313
- onChange={(e) => setEmailInput(e.target.value)}
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="flex-1"
366
+ className="w-full"
367
+ autoComplete="off"
321
368
  />
322
- <Popover open={peoplePopoverOpen} onOpenChange={setPeoplePopoverOpen}>
323
- <PopoverAnchor asChild>
324
- <Button
325
- type="button"
326
- variant="outline"
327
- size="sm"
328
- disabled={saving}
329
- onClick={() => {
330
- if (emailInput.trim()) {
331
- handleAddUserByEmail();
332
- } else {
333
- setPeoplePopoverOpen(true);
334
- }
335
- }}
336
- >
337
- <UserPlus className="h-4 w-4" />
338
- {t('sharing.add')}
339
- </Button>
340
- </PopoverAnchor>
341
- <PopoverContent className="w-72 p-0" align="start">
342
- <Command>
343
- <CommandInput
344
- placeholder={t('admin.searchUsers')}
345
- value={emailInput}
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">{t('sharing.emailNotAssociated')}</p>
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
- <Popover open={teamsPopoverOpen} onOpenChange={setTeamsPopoverOpen}>
377
- <PopoverTrigger asChild>
378
- <Button
379
- type="button"
380
- variant="outline"
381
- size="sm"
382
- disabled={saving}
383
- className="w-full justify-start"
384
- >
385
- <Users className="h-4 w-4 mr-2" />
386
- {t('sharing.teams')}
387
- </Button>
388
- </PopoverTrigger>
389
- <PopoverContent className="w-72 p-0" align="start">
390
- <Command>
391
- <CommandInput placeholder={t('admin.searchTeams')} />
392
- <CommandList>
393
- <CommandEmpty>{t('common.noResults')}</CommandEmpty>
394
- <CommandGroup>
395
- {filteredTeams.map((team) => (
396
- <CommandItem
397
- key={team.id}
398
- onSelect={() => handleAddTeam(team.id)}
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
- export default function Button({
33
- variant = 'primary',
34
- size = 'md',
35
- icon: Icon,
36
- iconPosition = 'left',
37
- iconClassName = defaultIconClass,
38
- loading = false,
39
- children,
40
- className = '',
41
- disabled,
42
- ...props
43
- }: ButtonProps) {
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
- const response = await api.get('/admin/users');
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.filter((u: UserType) => u.id !== user?.id));
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 [bookmarksRes, foldersRes, tagsRes, teamsRes] = await Promise.all([
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
- const payload = bookmarksRes.data;
201
- const items = payload.items ?? [];
202
- setTotal(payload.total ?? 0);
203
- setBookmarks(items);
204
- setFolders(foldersRes.data);
205
- setTags(tagsRes.data);
206
- setTeams(teamsRes.data);
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 border-b shadow-sm">
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 border-b shadow-sm">
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 border-b shadow-sm">
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' : ''}`}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdguggenbichler/slugbase-core",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "SlugBase core: backend and frontend entrypoints for self-hosted and cloud apps",
5
5
  "type": "module",
6
6
  "exports": {