@hed-hog/core 0.0.299 → 0.0.300

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 (112) hide show
  1. package/dist/dashboard/dashboard/dashboard.controller.d.ts +6 -0
  2. package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
  3. package/dist/dashboard/dashboard/dashboard.service.d.ts +6 -0
  4. package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
  5. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +2 -1
  6. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-component/dashboard-component.controller.js +6 -3
  8. package/dist/dashboard/dashboard-component/dashboard-component.controller.js.map +1 -1
  9. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +7 -1
  10. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-component/dashboard-component.service.js +76 -33
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
  13. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +65 -0
  14. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  15. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +111 -0
  16. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  17. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +69 -0
  18. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  19. package/dist/dashboard/dashboard-core/dashboard-core.service.js +526 -19
  20. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  21. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +2 -0
  22. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
  23. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +2 -0
  24. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
  25. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +2 -0
  26. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts.map +1 -1
  27. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +2 -0
  28. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts.map +1 -1
  29. package/hedhog/data/dashboard.yaml +12 -6
  30. package/hedhog/data/dashboard_component_role.yaml +66 -0
  31. package/hedhog/data/dashboard_role.yaml +2 -8
  32. package/hedhog/data/route.yaml +72 -0
  33. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +333 -128
  34. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +277 -53
  35. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +179 -231
  36. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +64 -18
  37. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +1389 -0
  38. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +37 -0
  39. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +1 -1
  40. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +6 -6
  41. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +8 -8
  42. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +3 -3
  43. package/hedhog/frontend/app/dashboard/page.tsx.ejs +3 -25
  44. package/hedhog/frontend/messages/en.json +112 -2
  45. package/hedhog/frontend/messages/pt.json +111 -1
  46. package/hedhog/frontend/widgets/account-security.tsx.ejs +1 -1
  47. package/hedhog/frontend/widgets/active-users-card.tsx.ejs +2 -2
  48. package/hedhog/frontend/widgets/activity-timeline.tsx.ejs +1 -1
  49. package/hedhog/frontend/widgets/email-notifications.tsx.ejs +1 -1
  50. package/hedhog/frontend/widgets/locale-config.tsx.ejs +1 -1
  51. package/hedhog/frontend/widgets/login-history-chart.tsx.ejs +1 -1
  52. package/hedhog/frontend/widgets/mail-config.tsx.ejs +1 -1
  53. package/hedhog/frontend/widgets/mail-sent-card.tsx.ejs +2 -2
  54. package/hedhog/frontend/widgets/mail-sent-chart.tsx.ejs +1 -1
  55. package/hedhog/frontend/widgets/menus-card.tsx.ejs +2 -2
  56. package/hedhog/frontend/widgets/oauth-config.tsx.ejs +1 -1
  57. package/hedhog/frontend/widgets/permissions-card.tsx.ejs +2 -2
  58. package/hedhog/frontend/widgets/permissions-chart.tsx.ejs +1 -1
  59. package/hedhog/frontend/widgets/profile-card.tsx.ejs +1 -1
  60. package/hedhog/frontend/widgets/routes-card.tsx.ejs +2 -2
  61. package/hedhog/frontend/widgets/session-activity-chart.tsx.ejs +1 -1
  62. package/hedhog/frontend/widgets/sessions-today-card.tsx.ejs +2 -2
  63. package/hedhog/frontend/widgets/stat-access-level.tsx.ejs +1 -1
  64. package/hedhog/frontend/widgets/stat-actions-today.tsx.ejs +1 -1
  65. package/hedhog/frontend/widgets/stat-consecutive-days.tsx.ejs +1 -1
  66. package/hedhog/frontend/widgets/stat-online-time.tsx.ejs +1 -1
  67. package/hedhog/frontend/widgets/storage-config.tsx.ejs +1 -1
  68. package/hedhog/frontend/widgets/theme-config.tsx.ejs +1 -1
  69. package/hedhog/frontend/widgets/user-growth-chart.tsx.ejs +1 -1
  70. package/hedhog/frontend/widgets/user-roles.tsx.ejs +1 -1
  71. package/hedhog/frontend/widgets/user-sessions.tsx.ejs +1 -1
  72. package/hedhog/table/dashboard.yaml +6 -0
  73. package/package.json +5 -5
  74. package/src/dashboard/dashboard-component/dashboard-component.controller.ts +15 -2
  75. package/src/dashboard/dashboard-component/dashboard-component.service.ts +107 -43
  76. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +112 -1
  77. package/src/dashboard/dashboard-core/dashboard-core.service.ts +674 -19
  78. package/hedhog/frontend/app/dashboard/components/widgets/core..gitkeep.ejs +0 -11
  79. package/hedhog/frontend/app/dashboard/components/widgets/core.account-security.tsx.ejs +0 -192
  80. package/hedhog/frontend/app/dashboard/components/widgets/core.active-users-card.tsx.ejs +0 -58
  81. package/hedhog/frontend/app/dashboard/components/widgets/core.activity-timeline.tsx.ejs +0 -223
  82. package/hedhog/frontend/app/dashboard/components/widgets/core.email-notifications.tsx.ejs +0 -226
  83. package/hedhog/frontend/app/dashboard/components/widgets/core.locale-config.tsx.ejs +0 -168
  84. package/hedhog/frontend/app/dashboard/components/widgets/core.login-history-chart.tsx.ejs +0 -115
  85. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-config.tsx.ejs +0 -199
  86. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-sent-card.tsx.ejs +0 -58
  87. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-sent-chart.tsx.ejs +0 -149
  88. package/hedhog/frontend/app/dashboard/components/widgets/core.menus-card.tsx.ejs +0 -58
  89. package/hedhog/frontend/app/dashboard/components/widgets/core.oauth-config.tsx.ejs +0 -175
  90. package/hedhog/frontend/app/dashboard/components/widgets/core.permissions-card.tsx.ejs +0 -61
  91. package/hedhog/frontend/app/dashboard/components/widgets/core.permissions-chart.tsx.ejs +0 -156
  92. package/hedhog/frontend/app/dashboard/components/widgets/core.profile-card.tsx.ejs +0 -186
  93. package/hedhog/frontend/app/dashboard/components/widgets/core.routes-card.tsx.ejs +0 -58
  94. package/hedhog/frontend/app/dashboard/components/widgets/core.session-activity-chart.tsx.ejs +0 -183
  95. package/hedhog/frontend/app/dashboard/components/widgets/core.sessions-today-card.tsx.ejs +0 -62
  96. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-access-level.tsx.ejs +0 -57
  97. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-actions-today.tsx.ejs +0 -57
  98. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-consecutive-days.tsx.ejs +0 -57
  99. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-online-time.tsx.ejs +0 -57
  100. package/hedhog/frontend/app/dashboard/components/widgets/core.storage-config.tsx.ejs +0 -196
  101. package/hedhog/frontend/app/dashboard/components/widgets/core.theme-config.tsx.ejs +0 -213
  102. package/hedhog/frontend/app/dashboard/components/widgets/core.user-growth-chart.tsx.ejs +0 -210
  103. package/hedhog/frontend/app/dashboard/components/widgets/core.user-roles.tsx.ejs +0 -132
  104. package/hedhog/frontend/app/dashboard/components/widgets/core.user-sessions.tsx.ejs +0 -236
  105. package/hedhog/frontend/app/dashboard/components/widgets/finance.alerts.tsx.ejs +0 -108
  106. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-balance-kpi.tsx.ejs +0 -66
  107. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-flow-chart.tsx.ejs +0 -122
  108. package/hedhog/frontend/app/dashboard/components/widgets/finance.default-kpi.tsx.ejs +0 -63
  109. package/hedhog/frontend/app/dashboard/components/widgets/finance.payable-30d-kpi.tsx.ejs +0 -73
  110. package/hedhog/frontend/app/dashboard/components/widgets/finance.receivable-30d-kpi.tsx.ejs +0 -73
  111. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-payable.tsx.ejs +0 -123
  112. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-receivable.tsx.ejs +0 -118
