@hed-hog/core 0.0.300 → 0.0.301

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.
Files changed (35) hide show
  1. package/dist/ai/ai.service.d.ts +13 -2
  2. package/dist/ai/ai.service.d.ts.map +1 -1
  3. package/dist/ai/ai.service.js +104 -2
  4. package/dist/ai/ai.service.js.map +1 -1
  5. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +26 -9
  6. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +11 -5
  8. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  9. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +34 -10
  10. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-core/dashboard-core.service.js +196 -69
  12. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/mail/mail.service.d.ts +9 -2
  18. package/dist/mail/mail.service.d.ts.map +1 -1
  19. package/dist/mail/mail.service.js +56 -4
  20. package/dist/mail/mail.service.js.map +1 -1
  21. package/dist/setting/setting.service.d.ts +6 -1
  22. package/dist/setting/setting.service.d.ts.map +1 -1
  23. package/dist/setting/setting.service.js +188 -15
  24. package/dist/setting/setting.service.js.map +1 -1
  25. package/hedhog/data/setting_group.yaml +28 -0
  26. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +305 -75
  27. package/hedhog/frontend/messages/en.json +15 -3
  28. package/hedhog/frontend/messages/pt.json +15 -3
  29. package/package.json +5 -5
  30. package/src/ai/ai.service.ts +129 -1
  31. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +9 -2
  32. package/src/dashboard/dashboard-core/dashboard-core.service.ts +276 -75
  33. package/src/index.ts +7 -6
  34. package/src/mail/mail.service.ts +67 -3
  35. package/src/setting/setting.service.ts +222 -15
@@ -13,6 +13,14 @@ import {
13
13
  } from '@/components/ui/alert-dialog';
14
14
  import { Badge } from '@/components/ui/badge';
15
15
  import { Button } from '@/components/ui/button';
