@donotdev/ui 0.0.12 → 0.0.14

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 (159) hide show
  1. package/dist/components/auth/AuthMenu.d.ts.map +1 -1
  2. package/dist/components/auth/AuthMenu.js +19 -20
  3. package/dist/components/common/FeatureCard.d.ts +3 -1
  4. package/dist/components/common/FeatureCard.d.ts.map +1 -1
  5. package/dist/components/common/FeatureCard.js +3 -3
  6. package/dist/components/common/ProgressBar.js +2 -2
  7. package/dist/components/common/RedirectOverlay.js +1 -1
  8. package/dist/components/common/TechBento.d.ts +14 -2
  9. package/dist/components/common/TechBento.d.ts.map +1 -1
  10. package/dist/components/common/TechBento.js +8 -9
  11. package/dist/components/cookie-consent/CookieConsent.d.ts.map +1 -1
  12. package/dist/components/cookie-consent/CookieConsent.js +6 -7
  13. package/dist/components/layout/GameContainer.d.ts +24 -8
  14. package/dist/components/layout/GameContainer.d.ts.map +1 -1
  15. package/dist/components/layout/GameContainer.js +21 -3
  16. package/dist/components/layout/GameFlow.d.ts.map +1 -1
  17. package/dist/components/layout/GameFlow.js +27 -11
  18. package/dist/components/layout/components/DropdownNavigation.d.ts.map +1 -1
  19. package/dist/components/layout/components/DropdownNavigation.js +3 -12
  20. package/dist/components/layout/components/FloatingLanguageSwitcher.js +1 -1
  21. package/dist/components/layout/components/Notifications.d.ts +1 -3
  22. package/dist/components/layout/components/Notifications.d.ts.map +1 -1
  23. package/dist/components/layout/components/Notifications.js +4 -2
  24. package/dist/components/layout/components/header/AppBranding.d.ts.map +1 -1
  25. package/dist/components/layout/components/header/AppBranding.js +2 -1
  26. package/dist/components/layout/components/header/AppIcon.d.ts.map +1 -1
  27. package/dist/components/layout/components/header/AppIcon.js +5 -2
  28. package/dist/components/layout/components/header/CacheSettings.d.ts.map +1 -1
  29. package/dist/components/layout/components/header/CacheSettings.js +4 -2
  30. package/dist/components/layout/components/header/HeaderNavigation.d.ts +6 -0
  31. package/dist/components/layout/components/header/HeaderNavigation.d.ts.map +1 -1
  32. package/dist/components/layout/components/header/HeaderNavigation.js +12 -2
  33. package/dist/components/license/LicenseWatermark.d.ts.map +1 -1
  34. package/dist/components/license/LicenseWatermark.js +3 -1
  35. package/dist/crud/components/CrudCardLink.d.ts +17 -0
  36. package/dist/crud/components/CrudCardLink.d.ts.map +1 -0
  37. package/dist/crud/components/CrudCardLink.js +17 -0
  38. package/dist/crud/components/EntityCardList.d.ts.map +1 -1
  39. package/dist/crud/components/EntityCardList.js +32 -81
  40. package/dist/crud/components/EntityDisplayRenderer.d.ts +1 -1
  41. package/dist/crud/components/EntityDisplayRenderer.d.ts.map +1 -1
  42. package/dist/crud/components/EntityDisplayRenderer.js +8 -4
  43. package/dist/crud/components/EntityFormRenderer.d.ts +1 -1
  44. package/dist/crud/components/EntityFormRenderer.d.ts.map +1 -1
  45. package/dist/crud/components/EntityFormRenderer.js +29 -18
  46. package/dist/crud/components/EntityList.d.ts +1 -1
  47. package/dist/crud/components/EntityList.d.ts.map +1 -1
  48. package/dist/crud/components/EntityList.js +8 -10
  49. package/dist/crud/components/EntityRecommendations.d.ts +28 -0
  50. package/dist/crud/components/EntityRecommendations.d.ts.map +1 -0
  51. package/dist/crud/components/EntityRecommendations.js +31 -0
  52. package/dist/crud/components/Form.js +1 -1
  53. package/dist/crud/components/index.d.ts +2 -1
  54. package/dist/crud/components/index.d.ts.map +1 -1
  55. package/dist/crud/components/index.js +1 -0
  56. package/dist/index.js +4 -4
  57. package/dist/internal/common/RouteErrorFallback.d.ts.map +1 -1
  58. package/dist/internal/common/RouteErrorFallback.js +3 -3
  59. package/dist/internal/devtools/components/AuthDebugButton.js +1 -1
  60. package/dist/internal/devtools/components/ConfigTab.js +1 -1
  61. package/dist/internal/devtools/components/CookieTab.js +1 -1
  62. package/dist/internal/devtools/components/DesignTab.d.ts.map +1 -1
  63. package/dist/internal/devtools/components/DesignTab.js +5 -4
  64. package/dist/internal/devtools/components/LayoutReset.d.ts.map +1 -1
  65. package/dist/internal/devtools/components/LayoutReset.js +2 -0
  66. package/dist/internal/devtools/components/StoresTab.d.ts.map +1 -1
  67. package/dist/internal/devtools/components/StoresTab.js +5 -2
  68. package/dist/internal/devtools/utils/envVarDiscovery.d.ts +1 -0
  69. package/dist/internal/devtools/utils/envVarDiscovery.d.ts.map +1 -1
  70. package/dist/internal/devtools/utils/envVarDiscovery.js +5 -0
  71. package/dist/internal/devtools/utils/virtualModuleInspector.d.ts.map +1 -1
  72. package/dist/internal/devtools/utils/virtualModuleInspector.js +27 -21
  73. package/dist/internal/initializers/BaseStoresInitializer.d.ts.map +1 -1
  74. package/dist/internal/initializers/BaseStoresInitializer.js +30 -6
  75. package/dist/internal/layout/components/AutoMetaTags.d.ts.map +1 -1
  76. package/dist/internal/layout/components/AutoMetaTags.js +10 -8
  77. package/dist/internal/layout/components/FontPreloadLinks.d.ts +16 -0
  78. package/dist/internal/layout/components/FontPreloadLinks.d.ts.map +1 -0
  79. package/dist/internal/layout/components/FontPreloadLinks.js +32 -0
  80. package/dist/internal/layout/components/PerformanceHints.d.ts +7 -12
  81. package/dist/internal/layout/components/PerformanceHints.d.ts.map +1 -1
  82. package/dist/internal/layout/components/PerformanceHints.js +8 -12
  83. package/dist/internal/layout/components/footer/useLegalLinks.d.ts +6 -5
  84. package/dist/internal/layout/components/footer/useLegalLinks.d.ts.map +1 -1
  85. package/dist/internal/layout/components/footer/useLegalLinks.js +6 -2
  86. package/dist/internal/layout/zones/DnDevFooter.d.ts +6 -0
  87. package/dist/internal/layout/zones/DnDevFooter.d.ts.map +1 -1
  88. package/dist/internal/layout/zones/DnDevFooter.js +10 -4
  89. package/dist/internal/layout/zones/DnDevHeader.d.ts +7 -0
  90. package/dist/internal/layout/zones/DnDevHeader.d.ts.map +1 -1
  91. package/dist/internal/layout/zones/DnDevHeader.js +7 -0
  92. package/dist/internal/layout/zones/DnDevMergedBar.d.ts +7 -0
  93. package/dist/internal/layout/zones/DnDevMergedBar.d.ts.map +1 -1
  94. package/dist/internal/layout/zones/DnDevMergedBar.js +10 -1
  95. package/dist/internal/layout/zones/DnDevSidebar.d.ts +4 -0
  96. package/dist/internal/layout/zones/DnDevSidebar.d.ts.map +1 -1
  97. package/dist/internal/layout/zones/DnDevSidebar.js +13 -1
  98. package/dist/next.d.ts +1 -0
  99. package/dist/next.d.ts.map +1 -1
  100. package/dist/next.js +1 -0
  101. package/dist/routing/404.js +3 -3
  102. package/dist/routing/AuthGuard.d.ts +1 -1
  103. package/dist/routing/AuthGuard.d.ts.map +1 -1
  104. package/dist/routing/AuthGuard.js +3 -1
  105. package/dist/routing/AuthGuardFallback.js +2 -2
  106. package/dist/routing/GoTo.d.ts.map +1 -1
  107. package/dist/routing/GoTo.js +3 -1
  108. package/dist/routing/GoToDialog.d.ts.map +1 -1
  109. package/dist/routing/GoToDialog.js +2 -7
  110. package/dist/routing/GoToInput.d.ts +0 -3
  111. package/dist/routing/GoToInput.d.ts.map +1 -1
  112. package/dist/routing/GoToInput.js +4 -2
  113. package/dist/routing/Link.js +1 -1
  114. package/dist/routing/NavigationItem.d.ts +29 -7
  115. package/dist/routing/NavigationItem.d.ts.map +1 -1
  116. package/dist/routing/NavigationItem.js +22 -6
  117. package/dist/routing/hooks/hooks.next.js +1 -1
  118. package/dist/routing/hooks/hooks.vite.js +1 -1
  119. package/dist/routing/hooks/useRedirectGuard.next.d.ts.map +1 -1
  120. package/dist/routing/hooks/useRedirectGuard.next.js +9 -8
  121. package/dist/routing/hooks/useRedirectGuard.vite.d.ts.map +1 -1
  122. package/dist/routing/hooks/useRedirectGuard.vite.js +9 -8
  123. package/dist/routing/hooks/useSearchParams.next.d.ts +18 -1
  124. package/dist/routing/hooks/useSearchParams.next.d.ts.map +1 -1
  125. package/dist/routing/hooks/useSearchParams.next.js +16 -0
  126. package/dist/routing/hooks/useSearchParams.vite.d.ts +16 -0
  127. package/dist/routing/hooks/useSearchParams.vite.d.ts.map +1 -1
  128. package/dist/routing/hooks/useSearchParams.vite.js +17 -1
  129. package/dist/routing/index.d.ts.map +1 -1
  130. package/dist/routing/index.js +2 -0
  131. package/dist/routing/useNavigation.d.ts +30 -0
  132. package/dist/routing/useNavigation.d.ts.map +1 -1
  133. package/dist/routing/useNavigation.js +40 -3
  134. package/dist/routing/useRouteDiscovery.d.ts +2 -2
  135. package/dist/routing/useRouteDiscovery.d.ts.map +1 -1
  136. package/dist/routing/useRouteDiscovery.js +10 -4
  137. package/dist/styles/index.css +366 -120
  138. package/dist/utils/index.d.ts +1 -0
  139. package/dist/utils/index.d.ts.map +1 -1
  140. package/dist/utils/index.js +1 -0
  141. package/dist/utils/sanitizeSvg.d.ts +13 -0
  142. package/dist/utils/sanitizeSvg.d.ts.map +1 -0
  143. package/dist/utils/sanitizeSvg.js +47 -0
  144. package/dist/utils/useBillingVisibility.d.ts.map +1 -1
  145. package/dist/utils/useBillingVisibility.js +0 -7
  146. package/dist/utils/useCrudSafe.d.ts +0 -2
  147. package/dist/utils/useCrudSafe.d.ts.map +1 -1
  148. package/dist/utils/useFormStoreSafe.d.ts +3 -1
  149. package/dist/utils/useFormStoreSafe.d.ts.map +1 -1
  150. package/dist/utils/useFormStoreSafe.js +4 -5
  151. package/dist/vite-routing/AppRoutes.d.ts +19 -8
  152. package/dist/vite-routing/AppRoutes.d.ts.map +1 -1
  153. package/dist/vite-routing/AppRoutes.js +0 -3
  154. package/package.json +15 -11
  155. package/assets/fonts/fonts.css +0 -206
  156. package/dist/dndev.css +0 -10673
  157. package/dist/routing/Navigate.d.ts +0 -10
  158. package/dist/routing/Navigate.d.ts.map +0 -1
  159. package/dist/routing/Navigate.js +0 -10