@@ -13,12 +13,15 @@ import { Separator } from '@/components/ui/separator';
13
13
  import { SidebarTrigger } from '@/components/ui/sidebar';
14
14
  import { Skeleton } from '@/components/ui/skeleton';
15
15
  import { useIsMobile } from '@/components/ui/use-mobile';
16
+ import { useDebounce } from '@/hooks/use-debounce';
17
+ import { useProgress } from '@bprogress/next';
16
18
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
17
- import { IconDeviceFloppy } from '@tabler/icons-react';
19
+ import { IconDeviceFloppy, IconLoader2 } from '@tabler/icons-react';
18
20
  import { toBlob } from 'html-to-image';
19
21
  import { useTranslations } from 'next-intl';
20
22
  import { useRouter } from 'next/navigation';
21
- import { useCallback, useEffect, useState } from 'react';
23
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
24
+ import { createPortal } from 'react-dom';
22
25
  import {
23
26
  AddWidgetSelectorDialog,
24
27
  DraggableGrid,
@@ -34,6 +37,10 @@ import { WidgetRenderer } from './widget-renderer';
34
37
 
35
38
  interface DashboardContentProps {
36
39
  dashboardSlug: string;
40
+ showHeader?: boolean;
41
+ headerActionsTargetId?: string;
42
+ openWidgetPickerSignal?: number;
43
+ onOpenWidgetPickerHandled?: () => void;
37
44
  }
38
45
 
39
46
  interface DashboardComponentsPage {
@@ -44,6 +51,7 @@ interface DashboardComponentsPage {
44
51
  pageSize: number;
45
52
  prev: number | null;
46
53
  next: number | null;
54
+ modules?: string[];
47
55
  }
48
56
 
49
57
  const USER_STATS_WIDGETS = new Set([
@@ -59,6 +67,19 @@ const USER_POST_HISTORY_WIDGETS = new Set([
59
67
  ]);
60
68
 
61
69
  const USER_BOTTOM_WIDGETS = new Set(['user-roles', 'activity-timeline']);
70
+ const LAYOUT_AUTOSAVE_DELAY = 1000;
71
+
72
+ const normalizeLayoutForSave = (layout: LayoutItem[]) =>
73
+ layout.map(({ i, x, y, w, h }) => ({
74
+ i,
75
+ x,
76
+ y,
77
+ w,
78
+ h,
79
+ }));
80
+
81
+ const getLayoutSignature = (layout: LayoutItem[]) =>
82
+ JSON.stringify(normalizeLayoutForSave(layout));
62
83
 
63
84
  const getWidgetBaseSlug = (slug: string): string => {
64
85
  const parts = slug.split('.');
@@ -129,9 +150,20 @@ const normalizeUserDashboardLayout = (item: WidgetLayout): LayoutItem => {
129
150
  return layoutItem;
130
151
  };
131
152
 
132
- export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
153
+ export const DashboardContent = ({
154
+ dashboardSlug,
155
+ showHeader = true,
156
+ headerActionsTargetId,
157
+ openWidgetPickerSignal,
158
+ onOpenWidgetPickerHandled,
159
+ }: DashboardContentProps) => {
133
160
  const t = useTranslations('core.DashboardPage');
134
161
  const { request } = useApp();
162
+ const {
163
+ start: startProgress,
164
+ stop: stopProgress,
165
+ set: setProgress,
166
+ } = useProgress();
135
167
  const router = useRouter();
136
168
  const isMobile = useIsMobile();
137
169
  const isDevelopment = process.env.NODE_ENV === 'development';
@@ -142,6 +174,23 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
142
174
  const [isSaving, setIsSaving] = useState(false);
143
175
  const [componentsPage, setComponentsPage] = useState(1);
144
176
  const [componentsPageSize, setComponentsPageSize] = useState(12);
177
+ const [componentsSearchQuery, setComponentsSearchQuery] = useState('');
178
+ const [componentsModuleFilter, setComponentsModuleFilter] = useState('all');
179
+ const [headerActionsTarget, setHeaderActionsTarget] =
180
+ useState<HTMLElement | null>(null);
181
+ const lastSavedLayoutSignatureRef = useRef('[]');
182
+ const latestLayoutRef = useRef(layout);
183
+
184
+ const debouncedComponentsSearch = useDebounce(componentsSearchQuery, 400);
185
+ const debouncedLayout = useDebounce(layout, LAYOUT_AUTOSAVE_DELAY);
186
+
187
+ useEffect(() => {
188
+ latestLayoutRef.current = layout;
189
+ }, [layout]);
190
+ const excludedComponentKeys = useMemo(
191
+ () => widgets.map((widget) => getWidgetIdentityKey(widget)),
192
+ [widgets]
193
+ );
145
194
 
146
195
  const { data: dashboardAccess, isLoading: isCheckingAccess } =
147
196
  useQuery<DashboardAccessResponse>({
@@ -165,6 +214,12 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
165
214
  }
166
215
  }, [dashboardAccess, dashboardSlug, router]);
167
216
 
217
+ useEffect(() => {
218
+ setComponentsPage(1);
219
+ setComponentsSearchQuery('');
220
+ setComponentsModuleFilter('all');
221
+ }, [dashboardSlug]);
222
+
168
223
  const {
169
224
  data: availableComponentsResponse,
170
225
  isLoading: isLoadingComponents,
@@ -175,14 +230,26 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
175
230
  dashboardSlug,
176
231
  componentsPage,
177
232
  componentsPageSize,
233
+ debouncedComponentsSearch,
234
+ componentsModuleFilter,
235
+ excludedComponentKeys.join(','),
178
236
  ],
179
237
  queryFn: async () => {
238
+ const trimmedSearch = debouncedComponentsSearch.trim();
239
+
180
240
  const { data } = await request<DashboardComponentsPage>({
181
241
  url: '/dashboard-component/user',
182
242
  method: 'GET',
183
243
  params: {
184
244
  page: componentsPage,
185
245
  pageSize: componentsPageSize,
246
+ ...(trimmedSearch ? { search: trimmedSearch } : {}),
247
+ ...(componentsModuleFilter !== 'all'
248
+ ? { librarySlug: componentsModuleFilter }
249
+ : {}),
250
+ ...(excludedComponentKeys.length > 0
251
+ ? { exclude: excludedComponentKeys.join(',') }
252
+ : {}),
186
253
  },
187
254
  });
188
255
  return data;
@@ -226,9 +293,11 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
226
293
  }
227
294
  );
228
295
 
296
+ lastSavedLayoutSignatureRef.current = getLayoutSignature(gridLayout);
229
297
  setLayout(gridLayout);
230
298
  setWidgets(userLayout);
231
299
  } else {
300
+ lastSavedLayoutSignatureRef.current = '[]';
232
301
  setLayout([]);
233
302
  setWidgets([]);
234
303
  }
@@ -236,18 +305,50 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
236
305
  }
237
306
  }, [userLayout, dashboardSlug]);
