@mdguggenbichler/slugbase-core 0.0.29 → 0.0.31

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.
@@ -14,7 +14,7 @@ import { ScrollArea } from '../ui/scroll-area';
14
14
  import Button from '../ui/Button';
15
15
  import Tooltip from '../ui/Tooltip';
16
16
  import { useToast } from '../ui/Toast';
17
- import { UserPlus, User, Users, X } from 'lucide-react';
17
+ import { User, Users, X } from 'lucide-react';
18
18
  import { cn } from '@/lib/utils';
19
19
 
20
20
  interface SharedUser {
@@ -146,9 +146,10 @@ export default function ShareResourceDialog({
146
146
  const msg = err.response?.data?.error || t('common.error');
147
147
  setError(msg);
148
148
  showToast(msg, 'error');
149
- } finally {
150
149
  setSaving(false);
150
+ throw err;
151
151
  }
152
+ setSaving(false);
152
153
  }
153
154
 
154
155
  function handleRemoveUser(userId: string) {
@@ -162,11 +163,19 @@ export default function ShareResourceDialog({
162
163
  }
163
164
 
164
165
  function handleAddUser(userId: string) {
166
+ if (sharedUsers.some((u) => u.id === userId)) return;
167
+ const userToAdd = allUsers.find((u) => u.id === userId);
165
168
  const newUserIds = [...sharedUsers.map((u) => u.id), userId];
166
- if (newUserIds.includes(userId)) return;
167
- updateShares(newUserIds, sharedTeams.map((t) => t.id), false);
169
+ if (userToAdd) {
170
+ setSharedUsers((prev) => [...prev, userToAdd]);
171
+ }
168
172
  setPeopleDropdownOpen(false);
169
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
+ });
170
179
  }
171
180
 
172
181
  function handleAddUserByEmail() {
@@ -181,9 +190,17 @@ export default function ShareResourceDialog({
181
190
  }
182
191
 
183
192
  function handleAddTeam(teamId: string) {
193
+ if (sharedTeams.some((t) => t.id === teamId)) return;
194
+ const teamToAdd = teams.find((t) => t.id === teamId);
184
195
  const newTeamIds = [...sharedTeams.map((t) => t.id), teamId];
185
- if (newTeamIds.includes(teamId)) return;
186
- updateShares(sharedUsers.map((u) => u.id), newTeamIds, 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
+ });
187
204
  }
188
205
 
189
206
  const searchQuery = emailInput.trim().toLowerCase();
@@ -326,41 +343,29 @@ export default function ShareResourceDialog({
326
343
  {allowShareToUsers && (activeTab === 'people' || !allowShareToTeams) && (
327
344
  <div className="space-y-2">
328
345
  <div className="relative">
329
- <div className="flex gap-2">
330
- <Input
331
- placeholder={t('admin.searchUsers')}
332
- value={emailInput}
333
- onChange={(e) => {
334
- setEmailInput(e.target.value);
346
+ <Input
347
+ placeholder={t('admin.searchUsers')}
348
+ value={emailInput}
349
+ onChange={(e) => {
350
+ setEmailInput(e.target.value);
351
+ setPeopleDropdownOpen(true);
352
+ }}
353
+ onFocus={() => {
354
+ if (searchQuery.length >= MIN_CHARS_FOR_USER_DROPDOWN)
335
355
  setPeopleDropdownOpen(true);
336
- }}
337
- onFocus={() => {
338
- if (searchQuery.length >= MIN_CHARS_FOR_USER_DROPDOWN)
339
- setPeopleDropdownOpen(true);
340
- }}
341
- onBlur={() => {
342
- setTimeout(() => setPeopleDropdownOpen(false), 150);
343
- }}
344
- onKeyDown={(e) => {
345
- if (e.key === 'Enter') {
346
- e.preventDefault();
347
- handleAddUserByEmail();
348
- }
349
- }}
350
- className="flex-1"
351
- autoComplete="off"
352
- />
353
- <Button
354
- type="button"
355
- variant="outline"
356
- size="sm"
357
- disabled={saving}
358
- onClick={() => handleAddUserByEmail()}
359
- >
360
- <UserPlus className="h-4 w-4" />
361
- {t('sharing.add')}
362
- </Button>
363
- </div>
356
+ }}
357
+ onBlur={() => {
358
+ setTimeout(() => setPeopleDropdownOpen(false), 150);
359
+ }}
360
+ onKeyDown={(e) => {
361
+ if (e.key === 'Enter') {
362
+ e.preventDefault();
363
+ handleAddUserByEmail();
364
+ }
365
+ }}
366
+ className="w-full"
367
+ autoComplete="off"
368
+ />
364
369
  {peopleDropdownOpen && showUserDropdown && (
365
370
  <div
366
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"
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Documentation URLs at docs.slugbase.app.
3
+ * Paths differ by mode: selfhosted (/selfhosted/...) vs cloud (/cloud/...).
4
+ */
5
+ import { isCloud } from './mode';
6
+
7
+ const DOCS_BASE = 'https://docs.slugbase.app';
8
+
9
+ /** URL to the API Reference section (selfhosted or cloud). */
10
+ export function getDocsApiReferenceUrl(): string {
11
+ const path = isCloud ? 'cloud/api-reference' : 'selfhosted/api-reference';
12
+ return `${DOCS_BASE}/${path}`;
13
+ }
14
+
15
+ /** URL to docs home/overview (selfhosted intro or cloud overview). */
16
+ export function getDocsBaseUrl(): string {
17
+ const path = isCloud ? 'cloud/overview' : 'selfhosted/intro';
18
+ return `${DOCS_BASE}/${path}`;
19
+ }
@@ -1,6 +1,8 @@
1
1
  /**
2
- * SlugBase frontend runtime mode is self-hosted only.
2
+ * SlugBase frontend runtime mode: self-hosted or cloud.
3
+ * Derived from VITE_SLUGBASE_MODE at build time (cloud builds set it to 'cloud').
3
4
  */
4
- export const mode: 'selfhosted' = 'selfhosted';
5
- export const isCloud = false;
6
- export const isSelfhosted = true;
5
+ const buildMode = import.meta.env.VITE_SLUGBASE_MODE === 'cloud' ? 'cloud' : 'selfhosted';
6
+ export const mode: 'selfhosted' | 'cloud' = buildMode;
7
+ export const isCloud = buildMode === 'cloud';
8
+ export const isSelfhosted = !isCloud;
@@ -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' && (
@@ -14,6 +14,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
14
14
  import { Badge } from '../components/ui/badge';
15
15
  import { Input } from '../components/ui/input';
16
16
  import api from '../api/client';
17
+ import { getDocsApiReferenceUrl } from '../config/docs';
17
18
 
18
19
  interface ApiToken {
19
20
  id: string;
@@ -58,7 +59,7 @@ function SettingsRow({
58
59
 
59
60
  export default function Profile() {
60
61
  const { t } = useTranslation();
61
- const { pathPrefixForLinks, apiBaseUrl } = useAppConfig();
62
+ const { pathPrefixForLinks } = useAppConfig();
62
63
  const prefix = (pathPrefixForLinks || '').replace(/\/+/g, '/') || '';
63
64
  const { user, updateUser, checkAuth } = useAuth();
64
65
  const { showToast } = useToast();
@@ -510,7 +511,7 @@ export default function Profile() {
510
511
  </p>
511
512
  </div>
512
513
  <a
513
- href={apiBaseUrl ? `${apiBaseUrl}/api-docs` : '/api-docs'}
514
+ href={getDocsApiReferenceUrl()}
514
515
  target="_blank"
515
516
  rel="noopener noreferrer"
516
517
  className="text-sm font-medium text-primary hover:text-primary/90 inline-block"
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
4
4
  import api from '../api/client';
5
5
  import { useAppConfig } from '../contexts/AppConfigContext';
6
6
  import Button from '../components/ui/Button';
7
+ import { getDocsBaseUrl } from '../config/docs';
7
8
 
8
9
  const MIN_PASSWORD_LENGTH = 8;
9
10
 
@@ -162,11 +163,11 @@ export default function Signup() {
162
163
  />
163
164
  <label htmlFor="signup-accept-terms" className="text-sm text-gray-700 dark:text-gray-300">
164
165
  {t('signup.acceptTermsPrefix')}
165
- <a href="https://docs.slugbase.app" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">
166
+ <a href={getDocsBaseUrl()} target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">
166
167
  {t('signup.acceptTermsTerms')}
167
168
  </a>
168
169
  {t('signup.acceptTermsAnd')}
169
- <a href="https://docs.slugbase.app" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">
170
+ <a href={getDocsBaseUrl()} target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">
170
171
  {t('signup.acceptTermsPrivacy')}
171
172
  </a>
172
173
  {t('signup.acceptTermsSuffix')}
@@ -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' : ''}`}>
@@ -1,6 +1,7 @@
1
1
  import { useTranslation } from 'react-i18next';
2
2
  import { Outlet } from 'react-router-dom';
3
3
  import { ExternalLink } from 'lucide-react';
4
+ import { getDocsApiReferenceUrl } from '../../config/docs';
4
5
 
5
6
  export default function AdminLayout() {
6
7
  const { t } = useTranslation();
@@ -25,7 +26,7 @@ export default function AdminLayout() {
25
26
  </p>
26
27
  </div>
27
28
  <a
28
- href="/api-docs"
29
+ href={getDocsApiReferenceUrl()}
29
30
  target="_blank"
30
31
  rel="noopener noreferrer"
31
32
  className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary hover:text-primary/90 transition-colors"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdguggenbichler/slugbase-core",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "SlugBase core: backend and frontend entrypoints for self-hosted and cloud apps",
5
5
  "type": "module",
6
6
  "exports": {