@@ -97,7 +97,9 @@ function CacheSettings() {
97
97
  tx.executeSql('SELECT name FROM sqlite_master WHERE type="table"', [], (tx, results) => {
98
98
  for (let i = 0; i < results.rows.length; i++) {
99
99
  const tableName = results.rows.item(i).name;
100
- tx.executeSql(`DROP TABLE IF EXISTS ${tableName}`);
100
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
101
+ tx.executeSql(`DROP TABLE IF EXISTS ${tableName}`);
102
+ }
101
103
  }
102
104
  });
103
105
  });
@@ -205,7 +207,7 @@ function CacheSettings() {
205
207
  setIsClearing(false);
206
208
  }
207
209
  };
208
- return (_jsxs(Stack, { gap: "medium", children: [_jsx(Stack, { gap: "medium", children: Object.entries(cacheOptions).map(([key, checked]) => (_jsxs(Stack, { direction: "row", align: "center", gap: "tight", children: [_jsx(Checkbox, { id: key, checked: checked, onCheckedChange: (checked) => setCacheOptions((prev) => ({ ...prev, [key]: !!checked })) }), _jsx("label", { htmlFor: key, style: {
210
+ return (_jsxs(Stack, { children: [_jsx(Stack, { children: Object.entries(cacheOptions).map(([key, checked]) => (_jsxs(Stack, { direction: "row", align: "center", gap: "tight", children: [_jsx(Checkbox, { id: key, checked: checked, onCheckedChange: (checked) => setCacheOptions((prev) => ({ ...prev, [key]: !!checked })) }), _jsx("label", { htmlFor: key, style: {
209
211
  fontSize: 'var(--font-size-sm)',
210
212
  fontWeight: 500,
211
213
  lineHeight: 1,
@@ -13,6 +13,12 @@ export interface HeaderNavigationProps {
13
13
  * @default 'auto'
14
14
  */
15
15
  display?: (typeof DISPLAY)[keyof typeof DISPLAY];
16
+ /**
17
+ * Single-item mode: render one route as a standalone Button.
18
+ * Resolves label, icon, and active state from the navigation store.
19
+ * Renders null if the path is not found or inaccessible.
20
+ */
21
+ path?: string;
16
22
  }
17
23
  /**
18
24
  * HeaderNavigation - DISPLAY-aware adaptive navigation component
@@ -1 +1 @@
1
- {"version":3,"file":"HeaderNavigation.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/components/header/HeaderNavigation.tsx"],"names":[],"mappings":"AAoBA,OAAO,EACL,OAAO,EAIR,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,MAAM,WAAW,qBAAqB;IACpC,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,OAAO,OAAO,CAAC,CAAC;CAClD;AAED;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,gBAAgB,EAAE,aAAa,CAAC,qBAAqB,CAkD1D,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"HeaderNavigation.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/components/header/HeaderNavigation.tsx"],"names":[],"mappings":"AAsBA,OAAO,EACL,OAAO,EAIR,MAAM,sBAAsB,CAAC;AAU9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,MAAM,WAAW,qBAAqB;IACpC,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,OAAO,OAAO,CAAC,CAAC;IACjD;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,gBAAgB,EAAE,aAAa,CAAC,qBAAqB,CAwE1D,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -16,12 +16,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
16
16
  * @author AMBROISE PARK Consulting
17
17
  */
18
18
  import { MoreHorizontal } from 'lucide-react';
19
+ import { Link as LinkIcon } from 'lucide-react';
19
20
  import { DISPLAY, DropdownMenu, Button, } from '@donotdev/components';
20
21
  import { cn } from '@donotdev/components';
21
22
  import { useBreakpoint, useTranslation } from '@donotdev/core';
23
+ import { Link } from '../../../../routing/Link';
24
+ import { Icon } from '../../../common/icon';
22
25
  import { DnDevNavigationMenu } from '../../../../routing/DnDevNavigationMenu';
23
26
  import { NavigationItemComponent } from '../../../../routing/NavigationItem';
24
- import { useNavigationItems } from '../../../../routing/useNavigation';
27
+ import { useNavigationItems, useNavigationRoute } from '../../../../routing/useNavigation';
25
28
  /**
26
29
  * HeaderNavigation - DISPLAY-aware adaptive navigation component
27
30
  *
@@ -34,11 +37,18 @@ import { useNavigationItems } from '../../../../routing/useNavigation';
34
37
  * @since 0.0.1
35
38
  * @author AMBROISE PARK Consulting
36
39
  */
37
- const HeaderNavigation = ({ className = '', showIcons = true, display = DISPLAY.AUTO, }) => {
40
+ const HeaderNavigation = ({ className = '', showIcons = true, display = DISPLAY.AUTO, path, }) => {
38
41
  const { t } = useTranslation('dndev');
39
42
  const isMobile = useBreakpoint('isMobile');
40
43
  const isTablet = useBreakpoint('isTablet');
41
44
  const navigationItems = useNavigationItems();
45
+ const singleRoute = useNavigationRoute(path ?? '');
46
+ // Single-item mode — render one route as a Button link.
47
+ if (path) {
48
+ if (!singleRoute)
49
+ return null;
50
+ return (_jsx(Button, { variant: "ghost", display: display, icon: showIcons ? _jsx(Icon, { icon: singleRoute.icon, fallback: LinkIcon }) : undefined, className: className, render: ({ children, ...props }) => (_jsx(Link, { path: singleRoute.path, ...props, children: children })), children: singleRoute.label }));
51
+ }
42
52
  const effectiveDisplay = display === DISPLAY.AUTO
43
53
  ? isMobile || isTablet
44
54
  ? DISPLAY.COMPACT
@@ -1 +1 @@
1
- {"version":3,"file":"LicenseWatermark.d.ts","sourceRoot":"","sources":["../../../src/components/license/LicenseWatermark.tsx"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,mDAsG/B"}
1
+ {"version":3,"file":"LicenseWatermark.d.ts","sourceRoot":"","sources":["../../../src/components/license/LicenseWatermark.tsx"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,mDAwG/B"}
@@ -41,6 +41,8 @@ import { watermarkSvg } from './watermarkData';
41
41
  */
42
42
  export function LicenseWatermark() {
43
43
  const license = checkLicense();
44
+ /** Production console.warn is intentional — alerts unlicensed deployments.
45
+ * This is a license enforcement mechanism, not a debug log. */
44
46
  useEffect(() => {
45
47
  if (!license.isValid) {
46
48
  // Console warnings
@@ -61,7 +63,7 @@ export function LicenseWatermark() {
61
63
  }
62
64
  return (_jsxs("a", { href: "https://donotdev.com/pricing", target: "_blank", rel: "noopener noreferrer", style: {
63
65
  position: 'fixed',
64
- right: '20px',
66
+ insetInlineEnd: '20px',
65
67
  top: '50%',
66
68
  transform: 'translateY(-50%)',
67
69
  zIndex: 9999,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @fileoverview UI CrudCard wrapper
3
+ * @description Thin wrapper around @donotdev/crud CrudCard that adds Link routing.
4
+ * CrudCard (crud) is platform-agnostic; this wrapper adds web navigation via Link.
5
+ *
6
+ * @version 0.2.0
7
+ * @since 0.0.1
8
+ * @author AMBROISE PARK Consulting
9
+ */
10
+ import type { CrudCardProps } from '@donotdev/core';
11
+ /**
12
+ * CrudCard with Link wrapping for web navigation.
13
+ * When detailHref is provided, wraps the card in a Link for a11y/SEO.
14
+ */
15
+ export declare function CrudCard({ detailHref, onClick, ...rest }: CrudCardProps): import("react/jsx-runtime").JSX.Element;
16
+ export default CrudCard;
17
+ //# sourceMappingURL=CrudCardLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CrudCardLink.d.ts","sourceRoot":"","sources":["../../../src/crud/components/CrudCardLink.tsx"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIpD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EACvB,UAAU,EACV,OAAO,EACP,GAAG,IAAI,EACR,EAAE,aAAa,2CAqBf;AAED,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { CrudCard as BaseCrudCard } from '@donotdev/crud';
4
+ import { Link } from '../../routing';
5
+ /**
6
+ * CrudCard with Link wrapping for web navigation.
7
+ * When detailHref is provided, wraps the card in a Link for a11y/SEO.
8
+ */
9
+ export function CrudCard({ detailHref, onClick, ...rest }) {
10
+ // When detailHref is set, Link handles navigation — don't pass onClick to inner card
11
+ const card = (_jsx(BaseCrudCard, { ...rest, onClick: detailHref ? undefined : onClick }));
12
+ if (detailHref) {
13
+ return (_jsx(Link, { path: detailHref, "aria-label": rest.item?.id ? `View ${rest.item.id}` : undefined, children: card }));
14
+ }
15
+ return card;
16
+ }
17
+ export default CrudCard;
@@ -1 +1 @@
1
- {"version":3,"file":"EntityCardList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityCardList.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAmB,EACnB,SAA0B,EAAE,2BAA2B;AACvD,MAAM,EACN,WAAmB,GACpB,EAAE,mBAAmB,2CA0RrB"}
1
+ {"version":3,"file":"EntityCardList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityCardList.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,mBAAmB,EAAgB,MAAM,gBAAgB,CAAC;AAIxE,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAmB,EACnB,SAA0B,EAAE,2BAA2B;AACvD,MAAM,EACN,WAAmB,GACpB,EAAE,mBAAmB,2CAqMrB"}
@@ -13,12 +13,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  * @since 0.0.1
14
14
  * @author AMBROISE PARK Consulting
15
15
  */
16
- import { useMemo, useCallback, useState } from 'react';
16
+ import { useMemo, useCallback } from 'react';
17
17
  import { Heart } from 'lucide-react';
18
- import { Grid, Card, Stack, Text, Spinner, Section, Button, } from '@donotdev/components';
19
- import { useTranslation } from '@donotdev/core';
18
+ import { Grid, Stack, Text, Spinner, Section, Button, } from '@donotdev/components';
19
+ import { useTranslation, getListCardFieldNames } from '@donotdev/core';
20
20
  import { useNavigate } from '../../routing';
21
- import { translateFieldLabel, useCrudCardList, EntityFilters, useEntityFavorites, matchesFilter, formatValue, } from '@donotdev/crud';
21
+ import { useCrudCardList, EntityFilters, useEntityFavorites, matchesFilter, useCrudFilters, } from '@donotdev/crud';
22
+ import { CrudCard } from './CrudCardLink';
22
23
  /**
23
24
  * Entity Card List Component - Card grid view for public/user-facing browsing
24
25
  *
@@ -41,9 +42,11 @@ filter, hideFilters = false, }) {
41
42
  const { isFavorite, toggleFavorite, favoritesFilter } = useEntityFavorites({
42
43
  collection: entity.collection,
43
44
  });
44
- const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
45
- // Filter state
46
- const [filters, setFilters] = useState({});
45
+ // Favorites toggle from CrudStore (persists across navigation)
46
+ // Note: EntityFilters now manages its own filters internally via useCrudFilters
47
+ const { showFavoritesOnly, setShowFavoritesOnly, filters } = useCrudFilters({
48
+ collection: entity.collection,
49
+ });
47
50
  // Apply filters from EntityFilters component
48
51
  const applyFilters = useCallback((item) => {
49
52
  if (Object.keys(filters).length === 0)
@@ -80,96 +83,44 @@ filter, hideFilters = false, }) {
80
83
  navigate(`${base}/${id}`);
81
84
  }
82
85
  }, [base, navigate, onClick]);
83
- // Determine which fields to show in cards
84
- const fieldsToShow = useMemo(() => {
85
- const cardFields = entity.listCardFields ?? entity.listFields;
86
- if (cardFields && cardFields.length > 0)
87
- return cardFields;
88
- return Object.keys(entity.fields).slice(0, 4);
89
- }, [entity.listCardFields, entity.listFields, entity.fields]);
90
- // Find image field
91
- const imageField = useMemo(() => {
92
- const imageFieldsInList = fieldsToShow.filter((fieldName) => {
93
- const fieldConfig = entity.fields[fieldName];
94
- return fieldConfig?.type === 'image' || fieldConfig?.type === 'images';
95
- });
96
- if (imageFieldsInList.length > 0)
97
- return imageFieldsInList[0];
98
- // Fallback: search all entity fields
99
- const allImageFields = Object.keys(entity.fields).filter((fieldName) => {
100
- const fieldConfig = entity.fields[fieldName];
101
- return fieldConfig?.type === 'image' || fieldConfig?.type === 'images';
102
- });
103
- return allImageFields[0] || null;
104
- }, [fieldsToShow, entity.fields]);
105
- // Get other fields (non-image)
106
- const otherFields = useMemo(() => {
107
- return fieldsToShow.filter((fieldName) => fieldName !== imageField);
108
- }, [fieldsToShow, imageField]);
86
+ // Flat field names for filters (works with both string[] and ListCardLayout)
87
+ const fieldsToFilter = useMemo(() => getListCardFieldNames(entity), [entity]);
109
88
  const entityName = t('name', { defaultValue: entity.name });
110
89
  return (_jsxs(_Fragment, { children: [!hideFilters && (_jsx(Section, { title: tCrud('filters.title', {
111
90
  entity: entityName,
112
91
  defaultValue: `Browse ${entityName} - Filters`,
113
- }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { direction: "column", gap: "medium", children: [_jsx(Button, { variant: showFavoritesOnly ? 'primary' : 'outline', icon: _jsx(Heart, { size: 18 }), onClick: () => setShowFavoritesOnly(!showFavoritesOnly), children: showFavoritesOnly
92
+ }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { direction: "column", children: [_jsx(Button, { variant: showFavoritesOnly ? 'primary' : 'outline', icon: _jsx(Heart, { size: 18 }), onClick: () => setShowFavoritesOnly(!showFavoritesOnly), children: showFavoritesOnly
114
93
  ? tCrud('favorites.showAll', { defaultValue: 'Show All' })
115
94
  : tCrud('favorites.showFavorites', {
116
95
  defaultValue: 'Show Favorites',
117
- }) }), _jsx(EntityFilters, { entity: entity, data: rawData, filters: filters, onFiltersChange: setFilters, fieldsToFilter: entity.listCardFields })] }) })), _jsx(Section, { title: loading
96
+ }) }), _jsx(EntityFilters, { entity: entity, data: rawData, fieldsToFilter: fieldsToFilter })] }) })), _jsx(Section, { title: loading
118
97
  ? tCrud('results.title.fetching', { defaultValue: 'Fetching...' })
119
98
  : tCrud('results.title.count', {
120
99
  count: data.length,
121
100
  defaultValue: data.length === 1
122
101
  ? 'Found 1 occurrence'
123
102
  : `Found ${data.length} occurrences`,
124
- }), collapsible: true, defaultOpen: true, children: loading ? (_jsx(Stack, { align: "center", justify: "center", gap: "medium", style: { padding: 'var(--gap-3xl)' }, children: _jsx(Spinner, {}) })) : data.length === 0 ? (_jsxs(Stack, { align: "center", justify: "center", gap: "medium", style: { padding: 'var(--gap-3xl)', textAlign: 'center' }, children: [_jsx(Text, { level: "h3", style: { color: 'var(--muted-foreground)' }, children: tCrud('emptyState.title', {
103
+ }), collapsible: true, defaultOpen: true, children: loading ? (_jsx(Stack, { align: "center", justify: "center", style: { padding: 'var(--gap-3xl)' }, children: _jsx(Spinner, {}) })) : data.length === 0 ? (_jsxs(Stack, { align: "center", justify: "center", style: { padding: 'var(--gap-3xl)', textAlign: 'center' }, children: [_jsx(Text, { level: "h3", style: { color: 'var(--muted-foreground)' }, children: tCrud('emptyState.title', {
125
104
  defaultValue: `No ${entity.name.toLowerCase()} found`,
126
105
  }) }), _jsx(Text, { style: { color: 'var(--muted-foreground)' }, children: tCrud('emptyState.description', {
127
106
  defaultValue: `No ${entity.name.toLowerCase()} available at this time.`,
128
- }) })] })) : (_jsx(Grid, { cols: cols, gap: "medium", children: data.map((item) => {
129
- const imageValue = imageField ? item[imageField] : null;
130
- // Backend optimizes picture fields for listCard: returns thumbUrl string directly
131
- // (or fullUrl if thumbUrl missing, or null if no picture)
132
- const imageUrl = typeof imageValue === 'string' ? imageValue : null;
133
- // Title from first non-image field
134
- const titleField = otherFields[0];
135
- const titleValue = titleField ? item[titleField] : item.id;
107
+ }) })] })) : (_jsx(Grid, { cols: cols, children: data.map((item) => {
136
108
  const itemIsFavorite = isFavorite(item.id);
137
- return (_jsxs(Card, { title: String(titleValue || ''), clickable: true, onClick: () => handleView(item.id), elevated: true, children: [_jsx(Heart, { fill: itemIsFavorite ? '#ef4444' : '#ffffff', stroke: itemIsFavorite ? '#ef4444' : 'var(--muted-foreground)', onClick: (e) => {
138
- e.stopPropagation();
139
- toggleFavorite(item.id);
140
- }, style: {
141
- position: 'absolute',
142
- top: 'var(--gap-sm)',
143
- right: 'var(--gap-sm)',
144
- zIndex: 10,
145
- cursor: 'pointer',
146
- width: 'var(--icon-md)',
147
- height: 'var(--icon-md)',
148
- transition: 'fill 0.2s, stroke 0.2s',
149
- }, "aria-label": itemIsFavorite
150
- ? tCrud('favorites.remove', {
151
- defaultValue: 'Remove from favorites',
152
- })
153
- : tCrud('favorites.add', {
154
- defaultValue: 'Add to favorites',
155
- }) }), _jsxs(Stack, { direction: "column", gap: "medium", children: [imageUrl && (_jsx("div", { style: {
156
- width: '100%',
157
- aspectRatio: '16/9',
158
- borderRadius: 'var(--radius-md)',
159
- overflow: 'hidden',
160
- backgroundColor: 'var(--muted)',
161
- position: 'relative',
162
- }, children: _jsx("img", { src: imageUrl, alt: String(titleValue || ''), style: {
163
- width: '100%',
164
- height: '100%',
165
- objectFit: 'cover',
166
- } }) })), _jsx(Stack, { direction: "column", gap: "tight", children: otherFields.slice(1, 4).map((fieldName) => {
167
- const fieldConfig = entity.fields[fieldName];
168
- if (!fieldConfig)
169
- return null;
170
- return (_jsxs("div", { children: [_jsx(Text, { level: "small", variant: "muted", children: translateFieldLabel(fieldName, fieldConfig, t) }), _jsx(Text, { children: formatValue(item[fieldName], fieldConfig, t, {
171
- compact: true,
172
- }) })] }, fieldName));
173
- }) })] })] }, item.id));
109
+ const detailHref = onClick ? undefined : `${base}/${item.id}`;
110
+ return (_jsx(CrudCard, { item: item, entity: entity, detailHref: detailHref, onClick: onClick ? () => handleView(item.id) : undefined, renderActions: _jsx(Heart, { fill: itemIsFavorite ? '#ef4444' : '#ffffff', stroke: itemIsFavorite ? '#ef4444' : 'var(--muted-foreground)', onClick: (e) => {
111
+ e.stopPropagation();
112
+ toggleFavorite(item.id);
113
+ }, style: {
114
+ cursor: 'pointer',
115
+ width: 'var(--icon-md)',
116
+ height: 'var(--icon-md)',
117
+ transition: 'fill 0.2s, stroke 0.2s',
118
+ }, "aria-label": itemIsFavorite
119
+ ? tCrud('favorites.remove', {
120
+ defaultValue: 'Remove from favorites',
121
+ })
122
+ : tCrud('favorites.add', {
123
+ defaultValue: 'Add to favorites',
124
+ }) }) }, item.id));
174
125
  }) })) })] }));
175
126
  }
@@ -16,6 +16,6 @@ export type { EntityDisplayRendererProps };
16
16
  * <EntityDisplayRenderer entity={carEntity} id={carId} />
17
17
  * ```
18
18
  */
19
- export declare function EntityDisplayRenderer<T extends EntityRecord = EntityRecord>({ entity, id, t, className, backend, loadingMessage, notFoundMessage, viewerRole, }: EntityDisplayRendererProps<T>): import("react/jsx-runtime").JSX.Element;
19
+ export declare function EntityDisplayRenderer<T extends EntityRecord = EntityRecord>({ entity, id, t, className, loadingMessage, notFoundMessage, viewerRole: viewerRoleProp, }: EntityDisplayRendererProps<T>): import("react/jsx-runtime").JSX.Element;
20
20
  export default EntityDisplayRenderer;
21
21
  //# sourceMappingURL=EntityDisplayRenderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EntityDisplayRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityDisplayRenderer.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,0BAA0B,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG/E,YAAY,EAAE,0BAA0B,EAAE,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EAC3E,MAAM,EACN,EAAE,EACF,CAAC,EACD,SAAc,EACd,OAAqB,EACrB,cAAc,EACd,eAAe,EACf,UAAoB,GACrB,EAAE,0BAA0B,CAAC,CAAC,CAAC,2CAgM/B;AAED,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"EntityDisplayRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityDisplayRenderer.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,0BAA0B,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAI/E,YAAY,EAAE,0BAA0B,EAAE,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EAC3E,MAAM,EACN,EAAE,EACF,CAAC,EACD,SAAc,EACd,cAAc,EACd,eAAe,EACf,UAAU,EAAE,cAAc,GAC3B,EAAE,0BAA0B,CAAC,CAAC,CAAC,2CAkM/B;AAED,eAAe,qBAAqB,CAAC"}
@@ -14,6 +14,7 @@ import { useEffect, useState, useMemo } from 'react';
14
14
  import { Stack, Spinner } from '@donotdev/components';
15
15
  import { useTranslation, isFieldVisible } from '@donotdev/core';
16
16
  import { useCrud, DisplayFieldRenderer } from '@donotdev/crud';
17
+ import { useAuthSafe } from '../../utils/useAuthSafe';
17
18
  /**
18
19
  * EntityDisplayRenderer - Automatically fetches and displays entity data
19
20
  *
@@ -30,8 +31,11 @@ import { useCrud, DisplayFieldRenderer } from '@donotdev/crud';
30
31
  * <EntityDisplayRenderer entity={carEntity} id={carId} />
31
32
  * ```
32
33
  */
33
- export function EntityDisplayRenderer({ entity, id, t, className = '', backend = 'functions', loadingMessage, notFoundMessage, viewerRole = 'guest', }) {
34
- const { get, loading, data: storeData, error: storeError, isAvailable, } = useCrud(entity, { backend });
34
+ export function EntityDisplayRenderer({ entity, id, t, className = '', loadingMessage, notFoundMessage, viewerRole: viewerRoleProp, }) {
35
+ // Auto-detect role from auth; prop overrides
36
+ const authRole = useAuthSafe('userRole');
37
+ const viewerRole = viewerRoleProp ?? authRole;
38
+ const { get, loading, data: storeData, error: storeError, isAvailable, } = useCrud(entity);
35
39
  const [isFetching, setIsFetching] = useState(false);
36
40
  const [fetchError, setFetchError] = useState(null);
37
41
  const [data, setData] = useState(null);
@@ -135,7 +139,7 @@ export function EntityDisplayRenderer({ entity, id, t, className = '', backend =
135
139
  }
136
140
  // Error or not found state
137
141
  if (displayError || !displayData) {
138
- return (_jsx(Stack, { align: "center", justify: "center", gap: "medium", style: {
142
+ return (_jsx(Stack, { align: "center", justify: "center", style: {
139
143
  padding: 'var(--gap-3xl)',
140
144
  textAlign: 'center',
141
145
  }, className: className, children: _jsxs(Stack, { direction: "column", gap: "tight", children: [_jsx("h3", { style: { color: 'var(--muted-foreground)' }, children: notFoundMessage ||
@@ -149,7 +153,7 @@ export function EntityDisplayRenderer({ entity, id, t, className = '', backend =
149
153
  : String(displayError) }))] }) }));
150
154
  }
151
155
  // Render all visible fields with values
152
- return (_jsx(Stack, { direction: "column", gap: "medium", className: className, children: visibleFields.map(([fieldName, fieldConfig]) => {
156
+ return (_jsx(Stack, { direction: "column", className: className, children: visibleFields.map(([fieldName, fieldConfig]) => {
153
157
  return (_jsx(DisplayFieldRenderer, { name: fieldName, config: fieldConfig, value: displayData[fieldName], t: translate }, fieldName));
154
158
  }) }));
155
159
  }
@@ -13,6 +13,6 @@ export type { EntityFormRendererProps };
13
13
  * @since 0.0.1
14
14
  * @author AMBROISE PARK Consulting
15
15
  */
16
- export declare function EntityFormRenderer<T extends EntityRecord = EntityRecord>({ entity, onSubmit, t, className, submitText, loading, defaultValues, submitVariant, secondaryButtonText, secondaryButtonVariant, onSecondarySubmit, viewerRole, operation, autoSave, formId: externalFormId, cancelText, cancelPath, successPath, onCancel, warnOnUnsavedChanges, unsavedChangesMessage, hideVisibilityInfo, }: EntityFormRendererProps<T>): import("react/jsx-runtime").JSX.Element;
16
+ export declare function EntityFormRenderer<T extends EntityRecord = EntityRecord>({ entity, onSubmit, t, className, submitText, loading, defaultValues, submitVariant, secondaryButtonText, secondaryButtonVariant, onSecondarySubmit, viewerRole: viewerRoleProp, operation, autoSave, formId: externalFormId, cancelText, cancelPath, successPath, onCancel, warnOnUnsavedChanges, unsavedChangesMessage, hideVisibilityInfo, }: EntityFormRendererProps<T>): import("react/jsx-runtime").JSX.Element;
17
17
  export default EntityFormRenderer;
18
18
  //# sourceMappingURL=EntityFormRenderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EntityFormRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityFormRenderer.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EACV,uBAAuB,EACvB,YAAY,EAEb,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,uBAAuB,EAAE,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACxE,MAAM,EACN,QAAQ,EACR,CAAC,EACD,SAAc,EACd,UAAU,EACV,OAAe,EACf,aAAa,EACb,aAAyB,EACzB,mBAAmB,EACnB,sBAAkC,EAClC,iBAAiB,EACjB,UAAU,EACV,SAA6C,EAC7C,QAAiC,EACjC,MAAM,EAAE,cAAc,EACtB,UAAU,EACV,UAAU,EACV,WAAW,EACX,QAAQ,EACR,oBAA2B,EAC3B,qBAAqB,EACrB,kBAA0B,GAC3B,EAAE,uBAAuB,CAAC,CAAC,CAAC,2CA+b5B;AAED,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"EntityFormRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityFormRenderer.tsx"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EACV,uBAAuB,EACvB,YAAY,EAEb,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,uBAAuB,EAAE,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACxE,MAAM,EACN,QAAQ,EACR,CAAC,EACD,SAAc,EACd,UAAU,EACV,OAAe,EACf,aAAa,EACb,aAAyB,EACzB,mBAAmB,EACnB,sBAAkC,EAClC,iBAAiB,EACjB,UAAU,EAAE,cAAc,EAC1B,SAA6C,EAC7C,QAAiC,EACjC,MAAM,EAAE,cAAc,EACtB,UAAU,EACV,UAAU,EACV,WAAW,EACX,QAAQ,EACR,oBAA2B,EAC3B,qBAAqB,EACrB,kBAA0B,GAC3B,EAAE,uBAAuB,CAAC,CAAC,CAAC,2CA4c5B;AAED,eAAe,kBAAkB,CAAC"}
@@ -16,6 +16,7 @@ import { Badge, Button, DropdownMenu, Grid, Stack, Spinner, } from '@donotdev/co
16
16
  import { hasRoleAccess, useTranslation } from '@donotdev/core';
17
17
  import { useNavigate } from '../../routing';
18
18
  import { DisplayFieldRenderer, FormFieldRenderer, UploadProvider, useEntityForm, useUnsavedChangesWarning, useConfirmNavigation, useFormStore, } from '@donotdev/crud';
19
+ import { useAuthSafe } from '../../utils/useAuthSafe';
19
20
  /**
20
21
  * EntityFormRenderer - Dumb component that renders a form from entity definition.
21
22
  *
@@ -29,8 +30,11 @@ import { DisplayFieldRenderer, FormFieldRenderer, UploadProvider, useEntityForm,
29
30
  * @since 0.0.1
30
31
  * @author AMBROISE PARK Consulting
31
32
  */
32
- export function EntityFormRenderer({ entity, onSubmit, t, className = '', submitText, loading = false, defaultValues, submitVariant = 'primary', secondaryButtonText, secondaryButtonVariant = 'outline', onSecondarySubmit, viewerRole, operation = defaultValues ? 'edit' : 'create', autoSave = operation === 'create', formId: externalFormId, cancelText, cancelPath, successPath, onCancel, warnOnUnsavedChanges = true, unsavedChangesMessage, hideVisibilityInfo = false, }) {
33
+ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submitText, loading = false, defaultValues, submitVariant = 'primary', secondaryButtonText, secondaryButtonVariant = 'outline', onSecondarySubmit, viewerRole: viewerRoleProp, operation = defaultValues ? 'edit' : 'create', autoSave = operation === 'create', formId: externalFormId, cancelText, cancelPath, successPath, onCancel, warnOnUnsavedChanges = true, unsavedChangesMessage, hideVisibilityInfo = false, }) {
33
34
  const navigate = useNavigate();
35
+ // Auto-detect role from auth; prop overrides (e.g. View-As preview)
36
+ const authRole = useAuthSafe('userRole');
37
+ const viewerRole = viewerRoleProp ?? authRole;
34
38
  // Generate stable form ID
35
39
  const generatedFormId = useId();
36
40
  const formId = externalFormId ?? `entity-form-${entity.name}-${generatedFormId}`;
@@ -101,12 +105,14 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
101
105
  return fieldRoleIndex <= previewRoleIndex;
102
106
  });
103
107
  }, [rawFields, canPreviewRole, previewRole]);
108
+ // Subscribe to setIsDirty action via React-compatible selector
109
+ const setFormIsDirty = useFormStore((state) => state.setIsDirty);
104
110
  // Sync isDirty to FormStore (single source of truth)
105
111
  useEffect(() => {
106
112
  if (formId) {
107
- useFormStore.getState().setIsDirty(formId, isDirtyForBlocking);
113
+ setFormIsDirty(formId, isDirtyForBlocking);
108
114
  }
109
- }, [formId, isDirtyForBlocking]);
115
+ }, [formId, isDirtyForBlocking, setFormIsDirty]);
110
116
  // Cleanup on unmount
111
117
  useEffect(() => {
112
118
  return cleanup;
@@ -151,22 +157,27 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
151
157
  }
152
158
  }
153
159
  };
154
- // Wrap onSubmit to navigate optimistically (fire and forget)
160
+ // Wrap onSubmit to await the result before navigating
155
161
  // handleSubmit from react-hook-form expects: (data: EntityData) => void | Promise<void>
156
162
  // Our onSubmit prop is (data: T) => void | Promise<void>, where T extends EntityRecord
157
163
  // EntityData should be compatible with T, so we can safely pass it
158
- const handleFormSubmit = (data) => {
159
- // Call onSubmit (don't await - optimistic update pattern)
160
- // Type assertion is safe: EntityData is the validated form data, T is EntityRecord
161
- onSubmit(data);
162
- // Navigate immediately (optimistic navigation - feels fast)
163
- // If onSubmit is async, navigation happens while it's processing in background
164
- if (successPath) {
165
- navigate(successPath);
164
+ const handleFormSubmit = async (data) => {
165
+ try {
166
+ // Await onSubmit so navigation only happens after successful submission
167
+ // Type assertion is safe: EntityData is the validated form data, T is EntityRecord
168
+ await onSubmit(data);
169
+ // Navigate only after successful submission
170
+ if (successPath) {
171
+ navigate(successPath);
172
+ }
173
+ else {
174
+ // Default: navigate back (user came from list, go back there)
175
+ navigate('back');
176
+ }
166
177
  }
167
- else {
168
- // Default: navigate back (optimistic - user came from list, go back there)
169
- navigate('back');
178
+ catch {
179
+ // onSubmit rejected stay on form so user can correct errors
180
+ // Error display is handled by the caller's onSubmit implementation
170
181
  }
171
182
  };
172
183
  // Determine if cancel button should show
@@ -274,13 +285,13 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
274
285
  if (buttonCount === 1) {
275
286
  return (_jsx(Stack, { direction: "column", gap: "tight", style: buttonContainerStyle, children: buttons[0] }));
276
287
  }
277
- // Multiple buttons: use Grid with proportional columns (Submit gets 2fr)
278
- const templateColumns = buttonCount === 2
288
+ // Multiple buttons: responsive stacked on mobile, proportional on tablet+
289
+ const colTemplate = buttonCount === 2
279
290
  ? '1fr 2fr' // Cancel/Preview + Submit
280
291
  : buttonCount === 3
281
292
  ? '1fr 1fr 2fr' // Cancel + Preview + Submit
282
293
  : '1fr 1fr 1fr 2fr'; // Cancel + Preview + Secondary + Submit
283
- return (_jsx(Grid, { cols: [1, buttonCount, buttonCount, buttonCount], templateColumns: templateColumns, gap: "tight", style: buttonContainerStyle, children: buttons }));
294
+ return (_jsx(Grid, { cols: ['1fr', colTemplate, colTemplate, colTemplate], gap: "tight", style: buttonContainerStyle, children: buttons }));
284
295
  })()] })] }) }) }));
285
296
  }
286
297
  export default EntityFormRenderer;
@@ -10,5 +10,5 @@ export type { EntityListProps };
10
10
  * - Edit and Delete actions (admin only)
11
11
  * - Auto-routing when handlers not provided
12
12
  */
13
- export declare function EntityList({ entity, userRole, basePath, onClick, hideFilters, pagination, pageSize: pageSizeProp, queryOptions, exportable, }: EntityListProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function EntityList({ entity, basePath, onClick, hideFilters, pagination, pageSize: pageSizeProp, queryOptions, exportable, }: EntityListProps): import("react/jsx-runtime").JSX.Element;
14
14
  //# sourceMappingURL=EntityList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EntityList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityList.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,QAAkB,EAClB,QAAQ,EACR,OAAO,EACP,WAAmB,EACnB,UAAqB,EACrB,QAAQ,EAAE,YAAY,EACtB,YAAY,EACZ,UAAiB,GAClB,EAAE,eAAe,2CA0TjB"}
1
+ {"version":3,"file":"EntityList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityList.tsx"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,QAAQ,EACR,OAAO,EACP,WAAmB,EACnB,UAAqB,EACrB,QAAQ,EAAE,YAAY,EACtB,YAAY,EACZ,UAAiB,GAClB,EAAE,eAAe,2CAiTjB"}
@@ -18,7 +18,7 @@ import { useMemo, useCallback, useState } from 'react';
18
18
  import { DataTable, Button, Stack, ActionButton, Section, Input, } from '@donotdev/components';
19
19
  import { useTranslation } from '@donotdev/core';
20
20
  import { useNavigate } from '../../routing';
21
- import { translateFieldLabel, useCrud, useCrudList, EntityFilters, matchesFilter, formatValue, } from '@donotdev/crud';
21
+ import { translateFieldLabel, useCrud, useCrudList, EntityFilters, matchesFilter, formatValue, useCrudFilters, } from '@donotdev/crud';
22
22
  /**
23
23
  * Entity List Component - Table view for admin/internal operations
24
24
  *
@@ -29,7 +29,7 @@ import { translateFieldLabel, useCrud, useCrudList, EntityFilters, matchesFilter
29
29
  * - Edit and Delete actions (admin only)
30
30
  * - Auto-routing when handlers not provided
31
31
  */
32
- export function EntityList({ entity, userRole = 'guest', basePath, onClick, hideFilters = false, pagination = 'client', pageSize: pageSizeProp, queryOptions, exportable = true, }) {
32
+ export function EntityList({ entity, basePath, onClick, hideFilters = false, pagination = 'client', pageSize: pageSizeProp, queryOptions, exportable = true, }) {
33
33
  const navigate = useNavigate();
34
34
  const base = basePath ?? `/${entity.collection}`;
35
35
  // Server-side pagination state (only used when pagination='server')
@@ -54,8 +54,10 @@ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hide
54
54
  const data = listData?.items || [];
55
55
  // Entity + crud namespaces so formatValue can resolve crud:price.* etc.
56
56
  const { t } = useTranslation([entity.namespace, 'crud']);
57
- // Filter state - supports string, number range {min, max}, date range {min, max}
58
- const [filters, setFilters] = useState({});
57
+ // Get filters for applying to data (EntityFilters manages its own state)
58
+ const { filters } = useCrudFilters({
59
+ collection: entity.collection,
60
+ });
59
61
  const [searchQuery, setSearchQuery] = useState('');
60
62
  // Refresh handler - triggers manual refetch in useList
61
63
  const handleRefresh = useCallback(async () => {
@@ -82,10 +84,6 @@ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hide
82
84
  const handleDelete = useCallback(async (itemId) => {
83
85
  await deleteItem(itemId);
84
86
  }, [deleteItem]);
85
- // Update filters (for EntityFilters component)
86
- const handleFiltersChange = useCallback((newFilters) => {
87
- setFilters(newFilters);
88
- }, []);
89
87
  // Apply search and filters to data
90
88
  // @todo Server-side filtering: Currently only handles client-side filtering.
91
89
  // When pagination='server', filters should be sent to the server via useCrudList options.
@@ -170,9 +168,9 @@ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hide
170
168
  return (_jsxs(_Fragment, { children: [_jsx(Section, { title: tCrud('filters.title', {
171
169
  entity: entityName,
172
170
  defaultValue: `Browse ${entityName} - Filters`,
173
- }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { gap: "medium", children: [_jsxs(Stack, { direction: "row", gap: "tight", align: "center", className: "dndev-w-full", style: { display: 'grid', gridTemplateColumns: '1fr auto auto' }, children: [_jsx(Input, { placeholder: tCrud('search.placeholder', {
171
+ }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", gap: "tight", align: "center", className: "dndev-w-full", style: { display: 'grid', gridTemplateColumns: '1fr auto auto' }, children: [_jsx(Input, { placeholder: tCrud('search.placeholder', {
174
172
  defaultValue: 'Search...',
175
- }), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), icon: Search, className: "dndev-w-full" }), _jsx(Button, { icon: RefreshCw, variant: "outline", onClick: handleRefresh, disabled: loading, display: "compact", "aria-label": tCrud('refresh', { defaultValue: 'Refresh' }) }), _jsx(Button, { icon: Plus, onClick: handleCreate, display: "compact", children: tCrud('addNew', { defaultValue: 'Add New' }) })] }), !hideFilters && (_jsx(EntityFilters, { entity: entity, data: data, filters: filters, onFiltersChange: handleFiltersChange, fieldsToFilter: entity.listFields }))] }) }), _jsx(Section, { title: loading
173
+ }), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), icon: Search, className: "dndev-w-full" }), _jsx(Button, { icon: RefreshCw, variant: "outline", onClick: handleRefresh, disabled: loading, display: "compact", "aria-label": tCrud('refresh', { defaultValue: 'Refresh' }) }), _jsx(Button, { icon: Plus, onClick: handleCreate, display: "compact", children: tCrud('addNew', { defaultValue: 'Add New' }) })] }), !hideFilters && (_jsx(EntityFilters, { entity: entity, data: data, fieldsToFilter: entity.listFields }))] }) }), _jsx(Section, { title: loading
176
174
  ? tCrud('results.title.fetching', {
177
175
  defaultValue: 'Fetching...',
178
176
  })
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @fileoverview Reusable recommendations section for related entities
3
+ * @description Fetches and displays related entity cards in a grid.
4
+ * Caller defines "related by what" via queryOptions.
5
+ *
6
+ * @version 0.0.1
7
+ * @since 0.0.15
8
+ * @author AMBROISE PARK Consulting
9
+ */
10
+ import type { EntityRecommendationsProps } from '@donotdev/core';
11
+ /**
12
+ * Displays a grid of related entity cards.
13
+ *
14
+ * @example
15
+ * <EntityRecommendations
16
+ * entity={apartmentEntity}
17
+ * title={t('apartment.recommendations')}
18
+ * queryOptions={{
19
+ * where: [
20
+ * { field: 'district_code', operator: 'eq', value: current.district_code },
21
+ * { field: 'id', operator: 'neq', value: current.id },
22
+ * ],
23
+ * limit: 3,
24
+ * }}
25
+ * />
26
+ */
27
+ export declare function EntityRecommendations({ entity, queryOptions, title, basePath, cols, className, }: EntityRecommendationsProps): import("react/jsx-runtime").JSX.Element | null;
28
+ //# sourceMappingURL=EntityRecommendations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityRecommendations.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityRecommendations.tsx"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAMjE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,IAAQ,EACR,SAAS,GACV,EAAE,0BAA0B,kDAuB5B"}
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Section } from '@donotdev/components';
4
+ import { useCrudList } from '@donotdev/crud';
5
+ import { CrudCard } from './CrudCardLink';
6
+ /**
7
+ * Displays a grid of related entity cards.
8
+ *
9
+ * @example
10
+ * <EntityRecommendations
11
+ * entity={apartmentEntity}
12
+ * title={t('apartment.recommendations')}
13
+ * queryOptions={{
14
+ * where: [
15
+ * { field: 'district_code', operator: 'eq', value: current.district_code },
16
+ * { field: 'id', operator: 'neq', value: current.id },
17
+ * ],
18
+ * limit: 3,
19
+ * }}
20
+ * />
21
+ */
22
+ export function EntityRecommendations({ entity, queryOptions, title, basePath, cols = 3, className, }) {
23
+ const { items, loading } = useCrudList(entity, {
24
+ queryOptions,
25
+ });
26
+ if (!loading && items.length === 0) {
27
+ return null;
28
+ }
29
+ const resolvedBasePath = basePath ?? `/${entity.collection}`;
30
+ return (_jsx(Section, { title: title, gridCols: cols, className: className, children: items.map((item) => (_jsx(CrudCard, { item: item, entity: entity, detailHref: `${resolvedBasePath}/${item.id}` }, item.id))) }));
31
+ }