238
307
 
239
- const componentsToFilter = availableComponentsResponse?.data || [];
240
- const totalAvailableComponents =
241
- availableComponentsResponse?.total ?? componentsToFilter.length;
308
+ const availableComponents = availableComponentsResponse?.data ?? [];
309
+ const totalAvailableComponents = availableComponentsResponse?.total ?? 0;
310
+ const availableComponentModules = availableComponentsResponse?.modules ?? [];
311
+ const layoutSignature = useMemo(() => getLayoutSignature(layout), [layout]);
312
+ const debouncedLayoutSignature = useMemo(
313
+ () => getLayoutSignature(debouncedLayout),
314
+ [debouncedLayout]
315
+ );
316
+ const isAutosavePending =
317
+ hasChanges && !isSaving && layoutSignature !== debouncedLayoutSignature;
318
+
319
+ useEffect(() => {
320
+ if (!isAutosavePending && !isSaving) {
321
+ stopProgress(150);
322
+ return;
323
+ }
324
+
325
+ startProgress(isSaving ? 0.55 : 0.2, 0, true);
326
+ setProgress(isSaving ? 0.8 : 0.35);
327
+ }, [isAutosavePending, isSaving, setProgress, startProgress, stopProgress]);
328
+
329
+ useEffect(() => {
330
+ const lastAvailablePage = Math.max(
331
+ availableComponentsResponse?.lastPage ?? 1,
332
+ 1
333
+ );
334
+
335
+ if (componentsPage > lastAvailablePage) {
336
+ setComponentsPage(lastAvailablePage);
337
+ }
338
+ }, [availableComponentsResponse?.lastPage, componentsPage]);
339
+
340
+ useEffect(() => {
341
+ if (
342
+ !headerActionsTargetId ||
343
+ showHeader ||
344
+ typeof document === 'undefined'
345
+ ) {
346
+ setHeaderActionsTarget(null);
347
+ return;
348
+ }
242
349
 
243
- const filteredComponents =
244
- componentsToFilter.filter(
245
- (component: DashboardComponent) =>
246
- !widgets.some(
247
- (widget) =>
248
- getWidgetIdentityKey(widget) === getWidgetIdentityKey(component)
249
- )
250
- ) || [];
350
+ setHeaderActionsTarget(document.getElementById(headerActionsTargetId));
351
+ }, [headerActionsTargetId, showHeader]);
251
352
 
