@hed-hog/core 0.0.304 → 0.0.306
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/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/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 +5 -5
- 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/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
|
@@ -39,6 +39,13 @@ interface DashboardContentProps {
|
|
|
39
39
|
dashboardSlug: string;
|
|
40
40
|
showHeader?: boolean;
|
|
41
41
|
headerActionsTargetId?: string;
|
|
42
|
+
externalHeaderActions?: boolean;
|
|
43
|
+
requestSaveSignal?: number;
|
|
44
|
+
onRuntimeActionsStateChange?: (state: {
|
|
45
|
+
hasChanges: boolean;
|
|
46
|
+
isSaving: boolean;
|
|
47
|
+
isAutosavePending: boolean;
|
|
48
|
+
}) => void;
|
|
42
49
|
openWidgetPickerSignal?: number;
|
|
43
50
|
onOpenWidgetPickerHandled?: () => void;
|
|
44
51
|
}
|
|
@@ -154,6 +161,9 @@ export const DashboardContent = ({
|
|
|
154
161
|
dashboardSlug,
|
|
155
162
|
showHeader = true,
|
|
156
163
|
headerActionsTargetId,
|
|
164
|
+
externalHeaderActions = false,
|
|
165
|
+
requestSaveSignal,
|
|
166
|
+
onRuntimeActionsStateChange,
|
|
157
167
|
openWidgetPickerSignal,
|
|
158
168
|
onOpenWidgetPickerHandled,
|
|
159
169
|
}: DashboardContentProps) => {
|
|
@@ -180,6 +190,7 @@ export const DashboardContent = ({
|
|
|
180
190
|
useState<HTMLElement | null>(null);
|
|
181
191
|
const lastSavedLayoutSignatureRef = useRef('[]');
|
|
182
192
|
const latestLayoutRef = useRef(layout);
|
|
193
|
+
const lastHandledSaveSignalRef = useRef<number | undefined>(undefined);
|
|
183
194
|
|
|
184
195
|
const debouncedComponentsSearch = useDebounce(componentsSearchQuery, 400);
|
|
185
196
|
const debouncedLayout = useDebounce(layout, LAYOUT_AUTOSAVE_DELAY);
|
|
@@ -316,6 +327,14 @@ export const DashboardContent = ({
|
|
|
316
327
|
const isAutosavePending =
|
|
317
328
|
hasChanges && !isSaving && layoutSignature !== debouncedLayoutSignature;
|
|
318
329
|
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
onRuntimeActionsStateChange?.({
|
|
332
|
+
hasChanges,
|
|
333
|
+
isSaving,
|
|
334
|
+
isAutosavePending,
|
|
335
|
+
});
|
|
336
|
+
}, [hasChanges, isAutosavePending, isSaving, onRuntimeActionsStateChange]);
|
|
337
|
+
|
|
319
338
|
useEffect(() => {
|
|
320
339
|
if (!isAutosavePending && !isSaving) {
|
|
321
340
|
stopProgress(150);
|
|
@@ -394,6 +413,22 @@ export const DashboardContent = ({
|
|
|
394
413
|
[dashboardSlug, isSaving, layout, request]
|
|
395
414
|
);
|
|
396
415
|
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
if (!requestSaveSignal) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (lastHandledSaveSignalRef.current === requestSaveSignal) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
lastHandledSaveSignalRef.current = requestSaveSignal;
|
|
426
|
+
|
|
427
|
+
if (!isSaving && hasChanges) {
|
|
428
|
+
void handleSaveLayout();
|
|
429
|
+
}
|
|
430
|
+
}, [handleSaveLayout, hasChanges, isSaving, requestSaveSignal]);
|
|
431
|
+
|
|
397
432
|
useEffect(() => {
|
|
398
433
|
if (!hasChanges || isSaving) {
|
|
399
434
|
return;
|
|
@@ -646,7 +681,8 @@ export const DashboardContent = ({
|
|
|
646
681
|
</>
|
|
647
682
|
);
|
|
648
683
|
|
|
649
|
-
const hasExternalActionTarget =
|
|
684
|
+
const hasExternalActionTarget =
|
|
685
|
+
!externalHeaderActions && !showHeader && Boolean(headerActionsTarget);
|
|
650
686
|
|
|
651
687
|
return (
|
|
652
688
|
<>
|
|
@@ -678,7 +714,7 @@ export const DashboardContent = ({
|
|
|
678
714
|
</div>
|
|
679
715
|
</div>
|
|
680
716
|
</header>
|
|
681
|
-
) : !hasExternalActionTarget ? (
|
|
717
|
+
) : !externalHeaderActions && !hasExternalActionTarget ? (
|
|
682
718
|
<div className="flex w-full items-center justify-end gap-2 px-4 pt-2">
|
|
683
719
|
{dashboardActions}
|
|
684
720
|
</div>
|
|
@@ -686,6 +722,35 @@ export const DashboardContent = ({
|
|
|
686
722
|
{hasExternalActionTarget && headerActionsTarget
|
|
687
723
|
? createPortal(dashboardActions, headerActionsTarget)
|
|
688
724
|
: null}
|
|
725
|
+
{externalHeaderActions ? (
|
|
726
|
+
<AddWidgetSelectorDialog
|
|
727
|
+
availableComponents={availableComponents}
|
|
728
|
+
totalItems={totalAvailableComponents}
|
|
729
|
+
currentPage={availableComponentsResponse?.page ?? componentsPage}
|
|
730
|
+
pageSize={availableComponentsResponse?.pageSize ?? componentsPageSize}
|
|
731
|
+
isLoading={isLoadingComponents}
|
|
732
|
+
searchQuery={componentsSearchQuery}
|
|
733
|
+
onSearchQueryChange={(value) => {
|
|
734
|
+
setComponentsSearchQuery(value);
|
|
735
|
+
setComponentsPage(1);
|
|
736
|
+
}}
|
|
737
|
+
moduleFilter={componentsModuleFilter}
|
|
738
|
+
modules={availableComponentModules}
|
|
739
|
+
onModuleFilterChange={(value) => {
|
|
740
|
+
setComponentsModuleFilter(value);
|
|
741
|
+
setComponentsPage(1);
|
|
742
|
+
}}
|
|
743
|
+
onPageChange={setComponentsPage}
|
|
744
|
+
onPageSizeChange={(nextPageSize) => {
|
|
745
|
+
setComponentsPageSize(nextPageSize);
|
|
746
|
+
setComponentsPage(1);
|
|
747
|
+
}}
|
|
748
|
+
onAdd={handleAddWidget}
|
|
749
|
+
hideTrigger
|
|
750
|
+
openSignal={openWidgetPickerSignal}
|
|
751
|
+
onOpenSignalHandled={onOpenWidgetPickerHandled}
|
|
752
|
+
/>
|
|
753
|
+
) : null}
|
|
689
754
|
<div className="flex flex-1 flex-col gap-4 overflow-auto pt-0">
|
|
690
755
|
{widgets.length > 0 ? (
|
|
691
756
|
<div className="min-h-105 sm:min-h-130 lg:min-h-150">
|
|
@@ -64,6 +64,7 @@ interface AddWidgetSelectorDialogProps {
|
|
|
64
64
|
onAdd: (slugs: string[]) => Promise<void> | void;
|
|
65
65
|
buttonLabel?: string;
|
|
66
66
|
buttonVariant?: 'default' | 'outline';
|
|
67
|
+
hideTrigger?: boolean;
|
|
67
68
|
openSignal?: number;
|
|
68
69
|
onOpenSignalHandled?: () => void;
|
|
69
70
|
}
|
|
@@ -153,6 +154,7 @@ export function AddWidgetSelectorDialog({
|
|
|
153
154
|
onAdd,
|
|
154
155
|
buttonLabel,
|
|
155
156
|
buttonVariant = 'outline',
|
|
157
|
+
hideTrigger = false,
|
|
156
158
|
openSignal,
|
|
157
159
|
onOpenSignalHandled,
|
|
158
160
|
}: AddWidgetSelectorDialogProps) {
|
|
@@ -210,15 +212,17 @@ export function AddWidgetSelectorDialog({
|
|
|
210
212
|
|
|
211
213
|
return (
|
|
212
214
|
<>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
215
|
+
{!hideTrigger ? (
|
|
216
|
+
<Button
|
|
217
|
+
size="sm"
|
|
218
|
+
variant={buttonVariant}
|
|
219
|
+
className="cursor-pointer gap-2"
|
|
220
|
+
onClick={() => setOpenWidgets(true)}
|
|
221
|
+
>
|
|
222
|
+
<IconPlus className="size-4" />
|
|
223
|
+
{buttonLabel || tMenu('addWidgets')}
|
|
224
|
+
</Button>
|
|
225
|
+
) : null}
|
|
222
226
|
|
|
223
227
|
<Sheet
|
|
224
228
|
open={openWidgets}
|
|
@@ -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 =
|