@hed-hog/core 0.0.303 → 0.0.305
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/README.md +84 -36
- package/dist/auth/auth.controller.d.ts +0 -3
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +4 -7
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +6 -3
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +9 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +7 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +45 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +9 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.js +49 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -0
- package/dist/mail-sent/mail-sent.controller.d.ts +24 -2
- package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.controller.js +5 -3
- package/dist/mail-sent/mail-sent.controller.js.map +1 -1
- package/dist/mail-sent/mail-sent.service.d.ts +28 -3
- package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.service.js +108 -3
- package/dist/mail-sent/mail-sent.service.js.map +1 -1
- package/dist/oauth/oauth-callback-coordinator.service.d.ts +18 -0
- package/dist/oauth/oauth-callback-coordinator.service.d.ts.map +1 -0
- package/dist/oauth/oauth-callback-coordinator.service.js +137 -0
- package/dist/oauth/oauth-callback-coordinator.service.js.map +1 -0
- package/dist/oauth/oauth.controller.d.ts +4 -2
- package/dist/oauth/oauth.controller.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.js +39 -11
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.errors.d.ts +16 -0
- package/dist/oauth/oauth.errors.d.ts.map +1 -0
- package/dist/oauth/oauth.errors.js +30 -0
- package/dist/oauth/oauth.errors.js.map +1 -0
- package/dist/oauth/oauth.module.d.ts.map +1 -1
- package/dist/oauth/oauth.module.js +10 -1
- package/dist/oauth/oauth.module.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +9 -1
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +97 -24
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/oauth/providers/abstract.provider.d.ts +6 -0
- package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
- package/dist/oauth/providers/abstract.provider.js +34 -0
- package/dist/oauth/providers/abstract.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +3 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +40 -22
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/security/security.service.d.ts +1 -0
- package/dist/security/security.service.d.ts.map +1 -1
- package/dist/security/security.service.js +9 -10
- package/dist/security/security.service.js.map +1 -1
- package/dist/session/session.service.d.ts +5 -1
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +76 -10
- package/dist/session/session.service.js.map +1 -1
- package/dist/setting/setting.service.js +4 -4
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/token/token.service.d.ts +1 -0
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +23 -12
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.service.d.ts +1 -1
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +3 -0
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/dashboard.yaml +2 -2
- package/hedhog/data/mail.yaml +147 -0
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +87 -8
- package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +90 -10
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +145 -31
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +67 -2
- package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +13 -9
- package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +250 -45
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +111 -15
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +110 -15
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +113 -15
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +481 -70
- package/hedhog/frontend/app/menu/page.tsx.ejs +202 -30
- package/hedhog/frontend/app/roles/menus.tsx.ejs +1 -1
- package/hedhog/frontend/app/roles/page.tsx.ejs +288 -104
- package/hedhog/frontend/app/users/page.tsx.ejs +168 -23
- package/hedhog/frontend/messages/en.json +45 -6
- package/hedhog/frontend/messages/pt.json +45 -6
- package/package.json +4 -4
- package/src/auth/auth.controller.ts +15 -19
- package/src/auth/auth.service.ts +13 -8
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +22 -9
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +75 -1
- package/src/index.ts +10 -7
- package/src/mail-sent/dto/query-mail-sent-list.dto.ts +30 -0
- package/src/mail-sent/mail-sent.controller.ts +14 -2
- package/src/mail-sent/mail-sent.service.ts +159 -4
- package/src/oauth/oauth-callback-coordinator.service.ts +158 -0
- package/src/oauth/oauth.controller.ts +58 -1
- package/src/oauth/oauth.errors.ts +32 -0
- package/src/oauth/oauth.module.ts +10 -1
- package/src/oauth/oauth.service.ts +178 -28
- package/src/oauth/providers/abstract.provider.ts +64 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +92 -56
- package/src/security/security.service.ts +12 -10
- package/src/session/session.service.ts +84 -9
- package/src/setting/setting.service.ts +10 -10
- package/src/token/token.service.ts +29 -11
- package/src/user/user.service.ts +10 -6
|
@@ -42,15 +42,19 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
|
42
42
|
import { useDebounce } from '@/hooks/use-debounce';
|
|
43
43
|
import { cn } from '@/lib/utils';
|
|
44
44
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
45
|
+
import useEmblaCarousel from 'embla-carousel-react';
|
|
45
46
|
import * as LucideIcons from 'lucide-react';
|
|
46
47
|
import {
|
|
47
48
|
Check,
|
|
49
|
+
ChevronLeft,
|
|
50
|
+
ChevronRight,
|
|
48
51
|
Home,
|
|
49
52
|
LayoutDashboard,
|
|
50
53
|
Loader2,
|
|
51
54
|
Palette,
|
|
52
55
|
Pencil,
|
|
53
56
|
Plus,
|
|
57
|
+
Save,
|
|
54
58
|
Share2,
|
|
55
59
|
Trash2,
|
|
56
60
|
UserPlus,
|
|
@@ -58,7 +62,12 @@ import {
|
|
|
58
62
|
type LucideIcon,
|
|
59
63
|
} from 'lucide-react';
|
|
60
64
|
import { useTranslations } from 'next-intl';
|
|
61
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
useEffect,
|
|
67
|
+
useMemo,
|
|
68
|
+
useState,
|
|
69
|
+
type DragEvent as ReactDragEvent,
|
|
70
|
+
} from 'react';
|
|
62
71
|
import { toast } from 'sonner';
|
|
63
72
|
import { DashboardContent } from './[slug]/dashboard-content';
|
|
64
73
|
|
|
@@ -104,7 +113,22 @@ type DashboardTemplate = {
|
|
|
104
113
|
const getDashboardName = (dashboard: DashboardTab) =>
|
|
105
114
|
dashboard.name || dashboard.dashboard_locale?.[0]?.name || dashboard.slug;
|
|
106
115
|
|
|
107
|
-
const
|
|
116
|
+
const reorderDashboards = (
|
|
117
|
+
currentDashboards: DashboardTab[],
|
|
118
|
+
sourceIndex: number,
|
|
119
|
+
targetIndex: number
|
|
120
|
+
) => {
|
|
121
|
+
const nextDashboards = [...currentDashboards];
|
|
122
|
+
const [movedDashboard] = nextDashboards.splice(sourceIndex, 1);
|
|
123
|
+
|
|
124
|
+
if (!movedDashboard) {
|
|
125
|
+
return currentDashboards;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
nextDashboards.splice(targetIndex, 0, movedDashboard);
|
|
129
|
+
return nextDashboards;
|
|
130
|
+
};
|
|
131
|
+
|
|
108
132
|
const EMPTY_SHAREABLE_USERS_PAGE: PaginatedResponse<SharedUser> = {
|
|
109
133
|
data: [],
|
|
110
134
|
total: 0,
|
|
@@ -289,6 +313,7 @@ const getErrorMessage = (error: unknown, fallback: string) => {
|
|
|
289
313
|
|
|
290
314
|
export function DashboardHomeTabs() {
|
|
291
315
|
const t = useTranslations('core.DashboardHomeTabs');
|
|
316
|
+
const tDashboardPage = useTranslations('core.DashboardPage');
|
|
292
317
|
const { request, currentLocaleCode } = useApp();
|
|
293
318
|
const [activeSlug, setActiveSlug] = useState('');
|
|
294
319
|
const [createOpen, setCreateOpen] = useState(false);
|
|
@@ -315,6 +340,27 @@ export function DashboardHomeTabs() {
|
|
|
315
340
|
const [iconDashboard, setIconDashboard] = useState<DashboardTab | null>(null);
|
|
316
341
|
const [iconValue, setIconValue] = useState('layout-dashboard');
|
|
317
342
|
const [isSavingIcon, setIsSavingIcon] = useState(false);
|
|
343
|
+
const [requestSaveLayoutSignal, setRequestSaveLayoutSignal] = useState(0);
|
|
344
|
+
const [runtimeHeaderState, setRuntimeHeaderState] = useState({
|
|
345
|
+
hasChanges: false,
|
|
346
|
+
isSaving: false,
|
|
347
|
+
isAutosavePending: false,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const [tabsEmblaRef, tabsEmblaApi] = useEmblaCarousel({
|
|
351
|
+
dragFree: true,
|
|
352
|
+
containScroll: 'keepSnaps',
|
|
353
|
+
align: 'start',
|
|
354
|
+
});
|
|
355
|
+
const [canScrollPrevTabs, setCanScrollPrevTabs] = useState(false);
|
|
356
|
+
const [canScrollNextTabs, setCanScrollNextTabs] = useState(false);
|
|
357
|
+
const [orderedDashboards, setOrderedDashboards] = useState<DashboardTab[]>(
|
|
358
|
+
[]
|
|
359
|
+
);
|
|
360
|
+
const [draggingDashboardSlug, setDraggingDashboardSlug] = useState<
|
|
361
|
+
string | null
|
|
362
|
+
>(null);
|
|
363
|
+
const [isReordering, setIsReordering] = useState(false);
|
|
318
364
|
|
|
319
365
|
const debouncedShareSearch = useDebounce(shareSearch, 300);
|
|
320
366
|
|
|
@@ -349,8 +395,10 @@ export function DashboardHomeTabs() {
|
|
|
349
395
|
});
|
|
350
396
|
|
|
351
397
|
const activeDashboard = useMemo(
|
|
352
|
-
() =>
|
|
353
|
-
|
|
398
|
+
() =>
|
|
399
|
+
orderedDashboards.find((dashboard) => dashboard.slug === activeSlug) ??
|
|
400
|
+
null,
|
|
401
|
+
[activeSlug, orderedDashboards]
|
|
354
402
|
);
|
|
355
403
|
|
|
356
404
|
const selectedTemplate = useMemo(
|
|
@@ -372,24 +420,47 @@ export function DashboardHomeTabs() {
|
|
|
372
420
|
);
|
|
373
421
|
|
|
374
422
|
useEffect(() => {
|
|
375
|
-
|
|
423
|
+
setOrderedDashboards(dashboards);
|
|
424
|
+
}, [dashboards]);
|
|
425
|
+
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (orderedDashboards.length === 0) {
|
|
376
428
|
setActiveSlug('');
|
|
377
429
|
return;
|
|
378
430
|
}
|
|
379
431
|
|
|
380
|
-
const hasActiveDashboard =
|
|
432
|
+
const hasActiveDashboard = orderedDashboards.some(
|
|
381
433
|
(dashboard) => dashboard.slug === activeSlug
|
|
382
434
|
);
|
|
383
435
|
|
|
384
436
|
if (!activeSlug || !hasActiveDashboard) {
|
|
385
437
|
const nextDashboard =
|
|
386
|
-
|
|
438
|
+
orderedDashboards.find((dashboard) => dashboard.is_home) ??
|
|
439
|
+
orderedDashboards[0];
|
|
387
440
|
|
|
388
441
|
if (nextDashboard) {
|
|
389
442
|
setActiveSlug(nextDashboard.slug);
|
|
390
443
|
}
|
|
391
444
|
}
|
|
392
|
-
}, [activeSlug,
|
|
445
|
+
}, [activeSlug, orderedDashboards]);
|
|
446
|
+
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
if (!tabsEmblaApi) return;
|
|
449
|
+
|
|
450
|
+
const updateScrollButtons = () => {
|
|
451
|
+
setCanScrollPrevTabs(tabsEmblaApi.canScrollPrev());
|
|
452
|
+
setCanScrollNextTabs(tabsEmblaApi.canScrollNext());
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
updateScrollButtons();
|
|
456
|
+
tabsEmblaApi.on('scroll', updateScrollButtons);
|
|
457
|
+
tabsEmblaApi.on('reInit', updateScrollButtons);
|
|
458
|
+
|
|
459
|
+
return () => {
|
|
460
|
+
tabsEmblaApi.off('scroll', updateScrollButtons);
|
|
461
|
+
tabsEmblaApi.off('reInit', updateScrollButtons);
|
|
462
|
+
};
|
|
463
|
+
}, [tabsEmblaApi]);
|
|
393
464
|
|
|
394
465
|
const {
|
|
395
466
|
data: sharedUsers = [],
|
|
@@ -462,6 +533,80 @@ export function DashboardHomeTabs() {
|
|
|
462
533
|
|
|
463
534
|
const shareableUsers = shareableUsersPage.data ?? [];
|
|
464
535
|
|
|
536
|
+
const handleTabDragStart = (
|
|
537
|
+
event: ReactDragEvent<HTMLElement>,
|
|
538
|
+
dashboardSlug: string
|
|
539
|
+
) => {
|
|
540
|
+
setDraggingDashboardSlug(dashboardSlug);
|
|
541
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
542
|
+
event.dataTransfer.setData('text/plain', dashboardSlug);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const handleTabDragOver = (event: ReactDragEvent<HTMLElement>) => {
|
|
546
|
+
event.preventDefault();
|
|
547
|
+
event.dataTransfer.dropEffect = 'move';
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const handleTabDrop = async (
|
|
551
|
+
event: ReactDragEvent<HTMLElement>,
|
|
552
|
+
targetDashboardSlug: string
|
|
553
|
+
) => {
|
|
554
|
+
event.preventDefault();
|
|
555
|
+
|
|
556
|
+
const sourceDashboardSlug =
|
|
557
|
+
draggingDashboardSlug || event.dataTransfer.getData('text/plain');
|
|
558
|
+
|
|
559
|
+
if (!sourceDashboardSlug || sourceDashboardSlug === targetDashboardSlug) {
|
|
560
|
+
setDraggingDashboardSlug(null);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const sourceIndex = orderedDashboards.findIndex(
|
|
565
|
+
(dashboard) => dashboard.slug === sourceDashboardSlug
|
|
566
|
+
);
|
|
567
|
+
const targetIndex = orderedDashboards.findIndex(
|
|
568
|
+
(dashboard) => dashboard.slug === targetDashboardSlug
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
if (sourceIndex < 0 || targetIndex < 0) {
|
|
572
|
+
setDraggingDashboardSlug(null);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const previousDashboards = orderedDashboards;
|
|
577
|
+
const nextDashboards = reorderDashboards(
|
|
578
|
+
orderedDashboards,
|
|
579
|
+
sourceIndex,
|
|
580
|
+
targetIndex
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
setOrderedDashboards(nextDashboards);
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
setIsReordering(true);
|
|
587
|
+
await request({
|
|
588
|
+
url: '/dashboard-core/dashboard/order',
|
|
589
|
+
method: 'PATCH',
|
|
590
|
+
data: {
|
|
591
|
+
slugs: nextDashboards.map((dashboard) => dashboard.slug),
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
await refetchDashboards();
|
|
595
|
+
} catch (error) {
|
|
596
|
+
setOrderedDashboards(previousDashboards);
|
|
597
|
+
toast.error(
|
|
598
|
+
getErrorMessage(error, 'Unable to save dashboard tab order.')
|
|
599
|
+
);
|
|
600
|
+
} finally {
|
|
601
|
+
setIsReordering(false);
|
|
602
|
+
setDraggingDashboardSlug(null);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const handleTabDragEnd = () => {
|
|
607
|
+
setDraggingDashboardSlug(null);
|
|
608
|
+
};
|
|
609
|
+
|
|
465
610
|
const handleSelectTemplate = (template: DashboardTemplate | null) => {
|
|
466
611
|
setSelectedTemplateSlug(template?.slug ?? '');
|
|
467
612
|
setNewDashboardName(template?.name ?? '');
|
|
@@ -738,34 +883,9 @@ export function DashboardHomeTabs() {
|
|
|
738
883
|
isSettingHome ||
|
|
739
884
|
isSavingIcon ||
|
|
740
885
|
isSharingUsers ||
|
|
886
|
+
isReordering ||
|
|
741
887
|
revokingUserId !== null;
|
|
742
888
|
|
|
743
|
-
const headerActions = (
|
|
744
|
-
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
745
|
-
{activeDashboard ? (
|
|
746
|
-
<div
|
|
747
|
-
id={DASHBOARD_HOME_HEADER_ACTIONS_TARGET_ID}
|
|
748
|
-
className="contents"
|
|
749
|
-
/>
|
|
750
|
-
) : null}
|
|
751
|
-
<Button
|
|
752
|
-
variant="outline"
|
|
753
|
-
size="sm"
|
|
754
|
-
onClick={() =>
|
|
755
|
-
activeDashboard && openShareForDashboard(activeDashboard)
|
|
756
|
-
}
|
|
757
|
-
disabled={!activeDashboard || isBusy}
|
|
758
|
-
>
|
|
759
|
-
<Share2 className="size-4" />
|
|
760
|
-
{t('share')}
|
|
761
|
-
</Button>
|
|
762
|
-
<Button size="sm" onClick={() => setCreateOpen(true)} disabled={isBusy}>
|
|
763
|
-
<Plus className="size-4" />
|
|
764
|
-
{t('newDashboard')}
|
|
765
|
-
</Button>
|
|
766
|
-
</div>
|
|
767
|
-
);
|
|
768
|
-
|
|
769
889
|
return (
|
|
770
890
|
<Page>
|
|
771
891
|
<PageHeader
|
|
@@ -777,7 +897,51 @@ export function DashboardHomeTabs() {
|
|
|
777
897
|
]}
|
|
778
898
|
title={t('pageTitle')}
|
|
779
899
|
description={t('pageDescription')}
|
|
780
|
-
actions={
|
|
900
|
+
actions={[
|
|
901
|
+
{
|
|
902
|
+
label: t('share'),
|
|
903
|
+
onClick: () =>
|
|
904
|
+
activeDashboard && openShareForDashboard(activeDashboard),
|
|
905
|
+
icon: <Share2 className="size-4" />,
|
|
906
|
+
variant: 'outline',
|
|
907
|
+
size: 'sm',
|
|
908
|
+
disabled: !activeDashboard || isBusy,
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
label: t('addWidgets'),
|
|
912
|
+
onClick: () =>
|
|
913
|
+
activeDashboard && openAddWidgetsForDashboard(activeDashboard),
|
|
914
|
+
icon: <Plus className="size-4" />,
|
|
915
|
+
variant: 'outline',
|
|
916
|
+
size: 'sm',
|
|
917
|
+
disabled: !activeDashboard || isBusy,
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
label: runtimeHeaderState.isSaving
|
|
921
|
+
? tDashboardPage('saving')
|
|
922
|
+
: tDashboardPage('saveLayout'),
|
|
923
|
+
onClick: () =>
|
|
924
|
+
setRequestSaveLayoutSignal((previous) => previous + 1),
|
|
925
|
+
icon: runtimeHeaderState.isSaving ? (
|
|
926
|
+
<Loader2 className="size-4 animate-spin" />
|
|
927
|
+
) : (
|
|
928
|
+
<Save className="size-4" />
|
|
929
|
+
),
|
|
930
|
+
size: 'sm',
|
|
931
|
+
disabled:
|
|
932
|
+
!activeDashboard ||
|
|
933
|
+
isBusy ||
|
|
934
|
+
runtimeHeaderState.isSaving ||
|
|
935
|
+
!runtimeHeaderState.hasChanges,
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
label: t('newDashboard'),
|
|
939
|
+
onClick: () => setCreateOpen(true),
|
|
940
|
+
icon: <Plus className="size-4" />,
|
|
941
|
+
size: 'sm',
|
|
942
|
+
disabled: isBusy,
|
|
943
|
+
},
|
|
944
|
+
]}
|
|
781
945
|
/>
|
|
782
946
|
|
|
783
947
|
{isLoading ? (
|
|
@@ -793,10 +957,20 @@ export function DashboardHomeTabs() {
|
|
|
793
957
|
onValueChange={setActiveSlug}
|
|
794
958
|
className="gap-0"
|
|
795
959
|
>
|
|
796
|
-
<div className="bg-muted dark:bg-muted/40">
|
|
797
|
-
|
|
798
|
-
<
|
|
799
|
-
|
|
960
|
+
<div className="bg-muted dark:bg-muted/40 relative">
|
|
961
|
+
{canScrollPrevTabs && (
|
|
962
|
+
<button
|
|
963
|
+
type="button"
|
|
964
|
+
onClick={() => tabsEmblaApi?.scrollPrev()}
|
|
965
|
+
aria-label="Scroll tabs left"
|
|
966
|
+
className="md:hidden absolute left-0 top-0 bottom-0 z-10 flex items-center px-1.5 bg-linear-to-r from-muted dark:from-muted/40 to-transparent"
|
|
967
|
+
>
|
|
968
|
+
<ChevronLeft className="size-4 text-muted-foreground" />
|
|
969
|
+
</button>
|
|
970
|
+
)}
|
|
971
|
+
<div ref={tabsEmblaRef} className="overflow-hidden">
|
|
972
|
+
<TabsList className="dashboard-tabs-list h-auto min-h-0 items-end gap-0 rounded-none bg-transparent p-0 pt-1 pl-1 flex w-max">
|
|
973
|
+
{orderedDashboards.map((dashboard) => {
|
|
800
974
|
const isRenamingCurrentTab =
|
|
801
975
|
renamingSlug === dashboard.slug;
|
|
802
976
|
const isActiveTab = activeSlug === dashboard.slug;
|
|
@@ -846,13 +1020,32 @@ export function DashboardHomeTabs() {
|
|
|
846
1020
|
<ContextMenu key={dashboard.slug}>
|
|
847
1021
|
<ContextMenuTrigger asChild>
|
|
848
1022
|
{isRenamingCurrentTab ? (
|
|
849
|
-
<div
|
|
1023
|
+
<div
|
|
1024
|
+
onDragOver={handleTabDragOver}
|
|
1025
|
+
onDrop={(event) =>
|
|
1026
|
+
void handleTabDrop(event, dashboard.slug)
|
|
1027
|
+
}
|
|
1028
|
+
className="-mb-px flex min-h-9 items-center gap-2 rounded-b-none rounded-t-lg border border-b-0 border-border bg-background px-3 py-2 text-foreground shadow-none"
|
|
1029
|
+
>
|
|
850
1030
|
{tabContent}
|
|
851
1031
|
</div>
|
|
852
1032
|
) : (
|
|
853
1033
|
<TabsTrigger
|
|
854
1034
|
value={dashboard.slug}
|
|
855
|
-
|
|
1035
|
+
draggable={!isBusy}
|
|
1036
|
+
onDragStart={(event) =>
|
|
1037
|
+
handleTabDragStart(event, dashboard.slug)
|
|
1038
|
+
}
|
|
1039
|
+
onDragOver={handleTabDragOver}
|
|
1040
|
+
onDrop={(event) =>
|
|
1041
|
+
void handleTabDrop(event, dashboard.slug)
|
|
1042
|
+
}
|
|
1043
|
+
onDragEnd={handleTabDragEnd}
|
|
1044
|
+
className={cn(
|
|
1045
|
+
'dashboard-tab-trigger -mb-px cursor-pointer gap-2 rounded-b-none rounded-t-lg px-3 py-2 mt-1 border-none shadow-none data-[state=active]:shadow-none',
|
|
1046
|
+
draggingDashboardSlug === dashboard.slug &&
|
|
1047
|
+
'opacity-70'
|
|
1048
|
+
)}
|
|
856
1049
|
style={
|
|
857
1050
|
isActiveTab
|
|
858
1051
|
? {
|
|
@@ -934,7 +1127,17 @@ export function DashboardHomeTabs() {
|
|
|
934
1127
|
<Plus className="size-4" />
|
|
935
1128
|
</button>
|
|
936
1129
|
</TabsList>
|
|
937
|
-
</
|
|
1130
|
+
</div>
|
|
1131
|
+
{canScrollNextTabs && (
|
|
1132
|
+
<button
|
|
1133
|
+
type="button"
|
|
1134
|
+
onClick={() => tabsEmblaApi?.scrollNext()}
|
|
1135
|
+
aria-label="Scroll tabs right"
|
|
1136
|
+
className="md:hidden absolute right-0 top-0 bottom-0 z-10 flex items-center px-1.5 bg-linear-to-l from-muted dark:from-muted/40 to-transparent"
|
|
1137
|
+
>
|
|
1138
|
+
<ChevronRight className="size-4 text-muted-foreground" />
|
|
1139
|
+
</button>
|
|
1140
|
+
)}
|
|
938
1141
|
</div>
|
|
939
1142
|
</Tabs>
|
|
940
1143
|
</div>
|
|
@@ -952,7 +1155,9 @@ export function DashboardHomeTabs() {
|
|
|
952
1155
|
key={activeDashboard.slug}
|
|
953
1156
|
dashboardSlug={activeDashboard.slug}
|
|
954
1157
|
showHeader={false}
|
|
955
|
-
|
|
1158
|
+
externalHeaderActions
|
|
1159
|
+
requestSaveSignal={requestSaveLayoutSignal}
|
|
1160
|
+
onRuntimeActionsStateChange={setRuntimeHeaderState}
|
|
956
1161
|
openWidgetPickerSignal={openAddWidgetSignal}
|
|
957
1162
|
onOpenWidgetPickerHandled={() => setOpenAddWidgetSignal(0)}
|
|
958
1163
|
/>
|
|
@@ -1131,7 +1336,7 @@ export function DashboardHomeTabs() {
|
|
|
1131
1336
|
|
|
1132
1337
|
<div className="space-y-2">
|
|
1133
1338
|
<p className="text-sm font-medium">{t('iconSuggestions')}</p>
|
|
1134
|
-
<div className="grid gap-2 grid-cols-12">
|
|
1339
|
+
<div className="grid grid-cols-6 gap-2 md:grid-cols-8 lg:grid-cols-12">
|
|
1135
1340
|
{DASHBOARD_ICON_OPTIONS.map((iconName) => {
|
|
1136
1341
|
const IconOption = resolveDashboardIcon(iconName);
|
|
1137
1342
|
const isSelected =
|
|
@@ -1241,7 +1446,7 @@ export function DashboardHomeTabs() {
|
|
|
1241
1446
|
|
|
1242
1447
|
<div className="space-y-2">
|
|
1243
1448
|
<p className="text-sm font-medium">{t('suggestions')}</p>
|
|
1244
|
-
<div className="grid gap-2 grid-cols-12">
|
|
1449
|
+
<div className="grid grid-cols-6 gap-2 md:grid-cols-8 lg:grid-cols-12">
|
|
1245
1450
|
{DASHBOARD_ICON_OPTIONS.map((iconName) => {
|
|
1246
1451
|
const IconOption = resolveDashboardIcon(iconName);
|
|
1247
1452
|
const isSelected =
|
|
@@ -45,6 +45,8 @@ import {
|
|
|
45
45
|
TableRow,
|
|
46
46
|
} from '@/components/ui/table';
|
|
47
47
|
import { useDebounce } from '@/hooks/use-debounce';
|
|
48
|
+
import { useFormDraft } from '@/hooks/use-form-draft';
|
|
49
|
+
import { formatDateTime } from '@/lib/format-date';
|
|
48
50
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
49
51
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
50
52
|
import {
|
|
@@ -55,9 +57,11 @@ import {
|
|
|
55
57
|
IconPlus,
|
|
56
58
|
IconTrash,
|
|
57
59
|
} from '@tabler/icons-react';
|
|
60
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
61
|
+
import { enUS, ptBR } from 'date-fns/locale';
|
|
58
62
|
import { useTranslations } from 'next-intl';
|
|
59
|
-
import { useState } from 'react';
|
|
60
|
-
import { useForm } from 'react-hook-form';
|
|
63
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
64
|
+
import { useForm, useWatch } from 'react-hook-form';
|
|
61
65
|
import { toast } from 'sonner';
|
|
62
66
|
import { z } from 'zod';
|
|
63
67
|
|
|
@@ -66,6 +70,13 @@ type RoleFormData = {
|
|
|
66
70
|
role_id: number;
|
|
67
71
|
};
|
|
68
72
|
|
|
73
|
+
type ComponentRoleDraftPayload = {
|
|
74
|
+
values: RoleFormData;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const COMPONENT_ROLE_DRAFT_STORAGE_KEY =
|
|
78
|
+
'core-dashboard-component-role-form-draft';
|
|
79
|
+
|
|
69
80
|
interface ComponentRole {
|
|
70
81
|
id: number;
|
|
71
82
|
component_id: number;
|
|
@@ -106,9 +117,25 @@ interface Role {
|
|
|
106
117
|
}>;
|
|
107
118
|
}
|
|
108
119
|
|
|
120
|
+
type ComponentListResponse = {
|
|
121
|
+
data?: Component[];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
type RoleListResponse = {
|
|
125
|
+
data?: Role[];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
type RequestError = {
|
|
129
|
+
response?: {
|
|
130
|
+
data?: {
|
|
131
|
+
message?: string;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
109
136
|
export function ComponentRolesTab() {
|
|
110
137
|
const t = useTranslations('core.DashboardManagement');
|
|
111
|
-
const { request, currentLocaleCode } = useApp();
|
|
138
|
+
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
112
139
|
|
|
113
140
|
const roleSchema = z.object({
|
|
114
141
|
component_id: z.coerce.number().min(1, t('selectComponentRequired')),
|
|
@@ -120,7 +147,7 @@ export function ComponentRolesTab() {
|
|
|
120
147
|
const [roleToDelete, setRoleToDelete] = useState<number | null>(null);
|
|
121
148
|
const [page, setPage] = useState(1);
|
|
122
149
|
const [pageSize, setPageSize] = useState(10);
|
|
123
|
-
const
|
|
150
|
+
const searchQuery = '';
|
|
124
151
|
const debouncedSearch = useDebounce(searchQuery, 500);
|
|
125
152
|
|
|
126
153
|
const {
|
|
@@ -153,10 +180,10 @@ export function ComponentRolesTab() {
|
|
|
153
180
|
const total = paginatedData?.total ?? 0;
|
|
154
181
|
const totalPages = Math.ceil(total / pageSize);
|
|
155
182
|
|
|
156
|
-
const { data: components } = useQuery<
|
|
183
|
+
const { data: components } = useQuery<ComponentListResponse>({
|
|
157
184
|
queryKey: ['dashboard-components'],
|
|
158
185
|
queryFn: async () => {
|
|
159
|
-
const { data } = await request<
|
|
186
|
+
const { data } = await request<ComponentListResponse>({
|
|
160
187
|
url: '/dashboard-component',
|
|
161
188
|
method: 'GET',
|
|
162
189
|
});
|
|
@@ -164,10 +191,10 @@ export function ComponentRolesTab() {
|
|
|
164
191
|
},
|
|
165
192
|
});
|
|
166
193
|
|
|
167
|
-
const { data: availableRoles } = useQuery<
|
|
194
|
+
const { data: availableRoles } = useQuery<RoleListResponse>({
|
|
168
195
|
queryKey: ['roles'],
|
|
169
196
|
queryFn: async () => {
|
|
170
|
-
const { data } = await request<
|
|
197
|
+
const { data } = await request<RoleListResponse>({
|
|
171
198
|
url: '/role',
|
|
172
199
|
method: 'GET',
|
|
173
200
|
});
|
|
@@ -183,6 +210,67 @@ export function ComponentRolesTab() {
|
|
|
183
210
|
},
|
|
184
211
|
});
|
|
185
212
|
|
|
213
|
+
const watchedValues = useWatch({
|
|
214
|
+
control: form.control,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const {
|
|
218
|
+
clearDraft,
|
|
219
|
+
loadDraft,
|
|
220
|
+
hasDraft,
|
|
221
|
+
savedAt: draftSavedAt,
|
|
222
|
+
} = useFormDraft<ComponentRoleDraftPayload>({
|
|
223
|
+
storageKey: COMPONENT_ROLE_DRAFT_STORAGE_KEY,
|
|
224
|
+
value: {
|
|
225
|
+
values: {
|
|
226
|
+
component_id: Number(watchedValues.component_id ?? 0),
|
|
227
|
+
role_id: Number(watchedValues.role_id ?? 0),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
hasData:
|
|
231
|
+
Number(watchedValues.component_id ?? 0) > 0 ||
|
|
232
|
+
Number(watchedValues.role_id ?? 0) > 0,
|
|
233
|
+
enabled: open,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const draftStatusContent = useMemo(() => {
|
|
237
|
+
if (!hasDraft || !draftSavedAt) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const savedDate = new Date(draftSavedAt);
|
|
242
|
+
if (Number.isNaN(savedDate.getTime())) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
|
|
247
|
+
const relativeLabel = formatDistanceToNow(savedDate, {
|
|
248
|
+
addSuffix: true,
|
|
249
|
+
locale,
|
|
250
|
+
});
|
|
251
|
+
const absoluteLabel = formatDateTime(
|
|
252
|
+
savedDate,
|
|
253
|
+
getSettingValue,
|
|
254
|
+
currentLocaleCode
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return currentLocaleCode.startsWith('pt')
|
|
258
|
+
? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
|
|
259
|
+
: `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
|
|
260
|
+
}, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
|
|
261
|
+
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (!open) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const storedDraft = loadDraft();
|
|
268
|
+
form.reset({
|
|
269
|
+
component_id: storedDraft?.payload.values.component_id ?? 0,
|
|
270
|
+
role_id: storedDraft?.payload.values.role_id ?? 0,
|
|
271
|
+
});
|
|
272
|
+
}, [form, loadDraft, open]);
|
|
273
|
+
|
|
186
274
|
const onSubmit = async (values: RoleFormData) => {
|
|
187
275
|
try {
|
|
188
276
|
await request({
|
|
@@ -194,12 +282,14 @@ export function ComponentRolesTab() {
|
|
|
194
282
|
},
|
|
195
283
|
});
|
|
196
284
|
toast.success(t('roleAdded'));
|
|
285
|
+
clearDraft();
|
|
197
286
|
setOpen(false);
|
|
198
287
|
form.reset();
|
|
199
288
|
refetch();
|
|
200
|
-
} catch (error:
|
|
289
|
+
} catch (error: unknown) {
|
|
201
290
|
console.error('Erro ao adicionar permissão:', error);
|
|
202
|
-
|
|
291
|
+
const message = (error as RequestError)?.response?.data?.message ?? '';
|
|
292
|
+
if (message.includes('already exists')) {
|
|
203
293
|
toast.error(t('roleAlreadyExists'));
|
|
204
294
|
} else {
|
|
205
295
|
toast.error(t('errorAddingRole'));
|
|
@@ -273,8 +363,8 @@ export function ComponentRolesTab() {
|
|
|
273
363
|
<FormItem>
|
|
274
364
|
<FormLabel>{t('component')}</FormLabel>
|
|
275
365
|
<Select
|
|
276
|
-
|
|
277
|
-
|
|
366
|
+
value={field.value ? String(field.value) : undefined}
|
|
367
|
+
onValueChange={(value) => field.onChange(Number(value))}
|
|
278
368
|
>
|
|
279
369
|
<FormControl>
|
|
280
370
|
<SelectTrigger className="w-full">
|
|
@@ -311,8 +401,8 @@ export function ComponentRolesTab() {
|
|
|
311
401
|
<FormItem>
|
|
312
402
|
<FormLabel>{t('role')}</FormLabel>
|
|
313
403
|
<Select
|
|
314
|
-
|
|
315
|
-
|
|
404
|
+
value={field.value ? String(field.value) : undefined}
|
|
405
|
+
onValueChange={(value) => field.onChange(Number(value))}
|
|
316
406
|
>
|
|
317
407
|
<FormControl>
|
|
318
408
|
<SelectTrigger className="w-full">
|
|
@@ -337,6 +427,12 @@ export function ComponentRolesTab() {
|
|
|
337
427
|
)}
|
|
338
428
|
/>
|
|
339
429
|
|
|
430
|
+
{draftStatusContent ? (
|
|
431
|
+
<p className="text-xs text-muted-foreground">
|
|
432
|
+
{draftStatusContent}
|
|
433
|
+
</p>
|
|
434
|
+
) : null}
|
|
435
|
+
|
|
340
436
|
<DialogFooter>
|
|
341
437
|
<Button type="submit">{t('save')}</Button>
|
|
342
438
|
</DialogFooter>
|
|
@@ -353,7 +449,7 @@ export function ComponentRolesTab() {
|
|
|
353
449
|
<TableHead>ID</TableHead>
|
|
354
450
|
<TableHead>{t('component')}</TableHead>
|
|
355
451
|
<TableHead>{t('role')}</TableHead>
|
|
356
|
-
<TableHead className="w-
|
|
452
|
+
<TableHead className="w-25">{t('actions')}</TableHead>
|
|
357
453
|
</TableRow>
|
|
358
454
|
</TableHeader>
|
|
359
455
|
<TableBody>
|