252
353
  const handleLayoutChange = useCallback((newLayout: LayoutItem[]) => {
253
354
  setLayout((prevLayout) => {
@@ -261,21 +362,51 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
261
362
  });
262
363
  }, []);
263
364
 
264
- const handleSaveLayout = async () => {
265
- setIsSaving(true);
266
- try {
267
- await request({
268
- url: `/dashboard-core/layout/${dashboardSlug}`,
269
- method: 'POST',
270
- data: { layout },
271
- });
272
- setHasChanges(false);
273
- } catch (error) {
274
- console.error('❌ Erro ao salvar layout:', error);
275
- } finally {
276
- setIsSaving(false);
365
+ const handleSaveLayout = useCallback(
366
+ async (layoutToSave: LayoutItem[] = layout) => {
367
+ const normalizedLayout = normalizeLayoutForSave(layoutToSave);
368
+ const nextLayoutSignature = JSON.stringify(normalizedLayout);
369
+
370
+ if (
371
+ isSaving ||
372
+ nextLayoutSignature === lastSavedLayoutSignatureRef.current
373
+ ) {
374
+ return;
375
+ }
376
+
377
+ setIsSaving(true);
378
+ try {
379
+ await request({
380
+ url: `/dashboard-core/layout/${dashboardSlug}`,
381
+ method: 'POST',
382
+ data: { layout: normalizedLayout },
383
+ });
384
+ lastSavedLayoutSignatureRef.current = nextLayoutSignature;
385
+ setHasChanges(
386
+ getLayoutSignature(latestLayoutRef.current) !== nextLayoutSignature
387
+ );
388
+ } catch (error) {
389
+ console.error('❌ Erro ao salvar layout:', error);
390
+ } finally {
391
+ setIsSaving(false);
392
+ }
393
+ },
394
+ [dashboardSlug, isSaving, layout, request]
395
+ );
396
+
397
+ useEffect(() => {
398
+ if (!hasChanges || isSaving) {
399
+ return;
277
400
  }
278
- };
401
+
402
+ const nextLayoutSignature = getLayoutSignature(debouncedLayout);
403
+
404
+ if (nextLayoutSignature === lastSavedLayoutSignatureRef.current) {
405
+ return;
406
+ }
407
+
408
+ void handleSaveLayout(debouncedLayout);
409
+ }, [debouncedLayout, handleSaveLayout, hasChanges, isSaving]);
279
410
 
280
411
  const handleAddWidget = async (slugs: string[]) => {
281
412
  if (!slugs.length) return;
@@ -331,6 +462,10 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
331
462
  cacheBust: true,
332
463
  pixelRatio: 2,
333
464
  backgroundColor: '#ffffff',
465
+ filter: (node) =>
466
+ !(
467
+ node instanceof HTMLElement && node.dataset.widgetAction === 'true'
468
+ ),
334
469
  });