16
+ import {
17
+ Command,
18
+ CommandEmpty,
19
+ CommandGroup,
20
+ CommandInput,
21
+ CommandItem,
22
+ CommandList,
23
+ } from '@/components/ui/command';
16
24
  import {
17
25
  ContextMenu,
18
26
  ContextMenuContent,
@@ -31,10 +39,12 @@ import {
31
39
  } from '@/components/ui/sheet';
32
40
  import { Skeleton } from '@/components/ui/skeleton';
33
41
  import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
42
+ import { useDebounce } from '@/hooks/use-debounce';
34
43
  import { cn } from '@/lib/utils';
35
44
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
36
45
  import * as LucideIcons from 'lucide-react';
37
46
  import {
47
+ Check,
38
48
  Home,
39
49
  LayoutDashboard,
40
50
  Loader2,
@@ -44,6 +54,7 @@ import {
44
54
  Share2,
45
55
  Trash2,
46
56
  UserPlus,
57
+ X,
47
58
  type LucideIcon,
48
59
  } from 'lucide-react';
49
60
  import { useTranslations } from 'next-intl';
@@ -68,6 +79,18 @@ type SharedUser = {
68
79
  email: string | null;
69
80
  isCurrentUser?: boolean;
70
81
  isHome?: boolean;
82
+ hasRequiredRoles?: boolean;
83
+ accessStatus?: 'allowed' | 'missing-roles';
84
+ };
85
+
86
+ type PaginatedResponse<T> = {
87
+ data: T[];
88
+ total: number;
89
+ page: number;
90
+ pageSize: number;
91
+ lastPage: number;
92
+ prev: number | null;
93
+ next: number | null;
71
94
  };
72
95
 
73
96
  type DashboardTemplate = {
@@ -82,6 +105,15 @@ const getDashboardName = (dashboard: DashboardTab) =>
82
105
  dashboard.name || dashboard.dashboard_locale?.[0]?.name || dashboard.slug;
83
106
 
84
107
  const DASHBOARD_HOME_HEADER_ACTIONS_TARGET_ID = 'dashboard-home-header-actions';
108
+ const EMPTY_SHAREABLE_USERS_PAGE: PaginatedResponse<SharedUser> = {
109
+ data: [],
110
+ total: 0,
111
+ page: 1,
112
+ pageSize: 10,
113
+ lastPage: 1,
114
+ prev: null,
115
+ next: null,
116
+ };
85
117
 
86
118
  const DASHBOARD_ICON_OPTIONS = [
87
119
  'layout-dashboard',
@@ -266,12 +298,16 @@ export function DashboardHomeTabs() {
266
298
  const [newDashboardIcon, setNewDashboardIcon] = useState('layout-dashboard');
267
299
  const [selectedTemplateSlug, setSelectedTemplateSlug] = useState('');
268
300
  const [shareSearch, setShareSearch] = useState('');
301
+ const [sharePage, setSharePage] = useState(1);
302
+ const [selectedShareUsers, setSelectedShareUsers] = useState<SharedUser[]>(
303
+ []
304
+ );
269
305
  const [renamingSlug, setRenamingSlug] = useState<string | null>(null);
270
306
  const [renameValue, setRenameValue] = useState('');
271
307
  const [isCreating, setIsCreating] = useState(false);
272
308
  const [isDeleting, setIsDeleting] = useState(false);
273
309
  const [isRenaming, setIsRenaming] = useState(false);
274
- const [sharingUserId, setSharingUserId] = useState<number | null>(null);
310
+ const [isSharingUsers, setIsSharingUsers] = useState(false);
275
311
  const [revokingUserId, setRevokingUserId] = useState<number | null>(null);
276
312
  const [isSettingHome, setIsSettingHome] = useState(false);
277
313
  const [openAddWidgetSignal, setOpenAddWidgetSignal] = useState(0);
@@ -280,6 +316,8 @@ export function DashboardHomeTabs() {
280
316
  const [iconValue, setIconValue] = useState('layout-dashboard');
281
317
  const [isSavingIcon, setIsSavingIcon] = useState(false);
282
318
 
319
+ const debouncedShareSearch = useDebounce(shareSearch, 300);
320
+
283
321
  const {
284
322
  data: dashboards = [],
285
323
  isLoading,
@@ -323,6 +361,16 @@ export function DashboardHomeTabs() {
323
361
  [dashboardTemplates, selectedTemplateSlug]
324
362
  );
325
363
 
364
+ const selectedShareUserIds = useMemo(
365
+ () => selectedShareUsers.map((user) => user.id),
366
+ [selectedShareUsers]
367
+ );
368
+
369
+ const selectedUsersWithoutRequiredRoles = useMemo(
370
+ () => selectedShareUsers.filter((user) => user.hasRequiredRoles === false),
371
+ [selectedShareUsers]
372
+ );
373
+
326
374
  useEffect(() => {
327
375
  if (dashboards.length === 0) {
328
376
  setActiveSlug('');
@@ -365,34 +413,55 @@ export function DashboardHomeTabs() {
365
413
  });
366
414
 
367
415
  const {
368
- data: shareableUsers = [],
416
+ data: shareableUsersPage = EMPTY_SHAREABLE_USERS_PAGE,
369
417
  isLoading: isLoadingShareableUsers,
370
418
  refetch: refetchShareableUsers,
371
- } = useQuery<SharedUser[]>({
372
- queryKey: ['dashboard-shareable-users', activeSlug, shareSearch, shareOpen],
419
+ } = useQuery<PaginatedResponse<SharedUser>>({
420
+ queryKey: [
421
+ 'dashboard-shareable-users',
422
+ activeSlug,
423
+ debouncedShareSearch,
424
+ sharePage,
425
+ shareOpen,
426
+ ],
373
427
  queryFn: async () => {
374
428
  if (!activeSlug) {
375
- return [];
429
+ return EMPTY_SHAREABLE_USERS_PAGE;
376
430
  }
377
431
 
378
432
  const params = new URLSearchParams();
379
- if (shareSearch.trim()) {
380
- params.set('search', shareSearch.trim());
433
+ params.set('page', String(sharePage));
434
+ params.set('pageSize', '10');
435
+
436
+ if (debouncedShareSearch.trim()) {
437
+ params.set('search', debouncedShareSearch.trim());
381
438
  }
382
439
 
383
- const queryString = params.toString();
384
- const response = await request<SharedUser[]>({
385
- url: `/dashboard-core/shareable-users/${activeSlug}${
386
- queryString ? `?${queryString}` : ''
387
- }`,
440
+ const response = await request<
441
+ PaginatedResponse<SharedUser> | SharedUser[]
442
+ >({
443
+ url: `/dashboard-core/shareable-users/${activeSlug}?${params.toString()}`,
388
444
  method: 'GET',
389
445
  });
390
446
 
391
- return response.data ?? [];
447
+ const payload = response.data;
448
+
449
+ if (Array.isArray(payload)) {
450
+ return {
451
+ ...EMPTY_SHAREABLE_USERS_PAGE,
452
+ data: payload,
453
+ total: payload.length,
454
+ };
455
+ }
456
+
457
+ return payload ?? EMPTY_SHAREABLE_USERS_PAGE;
392
458
  },
393
459
  enabled: shareOpen && Boolean(activeSlug),
460
+ placeholderData: (previous) => previous ?? EMPTY_SHAREABLE_USERS_PAGE,
394
461
  });
395
462
 
463
+ const shareableUsers = shareableUsersPage.data ?? [];
464
+
396
465
  const handleSelectTemplate = (template: DashboardTemplate | null) => {
397
466
  setSelectedTemplateSlug(template?.slug ?? '');
398
467
  setNewDashboardName(template?.name ?? '');
@@ -510,6 +579,9 @@ export function DashboardHomeTabs() {
510
579
  const openShareForDashboard = (dashboard: DashboardTab) => {
511
580
  cancelRenameDashboard();
512
581
  setActiveSlug(dashboard.slug);
582
+ setShareSearch('');
583
+ setSharePage(1);
584
+ setSelectedShareUsers([]);
513
585
  setShareOpen(true);
514
586
  };
515
587
 
@@ -593,24 +665,49 @@ export function DashboardHomeTabs() {
593
665
  }
594
666
  };
595
667
 
596
- const handleShareWithUser = async (userId: number) => {
597
- if (!activeDashboard) {
668
+ const toggleShareUserSelection = (user: SharedUser) => {
669
+ setSelectedShareUsers((current) => {
670
+ const alreadySelected = current.some((item) => item.id === user.id);
671
+
672
+ if (alreadySelected) {
673
+ return current.filter((item) => item.id !== user.id);
674
+ }
675
+
676
+ return [...current, user];
677
+ });
678
+ };
679
+
680
+ const removeSelectedShareUser = (userId: number) => {
681
+ setSelectedShareUsers((current) =>
682
+ current.filter((user) => user.id !== userId)
683
+ );
684
+ };
685
+
686
+ const handleShareSelectedUsers = async () => {
687
+ if (!activeDashboard || selectedShareUserIds.length === 0) {
598
688
  return;
599
689
  }
600
690
 
691
+ const selectionCount = selectedShareUserIds.length;
692
+
601
693
  try {
602
- setSharingUserId(userId);
694
+ setIsSharingUsers(true);
603
695
  await request({
604
696
  url: `/dashboard-core/dashboard/${activeDashboard.slug}/share`,
605
697
  method: 'POST',
606
- data: { userId },
698
+ data: {
699
+ userIds: selectedShareUserIds,
700
+ },
607
701
  });
608
702
  await Promise.all([refetchShares(), refetchShareableUsers()]);
609
- toast.success(t('dashboardShared'));
703
+ setSelectedShareUsers([]);
704
+ setShareSearch('');
705
+ setSharePage(1);
706
+ toast.success(t('dashboardSharedSelected', { count: selectionCount }));
610
707
  } catch (error) {
611
708
  toast.error(getErrorMessage(error, t('shareDashboardError')));
612
709
  } finally {
613
- setSharingUserId(null);
710
+ setIsSharingUsers(false);
614
711
  }
615
712
  };
616
713
 
@@ -640,7 +737,7 @@ export function DashboardHomeTabs() {
640
737
  isRenaming ||
641
738
  isSettingHome ||
642
739
  isSavingIcon ||
643
- sharingUserId !== null ||
740
+ isSharingUsers ||
644
741
  revokingUserId !== null;
645
742
 
646
743
  const headerActions = (
@@ -1195,10 +1292,12 @@ export function DashboardHomeTabs() {
1195
1292
  setShareOpen(open);
1196
1293
  if (!open) {
1197
1294
  setShareSearch('');
1295
+ setSharePage(1);
1296
+ setSelectedShareUsers([]);
1198
1297
  }
1199
1298
  }}
1200
1299
  >
1201
- <SheetContent className="w-full sm:max-w-xl">
1300
+ <SheetContent className="w-full sm:max-w-4xl">
1202
1301
  <SheetHeader>
1203
1302
  <SheetTitle>
1204
1303
  {t('shareDashboardTitle', {
@@ -1211,16 +1310,8 @@ export function DashboardHomeTabs() {
1211
1310
  </SheetHeader>
1212
1311
 
1213
1312
  <div className="flex flex-1 flex-col gap-4 overflow-hidden px-4 pb-4">
1214
- <div className="space-y-2">
1215
- <label className="text-sm font-medium" htmlFor="share-search">
1216
- {t('searchUser')}
1217
- </label>
1218
- <Input
1219
- id="share-search"
1220
- value={shareSearch}
1221
- onChange={(event) => setShareSearch(event.target.value)}
1222
- placeholder={t('searchUserPlaceholder')}
1223
- />
1313
+ <div className="rounded-lg border border-dashed bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
1314
+ {t('shareRoleNotice')}
1224
1315
  </div>
1225
1316
 
1226
1317
  <div className="grid flex-1 gap-4 overflow-hidden lg:grid-cols-2">
@@ -1237,7 +1328,7 @@ export function DashboardHomeTabs() {
1237
1328
  <Badge variant="outline">{sharedUsers.length}</Badge>
1238
1329
  </div>
1239
1330
 
1240
- <ScrollArea className="h-72">
1331
+ <ScrollArea className="h-80">
1241
1332
  <div className="space-y-2 p-3">
1242
1333
  {isLoadingShares ? (
1243
1334
  <>
@@ -1254,8 +1345,8 @@ export function DashboardHomeTabs() {
1254
1345
  key={user.id}
1255
1346
  className="flex items-center justify-between rounded-lg border p-3"
1256
1347
  >
1257
- <div className="min-w-0">
1258
- <div className="flex items-center gap-2">
1348
+ <div className="min-w-0 space-y-1">
1349
+ <div className="flex flex-wrap items-center gap-2">
1259
1350
  <p className="truncate text-sm font-medium">
1260
1351
  {user.name}
1261
1352
  </p>
@@ -1265,10 +1356,23 @@ export function DashboardHomeTabs() {
1265
1356
  {user.isHome ? (
1266
1357
  <Badge variant="outline">{t('home')}</Badge>
1267
1358
  ) : null}
1359
+ {user.hasRequiredRoles === false ? (
1360
+ <Badge
1361
+ variant="outline"
1362
+ className="border-amber-500/50 text-amber-700"
1363
+ >
1364
+ {t('roleRequiredBadge')}
1365
+ </Badge>
1366
+ ) : null}
1268
1367
  </div>
1269
1368
  <p className="text-muted-foreground truncate text-xs">
1270
1369
  {user.email || t('noPublicEmail')}
1271
1370
  </p>
1371
+ {user.hasRequiredRoles === false ? (
1372
+ <p className="text-xs text-amber-700">
1373
+ {t('shareBlockedHint')}
1374
+ </p>
1375
+ ) : null}
1272
1376
  </div>
1273
1377
 
1274
1378
  {!user.isCurrentUser ? (
@@ -1301,50 +1405,176 @@ export function DashboardHomeTabs() {
1301
1405
  </p>
1302
1406
  </div>
1303
1407
 
1304
- <ScrollArea className="h-72">
1305
- <div className="space-y-2 p-3">
1306
- {isLoadingShareableUsers ? (
1307
- <>
1308
- <Skeleton className="h-16 rounded-lg" />
1309
- <Skeleton className="h-16 rounded-lg" />
1310
- </>
1311
- ) : shareableUsers.length === 0 ? (
1312
- <p className="text-muted-foreground text-sm">
1313
- {t('noUsersToShare')}
1314
- </p>
1315
- ) : (
1316
- shareableUsers.map((user) => (
1317
- <div
1318
- key={user.id}
1319
- className="flex items-center justify-between rounded-lg border p-3"
1320
- >
1321
- <div className="min-w-0">
1322
- <p className="truncate text-sm font-medium">
1323
- {user.name}
1324
- </p>
1325
- <p className="text-muted-foreground truncate text-xs">
1326
- {user.email || t('noPublicEmail')}
1327
- </p>
1328
- </div>
1408
+ <div className="flex min-h-0 flex-1 flex-col gap-3 p-3">
1409
+ <div className="rounded-lg border">
1410
+ <Command shouldFilter={false}>
1411
+ <CommandInput
1412
+ value={shareSearch}
1413
+ onValueChange={(value) => {
1414
+ setShareSearch(value);
1415
+ setSharePage(1);
1416
+ }}
1417
+ placeholder={t('searchUserPlaceholder')}
1418
+ />
1419
+ <CommandList>
1420
+ <CommandEmpty>
1421
+ {isLoadingShareableUsers
1422
+ ? t('loadingUsers')
1423
+ : t('noUsersToShare')}
1424
+ </CommandEmpty>
1425
+ <CommandGroup>
1426
+ {shareableUsers.map((user) => {
1427
+ const isSelected = selectedShareUserIds.includes(
1428
+ user.id
1429
+ );
1430
+
1431
+ return (
1432
+ <CommandItem
1433
+ key={user.id}
1434
+ value={`${user.name}-${user.email ?? ''}-${user.id}`}
1435
+ className="cursor-pointer items-start gap-3 px-3 py-3"
1436
+ onSelect={() => toggleShareUserSelection(user)}
1437
+ >
1438
+ <div
1439
+ className={cn(
1440
+ 'mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-sm border',
1441
+ isSelected
1442
+ ? 'border-primary bg-primary text-primary-foreground'
1443
+ : 'border-muted-foreground/30'
1444
+ )}
1445
+ >
1446
+ <Check
1447
+ className={cn(
1448
+ 'size-3',
1449
+ isSelected ? 'opacity-100' : 'opacity-0'
1450
+ )}
1451
+ />
1452
+ </div>
1453
+ <div className="min-w-0 flex-1 space-y-1">
1454
+ <div className="flex flex-wrap items-center gap-2">
1455
+ <p className="truncate text-sm font-medium">
1456
+ {user.name}
1457
+ </p>
1458
+ {user.hasRequiredRoles === false ? (
1459
+ <Badge
1460
+ variant="outline"
1461
+ className="border-amber-500/50 text-amber-700"
1462
+ >
1463
+ {t('roleRequiredBadge')}
1464
+ </Badge>
1465
+ ) : null}
1466
+ </div>
1467
+ <p className="text-muted-foreground truncate text-xs">
1468
+ {user.email || t('noPublicEmail')}
1469
+ </p>
1470
+ </div>
1471
+ </CommandItem>
1472
+ );
1473
+ })}
1474
+ </CommandGroup>
1475
+ </CommandList>
1476
+ </Command>
1477
+ </div>
1329
1478
 
1330
- <Button
1331
- size="sm"
1332
- className="cursor-pointer"
1333
- onClick={() => void handleShareWithUser(user.id)}
1334
- disabled={sharingUserId === user.id}
1479
+ {selectedShareUsers.length > 0 ? (
1480
+ <div className="space-y-2 rounded-lg border p-3">
1481
+ <div className="flex items-center justify-between gap-2">
1482
+ <p className="text-sm font-semibold">
1483
+ {t('selectedUsers')}
1484
+ </p>
1485
+ <Badge variant="outline">
1486
+ {selectedShareUsers.length}
1487
+ </Badge>
1488
+ </div>
1489
+
1490
+ <div className="flex flex-wrap gap-2">
1491
+ {selectedShareUsers.map((user) => (
1492
+ <button
1493
+ key={`selected-${user.id}`}
1494
+ type="button"
1495
+ className="inline-flex cursor-pointer items-center gap-2 rounded-full border px-3 py-1 text-xs"
1496
+ onClick={() => removeSelectedShareUser(user.id)}
1335
1497
  >
1336
- {sharingUserId === user.id ? (
1337
- <Loader2 className="size-4 animate-spin" />
1338
- ) : (
1339
- <UserPlus className="size-4" />
1340
- )}
1341
- {t('add')}
1342
- </Button>
1343
- </div>
1344
- ))
1345
- )}
1498
+ <span className="max-w-40 truncate">
1499
+ {user.name}
1500
+ </span>
1501
+ <X className="size-3" />
1502
+ </button>
1503
+ ))}
1504
+ </div>
1505
+
1506
+ {selectedUsersWithoutRequiredRoles.length > 0 ? (
1507
+ <p className="text-xs text-amber-700">
1508
+ {t('selectedUsersWarning', {
1509
+ count: selectedUsersWithoutRequiredRoles.length,
1510
+ })}
1511
+ </p>
1512
+ ) : (
1513
+ <p className="text-muted-foreground text-xs">
1514
+ {t('selectedUsersHint')}
1515
+ </p>
1516
+ )}
1517
+ </div>
1518
+ ) : null}
1519
+
1520
+ <div className="mt-auto space-y-3 border-t pt-3">
1521
+ <div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
1522
+ <span>
1523
+ {t('sharePageStatus', {
1524
+ page: shareableUsersPage.page,
1525
+ totalPages: shareableUsersPage.lastPage,
1526
+ })}
1527
+ </span>
1528
+ <div className="flex items-center gap-2">
1529
+ <Button
1530
+ type="button"
1531
+ variant="outline"
1532
+ size="sm"
1533
+ className="cursor-pointer"
1534
+ onClick={() =>
1535
+ setSharePage((current) => Math.max(current - 1, 1))
1536
+ }
1537
+ disabled={
1538
+ isLoadingShareableUsers ||
1539
+ shareableUsersPage.prev === null
1540
+ }
1541
+ >
1542
+ {t('previousPage')}
1543
+ </Button>
1544
+ <Button
1545
+ type="button"
1546
+ variant="outline"
1547
+ size="sm"
1548
+ className="cursor-pointer"
1549
+ onClick={() => setSharePage((current) => current + 1)}
1550
+ disabled={
1551
+ isLoadingShareableUsers ||
1552
+ shareableUsersPage.next === null
1553
+ }
1554
+ >
1555
+ {t('nextPage')}
1556
+ </Button>
1557
+ </div>
1558
+ </div>
1559
+
1560
+ <Button
1561
+ className="w-full"
1562
+ onClick={() => void handleShareSelectedUsers()}
1563
+ disabled={
1564
+ isSharingUsers || selectedShareUserIds.length === 0
1565
+ }
1566
+ >
1567
+ {isSharingUsers ? (
1568
+ <Loader2 className="size-4 animate-spin" />
1569
+ ) : (
1570
+ <UserPlus className="size-4" />
1571
+ )}
1572
+ {t('shareSelectedUsers', {
1573
+ count: selectedShareUserIds.length,
1574
+ })}
1575
+ </Button>
1346
1576
  </div>
1347
- </ScrollArea>
1577
+ </div>
1348
1578
  </div>
1349
1579
  </div>
1350
1580
  </div>
@@ -974,17 +974,29 @@
974
974
  "suggestions": "Suggestions",
975
975
  "saveIcon": "Save icon",
976
976
  "shareDashboardTitle": "Share {name}",
977
- "shareDashboardDescription": "Shared users can edit the layout and widgets of this dashboard.",
977
+ "shareDashboardDescription": "Sharing only adds people to this dashboard. It does not change their roles or guarantee access to protected widgets.",
978
+ "shareRoleNotice": "Shared users keep their current roles. They can open this dashboard and its widgets only if they already have the required permissions.",
978
979
  "searchUser": "Search user",
979
980
  "searchUserPlaceholder": "Type a name or email",
980
981
  "usersWithAccess": "With access",
981
- "usersWithAccessDescription": "People who can already open and edit this dashboard.",
982
+ "usersWithAccessDescription": "People currently listed on this dashboard share. Removing them revokes the share immediately.",
982
983
  "noSharedUsers": "There are no shared users yet.",
983
984
  "you": "You",
984
985
  "home": "Home",
985
986
  "noPublicEmail": "No public email",
987
+ "roleRequiredBadge": "Needs roles",
988
+ "shareBlockedHint": "This user is shared on the dashboard, but will be blocked when opening it until the required roles are granted.",
986
989
  "addPeople": "Add people",
987
- "addPeopleDescription": "Share with users who do not have access yet.",
990
+ "addPeopleDescription": "Search users, select one or more, and confirm once. Sharing does not change roles.",
991
+ "loadingUsers": "Loading users...",
992
+ "selectedUsers": "Selected users",
993
+ "selectedUsersHint": "Select one or more users, then confirm once to grant dashboard sharing.",
994
+ "selectedUsersWarning": "{count} selected user(s) still need the required roles to open this dashboard.",
995
+ "sharePageStatus": "Page {page} of {totalPages}",
996
+ "previousPage": "Previous",
997
+ "nextPage": "Next",
998
+ "shareSelectedUsers": "Grant access to {count} user(s)",
999
+ "dashboardSharedSelected": "Dashboard shared with {count} user(s).",
988
1000
  "noUsersToShare": "No users found to share.",
989
1001
  "add": "Add",
990
1002
  "cancel": "Cancel",
@@ -977,17 +977,29 @@
977
977
  "suggestions": "Sugestões",
978
978
  "saveIcon": "Salvar ícone",
979
979
  "shareDashboardTitle": "Compartilhar {name}",
980
- "shareDashboardDescription": "Usuários compartilhados podem editar o layout e os widgets deste dashboard.",
980
+ "shareDashboardDescription": "O compartilhamento apenas adiciona pessoas a este dashboard. Isso não altera roles nem garante acesso a widgets protegidos.",
981
+ "shareRoleNotice": "Usuários compartilhados mantêm as roles atuais. Eles só conseguem abrir este dashboard e seus widgets se já tiverem as permissões necessárias.",
981
982
  "searchUser": "Buscar usuário",
982
983
  "searchUserPlaceholder": "Digite um nome ou e-mail",
983
984
  "usersWithAccess": "Com acesso",
984
- "usersWithAccessDescription": "Quem pode abrir e editar este dashboard.",
985
+ "usersWithAccessDescription": "Pessoas atualmente listadas neste compartilhamento. Ao remover, o compartilhamento é revogado imediatamente.",
985
986
  "noSharedUsers": "Ainda não há usuários compartilhados.",
986
987
  "you": "Você",
987
988
  "home": "Home",
988
989
  "noPublicEmail": "Sem e-mail público",
990
+ "roleRequiredBadge": "Precisa de roles",
991
+ "shareBlockedHint": "Este usuário continuará compartilhado, mas será bloqueado ao abrir o dashboard até receber as roles necessárias.",
989
992
  "addPeople": "Adicionar pessoas",
990
- "addPeopleDescription": "Compartilhe com usuários que ainda não possuem acesso.",
993
+ "addPeopleDescription": "Busque usuários, selecione um ou mais e confirme uma única vez. Compartilhar não altera roles.",
994
+ "loadingUsers": "Carregando usuários...",
995
+ "selectedUsers": "Usuários selecionados",
996
+ "selectedUsersHint": "Selecione um ou mais usuários e confirme uma única vez para conceder o compartilhamento do dashboard.",
997
+ "selectedUsersWarning": "{count} usuário(s) selecionado(s) ainda precisam das roles necessárias para abrir este dashboard.",
998
+ "sharePageStatus": "Página {page} de {totalPages}",
999
+ "previousPage": "Anterior",
1000
+ "nextPage": "Próxima",
1001
+ "shareSelectedUsers": "Conceder acesso para {count} usuário(s)",
1002
+ "dashboardSharedSelected": "Dashboard compartilhado com {count} usuário(s).",
991
1003
  "noUsersToShare": "Nenhum usuário encontrado para compartilhar.",
992
1004
  "add": "Adicionar",
993
1005
  "cancel": "Cancelar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.300",
3
+ "version": "0.0.301",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -30,12 +30,12 @@
30
30
  "sharp": "^0.34.2",
31
31
  "speakeasy": "^2.0.0",
32
32
  "uuid": "^11.1.0",
33
- "@hed-hog/api-pagination": "0.0.7",
34
- "@hed-hog/api-mail": "0.0.9",
35
33
  "@hed-hog/api": "0.0.6",
34
+ "@hed-hog/api-types": "0.0.1",
35
+ "@hed-hog/api-mail": "0.0.9",
36
36
  "@hed-hog/api-locale": "0.0.14",
37
- "@hed-hog/api-prisma": "0.0.6",
38
- "@hed-hog/api-types": "0.0.1"
37
+ "@hed-hog/api-pagination": "0.0.7",
38
+ "@hed-hog/api-prisma": "0.0.6"
39
39
  },
40
40
  "exports": {
41
41
  ".": {