335
470
 
336
471
  if (!screenshot) {
@@ -376,28 +511,30 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
376
511
  if (isCheckingAccess || isLoadingLayout) {
377
512
  return (
378
513
  <>
379
- <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
380
- <div className="flex w-full items-center justify-between gap-2 px-4">
381
- <div className="flex items-center gap-2">
382
- <SidebarTrigger className="-ml-1" />
383
- <Separator
384
- orientation="vertical"
385
- className="mr-2 data-[orientation=vertical]:h-4"
386
- />
387
- <Breadcrumb>
388
- <BreadcrumbList>
389
- <BreadcrumbItem className="hidden md:block">
390
- <BreadcrumbLink href="#">{t('dashboard')}</BreadcrumbLink>
391
- </BreadcrumbItem>
392
- <BreadcrumbSeparator className="hidden md:block" />
393
- <BreadcrumbItem>
394
- <BreadcrumbPage>{t('overview')}</BreadcrumbPage>
395
- </BreadcrumbItem>
396
- </BreadcrumbList>
397
- </Breadcrumb>
514
+ {showHeader ? (
515
+ <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
516
+ <div className="flex w-full items-center justify-between gap-2 px-4">
517
+ <div className="flex items-center gap-2">
518
+ <SidebarTrigger className="-ml-1" />
519
+ <Separator
520
+ orientation="vertical"
521
+ className="mr-2 data-[orientation=vertical]:h-4"
522
+ />
523
+ <Breadcrumb>
524
+ <BreadcrumbList>
525
+ <BreadcrumbItem className="hidden md:block">
526
+ <BreadcrumbLink href="#">{t('dashboard')}</BreadcrumbLink>
527
+ </BreadcrumbItem>
528
+ <BreadcrumbSeparator className="hidden md:block" />
529
+ <BreadcrumbItem>
530
+ <BreadcrumbPage>{t('overview')}</BreadcrumbPage>
531
+ </BreadcrumbItem>
532
+ </BreadcrumbList>
533
+ </Breadcrumb>
534
+ </div>
398
535
  </div>
399
- </div>
400
- </header>
536
+ </header>
537
+ ) : null}
401
538
  <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
402
539
  <Skeleton className="h-32 w-full" />
403
540
  <Skeleton className="h-32 w-full" />
@@ -410,28 +547,30 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
410
547
  if (!dashboardAccess?.hasAccess) {
411
548
  return (
412
549
  <>
413
- <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
414
- <div className="flex w-full items-center justify-between gap-2 px-4">
415
- <div className="flex items-center gap-2">
416
- <SidebarTrigger className="-ml-1" />
417
- <Separator
418
- orientation="vertical"
419
- className="mr-2 data-[orientation=vertical]:h-4"
420
- />
421
- <Breadcrumb>
422
- <BreadcrumbList>
423
- <BreadcrumbItem className="hidden md:block">
424
- <BreadcrumbLink href="#">{t('dashboard')}</BreadcrumbLink>
425
- </BreadcrumbItem>
426
- <BreadcrumbSeparator className="hidden md:block" />
427
- <BreadcrumbItem>
428
- <BreadcrumbPage>{t('accessDenied')}</BreadcrumbPage>
429
- </BreadcrumbItem>
430
- </BreadcrumbList>
431
- </Breadcrumb>
550
+ {showHeader ? (
551
+ <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
552
+ <div className="flex w-full items-center justify-between gap-2 px-4">
553
+ <div className="flex items-center gap-2">
554
+ <SidebarTrigger className="-ml-1" />
555
+ <Separator
556
+ orientation="vertical"
557
+ className="mr-2 data-[orientation=vertical]:h-4"
558
+ />
559
+ <Breadcrumb>
560
+ <BreadcrumbList>
561
+ <BreadcrumbItem className="hidden md:block">
562
+ <BreadcrumbLink href="#">{t('dashboard')}</BreadcrumbLink>
563
+ </BreadcrumbItem>
564
+ <BreadcrumbSeparator className="hidden md:block" />
565
+ <BreadcrumbItem>
566
+ <BreadcrumbPage>{t('accessDenied')}</BreadcrumbPage>
567
+ </BreadcrumbItem>
568
+ </BreadcrumbList>
569
+ </Breadcrumb>
570
+ </div>
432
571
  </div>
433
- </div>
434
- </header>
572
+ </header>
573
+ ) : null}
435
574
  <div className="flex flex-1 flex-col items-center justify-center gap-4 p-4 pt-0">
436
575
  <div className="text-center">
437
576
  <h2 className="text-2xl font-bold">{t('accessDenied')}</h2>
@@ -445,67 +584,109 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
445
584
  }
446
585
 
447
586
  const dashboardName = dashboardAccess?.dashboard?.name || dashboardSlug;
587
+ const autosaveStatusLabel = isSaving
588
+ ? 'Salvando layout...'
589
+ : isAutosavePending
590
+ ? 'Salvando automaticamente...'
591
+ : null;
592
+
593
+ const dashboardActions = (
594
+ <>
595
+ {autosaveStatusLabel ? (
596
+ <div className="text-muted-foreground bg-muted inline-flex items-center gap-2 rounded-md px-2.5 py-1 text-xs">
597
+ <IconLoader2 className="size-3.5 animate-spin" />
598
+ <span>{autosaveStatusLabel}</span>
599
+ </div>
600
+ ) : null}
601
+ {hasChanges ? (
602
+ <Button
603
+ size="sm"
604
+ variant="default"
605
+ className="gap-1 px-2 sm:gap-2 sm:px-3"
606
+ onClick={() => void handleSaveLayout()}
607
+ disabled={isSaving}
608
+ aria-label={isSaving ? t('saving') : t('saveLayout')}
609
+ >
610
+ {isSaving ? (
611
+ <IconLoader2 className="size-4 animate-spin" />
612
+ ) : (
613
+ <IconDeviceFloppy className="size-4" />
614
+ )}
615
+ <span className="hidden sm:inline">
616
+ {isSaving ? t('saving') : t('saveLayout')}
617
+ </span>
618
+ </Button>
619
+ ) : null}
620
+ <AddWidgetSelectorDialog
621
+ availableComponents={availableComponents}
622
+ totalItems={totalAvailableComponents}
623
+ currentPage={availableComponentsResponse?.page ?? componentsPage}
624
+ pageSize={availableComponentsResponse?.pageSize ?? componentsPageSize}
625
+ isLoading={isLoadingComponents}
626
+ searchQuery={componentsSearchQuery}
627
+ onSearchQueryChange={(value) => {
628
+ setComponentsSearchQuery(value);
629
+ setComponentsPage(1);
630
+ }}
631
+ moduleFilter={componentsModuleFilter}
632
+ modules={availableComponentModules}
633
+ onModuleFilterChange={(value) => {
634
+ setComponentsModuleFilter(value);
635
+ setComponentsPage(1);
636
+ }}
637
+ onPageChange={setComponentsPage}
638
+ onPageSizeChange={(nextPageSize) => {
639
+ setComponentsPageSize(nextPageSize);
640
+ setComponentsPage(1);
641
+ }}
642
+ onAdd={handleAddWidget}
643
+ openSignal={openWidgetPickerSignal}
644
+ onOpenSignalHandled={onOpenWidgetPickerHandled}
645
+ />
646
+ </>
647
+ );
648
+
649
+ const hasExternalActionTarget = !showHeader && Boolean(headerActionsTarget);
448
650
 
449
651
  return (
450
652
  <>
451
- <header className="flex min-h-16 shrink-0 items-center gap-2 py-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 sm:h-16 sm:py-0">
452
- <div className="flex w-full flex-col gap-2 px-4 sm:flex-row sm:items-center sm:justify-between">
453
- <div className="flex min-w-0 items-center gap-2">
454
- <SidebarTrigger className="-ml-1" />
455
- <Separator
456
- orientation="vertical"
457
- className="mr-2 data-[orientation=vertical]:h-4"
458
- />
459
- <Breadcrumb className="min-w-0">
460
- <BreadcrumbList>
461
- <BreadcrumbItem className="hidden md:block">
462
- <BreadcrumbLink href="#">{t('dashboard')}</BreadcrumbLink>
463
- </BreadcrumbItem>
464
- <BreadcrumbSeparator className="hidden md:block" />
465
- <BreadcrumbItem>
466
- <BreadcrumbPage className="truncate">
467
- {dashboardName}
468
- </BreadcrumbPage>
469
- </BreadcrumbItem>
470
- </BreadcrumbList>
471
- </Breadcrumb>
472
- </div>
473
- <div className="flex w-full items-center justify-end gap-2 sm:w-auto">
474
- {hasChanges && (
475
- <Button
476
- size="sm"
477
- variant="default"
478
- className="gap-1 px-2 sm:gap-2 sm:px-3"
479
- onClick={handleSaveLayout}
480
- disabled={isSaving}
481
- aria-label={isSaving ? t('saving') : t('saveLayout')}
482
- >
483
- <IconDeviceFloppy className="size-4" />
484
- <span className="hidden sm:inline">
485
- {isSaving ? t('saving') : t('saveLayout')}
486
- </span>
487
- </Button>
488
- )}
489
- <AddWidgetSelectorDialog
490
- availableComponents={filteredComponents}
491
- totalItems={totalAvailableComponents}
492
- currentPage={availableComponentsResponse?.page ?? componentsPage}
493
- pageSize={
494
- availableComponentsResponse?.pageSize ?? componentsPageSize
495
- }
496
- isLoading={isLoadingComponents}
497
- onPageChange={setComponentsPage}
498
- onPageSizeChange={(nextPageSize) => {
499
- setComponentsPageSize(nextPageSize);
500
- setComponentsPage(1);
501
- }}
502
- onAdd={handleAddWidget}
503
- currentSlug={dashboardSlug}
504
- />
653
+ {showHeader ? (
654
+ <header className="flex min-h-16 shrink-0 items-center gap-2 py-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 sm:h-16 sm:py-0">
655
+ <div className="flex w-full flex-col gap-2 px-4 sm:flex-row sm:items-center sm:justify-between">
656
+ <div className="flex min-w-0 items-center gap-2">
657
+ <SidebarTrigger className="-ml-1" />
658
+ <Separator
659
+ orientation="vertical"
660
+ className="mr-2 data-[orientation=vertical]:h-4"
661
+ />
662
+ <Breadcrumb className="min-w-0">
663
+ <BreadcrumbList>
664
+ <BreadcrumbItem className="hidden md:block">
665
+ <BreadcrumbLink href="#">{t('dashboard')}</BreadcrumbLink>
666
+ </BreadcrumbItem>
667
+ <BreadcrumbSeparator className="hidden md:block" />
668
+ <BreadcrumbItem>
669
+ <BreadcrumbPage className="truncate">
670
+ {dashboardName}
671
+ </BreadcrumbPage>
672
+ </BreadcrumbItem>
673
+ </BreadcrumbList>
674
+ </Breadcrumb>
675
+ </div>
676
+ <div className="flex w-full items-center justify-end gap-2 sm:w-auto">
677
+ {dashboardActions}
678
+ </div>
505
679
  </div>
680
+ </header>
681
+ ) : !hasExternalActionTarget ? (
682
+ <div className="flex w-full items-center justify-end gap-2 px-4 pt-2">
683
+ {dashboardActions}
506
684
  </div>
507
- </header>
508
- <div className="flex flex-1 flex-col gap-4 overflow-auto p-4 pt-0">
685
+ ) : null}
686
+ {hasExternalActionTarget && headerActionsTarget
687
+ ? createPortal(dashboardActions, headerActionsTarget)
688
+ : null}
689
+ <div className="flex flex-1 flex-col gap-4 overflow-auto pt-0">
509
690
  {widgets.length > 0 ? (
510
691
  <div className="min-h-105 sm:min-h-130 lg:min-h-150">
511
692
  <DraggableGrid
@@ -514,6 +695,7 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
514
695
  onLayoutChange={handleLayoutChange}
515
696
  cols={12}
516
697
  rowHeight={80}
698
+ compactType={null}
517
699
  preventCollision
518
700
  isDraggable={!isMobile}
519
701
  isResizable={!isMobile}
@@ -532,10 +714,33 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
532
714
  </p>
533
715
  <div className="mt-4">
534
716
  <AddWidgetSelectorDialog
535
- availableComponents={filteredComponents}
717
+ availableComponents={availableComponents}
718
+ totalItems={totalAvailableComponents}
719
+ currentPage={
720
+ availableComponentsResponse?.page ?? componentsPage
721
+ }
722
+ pageSize={
723
+ availableComponentsResponse?.pageSize ?? componentsPageSize
724
+ }
536
725
  isLoading={isLoadingComponents}
726
+ searchQuery={componentsSearchQuery}
727
+ onSearchQueryChange={(value) => {
728
+ setComponentsSearchQuery(value);
729
+ setComponentsPage(1);
730
+ }}
731
+ moduleFilter={componentsModuleFilter}
732
+ modules={availableComponentModules}
733
+ onModuleFilterChange={(value) => {
734
+ setComponentsModuleFilter(value);
735
+ setComponentsPage(1);
736
+ }}
737
+ onPageChange={setComponentsPage}
738
+ onPageSizeChange={(nextPageSize) => {
739
+ setComponentsPageSize(nextPageSize);
740
+ setComponentsPage(1);
741
+ }}
537
742
  onAdd={handleAddWidget}
538
- currentSlug={dashboardSlug}
743
+ buttonVariant="default"
539
744
  />
540
745
  </div>
541
746
  </div>