@vendure/dashboard 3.4.2-master-202509081248 → 3.4.2-master-202509110229

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 (81) hide show
  1. package/package.json +4 -4
  2. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +4 -4
  3. package/src/app/routes/_authenticated/_assets/assets.tsx +1 -0
  4. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +1 -1
  5. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +5 -5
  6. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +4 -4
  7. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +4 -4
  8. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +4 -4
  9. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +5 -5
  10. package/src/app/routes/_authenticated/_facets/facets.tsx +5 -8
  11. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +4 -4
  12. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +4 -4
  13. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +2 -2
  14. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +1 -1
  15. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +5 -1
  16. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +1 -1
  17. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +4 -4
  18. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +5 -5
  19. package/src/app/routes/_authenticated/_products/products_.$id.tsx +4 -4
  20. package/src/app/routes/_authenticated/_products/products_.$id_.variants.tsx +2 -2
  21. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +5 -5
  22. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +4 -4
  23. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +4 -4
  24. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +4 -4
  25. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +4 -4
  26. package/src/app/routes/_authenticated/_system/healthchecks.tsx +1 -0
  27. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +4 -4
  28. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +5 -4
  29. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +4 -4
  30. package/src/lib/components/data-input/affixed-input.tsx +18 -0
  31. package/src/lib/components/data-input/boolean-input.tsx +7 -0
  32. package/src/lib/components/data-input/checkbox-input.tsx +7 -0
  33. package/src/lib/components/data-input/datetime-input.tsx +7 -0
  34. package/src/lib/components/data-input/money-input.tsx +8 -0
  35. package/src/lib/components/data-input/number-input.tsx +7 -0
  36. package/src/lib/components/data-input/password-input.tsx +7 -0
  37. package/src/lib/components/data-input/rich-text-input.tsx +7 -0
  38. package/src/lib/components/data-input/text-input.tsx +7 -0
  39. package/src/lib/components/data-input/textarea-input.tsx +7 -0
  40. package/src/lib/components/data-table/data-table-view-options.tsx +29 -26
  41. package/src/lib/components/data-table/data-table.tsx +20 -0
  42. package/src/lib/components/data-table/types.ts +39 -0
  43. package/src/lib/components/layout/channel-switcher.tsx +1 -3
  44. package/src/lib/components/layout/generated-breadcrumbs.tsx +103 -43
  45. package/src/lib/components/shared/asset/asset-gallery.tsx +58 -0
  46. package/src/lib/components/shared/asset/asset-picker-dialog.tsx +39 -0
  47. package/src/lib/components/shared/detail-page-button.tsx +8 -22
  48. package/src/lib/components/shared/facet-value-chip.tsx +7 -0
  49. package/src/lib/components/shared/facet-value-selector.tsx +55 -0
  50. package/src/lib/components/shared/form-field-wrapper.tsx +51 -0
  51. package/src/lib/components/shared/paginated-list-data-table.tsx +128 -16
  52. package/src/lib/components/shared/permission-guard.tsx +30 -0
  53. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +1 -1
  54. package/src/lib/components/shared/translatable-form-field.tsx +52 -0
  55. package/src/lib/components/shared/vendure-image.tsx +114 -2
  56. package/src/lib/framework/extension-api/define-dashboard-extension.ts +25 -3
  57. package/src/lib/framework/extension-api/extension-api-types.ts +12 -3
  58. package/src/lib/framework/extension-api/types/alerts.ts +2 -3
  59. package/src/lib/framework/extension-api/types/data-table.ts +2 -2
  60. package/src/lib/framework/extension-api/types/detail-forms.ts +2 -2
  61. package/src/lib/framework/extension-api/types/form-components.ts +2 -2
  62. package/src/lib/framework/extension-api/types/layout.ts +24 -13
  63. package/src/lib/framework/extension-api/types/login.ts +6 -5
  64. package/src/lib/framework/extension-api/types/navigation.ts +3 -3
  65. package/src/lib/framework/extension-api/types/widgets.ts +7 -3
  66. package/src/lib/framework/form-engine/form-engine-types.ts +13 -7
  67. package/src/lib/framework/form-engine/use-generated-form.tsx +44 -0
  68. package/src/lib/framework/layout-engine/page-layout.tsx +94 -31
  69. package/src/lib/framework/page/detail-page.tsx +3 -5
  70. package/src/lib/framework/page/list-page.tsx +87 -5
  71. package/src/lib/framework/page/use-detail-page.ts +4 -5
  72. package/src/lib/graphql/api.ts +2 -2
  73. package/src/lib/graphql/graphql-env.d.ts +7 -16
  74. package/src/lib/hooks/use-auth.tsx +1 -3
  75. package/src/lib/hooks/use-channel.ts +4 -2
  76. package/src/lib/hooks/use-page-block.tsx +9 -0
  77. package/src/lib/hooks/use-permissions.ts +6 -2
  78. package/src/lib/index.ts +2 -0
  79. package/src/lib/providers/auth.tsx +34 -2
  80. package/src/lib/providers/channel-provider.tsx +22 -1
  81. package/src/lib/components/shared/table-cell/table-cell-types.ts +0 -33
@@ -5,9 +5,11 @@ import {
5
5
  BreadcrumbList,
6
6
  BreadcrumbSeparator,
7
7
  } from '@/vdb/components/ui/breadcrumb.js';
8
- import { Link, useRouterState } from '@tanstack/react-router';
8
+ import { Link, useRouter, useRouterState } from '@tanstack/react-router';
9
9
  import * as React from 'react';
10
10
  import { Fragment } from 'react';
11
+ import { getNavMenuConfig } from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
12
+ import type { NavMenuItem, NavMenuSection } from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
11
13
 
12
14
  export interface BreadcrumbPair {
13
15
  label: string | React.ReactElement;
@@ -20,54 +22,112 @@ export type PageBreadcrumb = BreadcrumbPair | BreadcrumbShorthand;
20
22
 
21
23
  export function GeneratedBreadcrumbs() {
22
24
  const matches = useRouterState({ select: s => s.matches });
23
- const breadcrumbs: BreadcrumbPair[] = matches
24
- .filter(match => match.loaderData?.breadcrumb)
25
- .map(({ pathname, loaderData }) => {
26
- if (typeof loaderData.breadcrumb === 'string') {
27
- return {
28
- label: loaderData.breadcrumb,
29
- path: pathname,
30
- };
31
- }
32
- if (Array.isArray(loaderData.breadcrumb)) {
33
- return loaderData.breadcrumb.map((breadcrumb: PageBreadcrumb) => {
34
- if (typeof breadcrumb === 'string') {
35
- return {
36
- label: breadcrumb,
37
- path: pathname,
38
- };
39
- } else if (React.isValidElement(breadcrumb)) {
40
- return {
41
- label: breadcrumb,
42
- path: pathname,
43
- };
44
- } else {
45
- return {
46
- label: breadcrumb.label,
47
- path: breadcrumb.path,
48
- };
49
- }
50
- });
51
- }
52
- if (typeof loaderData.breadcrumb === 'function') {
53
- return {
54
- label: loaderData.breadcrumb(),
55
- path: pathname,
56
- };
25
+ const currentPath = useRouterState({ select: s => s.location.pathname });
26
+ const router = useRouter();
27
+ const navMenuConfig = getNavMenuConfig();
28
+ const basePath = router.basepath || '';
29
+
30
+ const normalizeBreadcrumb = (breadcrumb: any, pathname: string): BreadcrumbPair[] => {
31
+ if (typeof breadcrumb === 'string') {
32
+ return [{ label: breadcrumb, path: pathname }];
33
+ }
34
+ if (React.isValidElement(breadcrumb)) {
35
+ return [{ label: breadcrumb, path: pathname }];
36
+ }
37
+ if (typeof breadcrumb === 'function') {
38
+ return [{ label: breadcrumb(), path: pathname }];
39
+ }
40
+ if (Array.isArray(breadcrumb)) {
41
+ return breadcrumb.map((crumb: PageBreadcrumb) => {
42
+ if (typeof crumb === 'string' || React.isValidElement(crumb)) {
43
+ return { label: crumb, path: pathname };
44
+ }
45
+ return { label: crumb.label, path: crumb.path };
46
+ });
47
+ }
48
+ return [];
49
+ };
50
+
51
+ const rawCrumbs: BreadcrumbPair[] = React.useMemo(() => {
52
+ return matches
53
+ .filter(match => match.loaderData?.breadcrumb)
54
+ .flatMap(({ pathname, loaderData }) =>
55
+ normalizeBreadcrumb(loaderData.breadcrumb, pathname)
56
+ );
57
+ }, [matches]);
58
+
59
+ const isBaseRoute = (p: string) => p === basePath || p === `${basePath}/`;
60
+ const pageCrumbs: BreadcrumbPair[] = rawCrumbs.filter(c => !isBaseRoute(c.path));
61
+
62
+ const normalizePath = (path: string): string => {
63
+ const normalizedPath = basePath && path.startsWith(basePath) ? path.slice(basePath.length) : path;
64
+ return normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`;
65
+ };
66
+
67
+ const pathMatches = (cleanPath: string, rawUrl?: string): boolean => {
68
+ if (!rawUrl) return false;
69
+ const strip = (p: string) => (p !== '/' && p.endsWith('/') ? p.slice(0, -1) : p);
70
+ const p = strip(cleanPath);
71
+ const u = strip(rawUrl);
72
+ return p === u || p.startsWith(`${u}/`);
73
+ };
74
+
75
+ const checkSectionItems = (
76
+ section: NavMenuSection | NavMenuItem,
77
+ cleanPath: string,
78
+ ): BreadcrumbPair | undefined => {
79
+ if (!('items' in section) || !Array.isArray(section.items)) {
80
+ return undefined;
81
+ }
82
+
83
+ for (const item of section.items) {
84
+ if (!item?.url) continue;
85
+ if (pathMatches(cleanPath, item.url)) {
86
+ return { label: section.title, path: item.url };
57
87
  }
58
- if (React.isValidElement(loaderData.breadcrumb)) {
59
- return {
60
- label: loaderData.breadcrumb,
61
- path: pathname,
62
- };
88
+ }
89
+ return undefined;
90
+ };
91
+
92
+ const checkDirectSection = (
93
+ section: NavMenuSection | NavMenuItem,
94
+ cleanPath: string,
95
+ ): BreadcrumbPair | undefined => {
96
+ if ('url' in section && section.url && pathMatches(cleanPath, section.url)) {
97
+ return { label: section.title, path: section.url };
98
+ }
99
+ return undefined;
100
+ };
101
+
102
+ const findSectionCrumb = (path: string): BreadcrumbPair | undefined => {
103
+ const cleanPath = normalizePath(path);
104
+ const sections: Array<NavMenuSection | NavMenuItem> = navMenuConfig?.sections ?? [];
105
+ if (sections.length === 0) return undefined;
106
+
107
+ for (const section of sections) {
108
+ const result = checkSectionItems(section, cleanPath) || checkDirectSection(section, cleanPath);
109
+ if (result) {
110
+ return result;
63
111
  }
64
- })
65
- .flat();
112
+ }
113
+ return undefined;
114
+ };
115
+
116
+ const sectionCrumb = React.useMemo(
117
+ () => findSectionCrumb(currentPath),
118
+ [currentPath, basePath, navMenuConfig],
119
+ );
120
+ const breadcrumbs: BreadcrumbPair[] = React.useMemo(() => {
121
+ const arr = sectionCrumb ? [sectionCrumb, ...pageCrumbs] : pageCrumbs;
122
+ return arr.filter((c, i, self) =>
123
+ self.findIndex(x => x.path === c.path && x.label === c.label) === i,
124
+ );
125
+ }, [sectionCrumb, pageCrumbs]);
66
126
  return (
67
127
  <Breadcrumb>
68
128
  <BreadcrumbList>
69
129
  {breadcrumbs.map(({ label, path }, index, arr) => (
70
- <Fragment key={index}>
130
+ <Fragment key={`${path}-${index}`}>
71
131
  <BreadcrumbItem className="hidden md:block">
72
132
  <BreadcrumbLink asChild>
73
133
  <Link to={path}>{label}</Link>
@@ -71,6 +71,13 @@ const AssetType = {
71
71
 
72
72
  export type Asset = AssetFragment;
73
73
 
74
+ /**
75
+ * @description
76
+ * Props for the {@link AssetGallery} component.
77
+ *
78
+ * @docsCategory components
79
+ * @docsPage AssetGallery
80
+ */
74
81
  export interface AssetGalleryProps {
75
82
  onSelect?: (assets: Asset[]) => void;
76
83
  selectable?: boolean;
@@ -82,16 +89,67 @@ export interface AssetGalleryProps {
82
89
  * If set to 'manual', multiple selection will occur only if the user holds down the control/cmd key.
83
90
  */
84
91
  multiSelect?: 'auto' | 'manual';
92
+ /**
93
+ * @description
94
+ * The initial assets that should be selected.
95
+ */
85
96
  initialSelectedAssets?: Asset[];
97
+ /**
98
+ * @description
99
+ * The number of assets to display per page.
100
+ */
86
101
  pageSize?: number;
102
+ /**
103
+ * @description
104
+ * Whether the gallery should have a fixed height.
105
+ */
87
106
  fixedHeight?: boolean;
107
+ /**
108
+ * @description
109
+ * Whether the gallery should show a header.
110
+ */
88
111
  showHeader?: boolean;
112
+ /**
113
+ * @description
114
+ * The class name to apply to the gallery.
115
+ */
89
116
  className?: string;
117
+ /**
118
+ * @description
119
+ * The function to call when files are dropped.
120
+ */
90
121
  onFilesDropped?: (files: File[]) => void;
122
+ /**
123
+ * @description
124
+ * The bulk actions to display in the gallery.
125
+ */
91
126
  bulkActions?: AssetBulkAction[];
127
+ /**
128
+ * @description
129
+ * Whether the gallery should display bulk actions.
130
+ */
92
131
  displayBulkActions?: boolean;
93
132
  }
94
133
 
134
+ /**
135
+ * @description
136
+ * A component for displaying a gallery of assets.
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * <AssetGallery
141
+ onSelect={handleAssetSelect}
142
+ multiSelect="manual"
143
+ initialSelectedAssets={initialSelectedAssets}
144
+ fixedHeight={false}
145
+ displayBulkActions={false}
146
+ />
147
+ * ```
148
+ *
149
+ * @docsCategory components
150
+ * @docsPage AssetGallery
151
+ * @docsWeight 0
152
+ */
95
153
  export function AssetGallery({
96
154
  onSelect,
97
155
  selectable = true,
@@ -9,15 +9,54 @@ import {
9
9
  import { useState } from 'react';
10
10
  import { Asset, AssetGallery } from './asset-gallery.js';
11
11
 
12
+ /**
13
+ * @description
14
+ * Props for the {@link AssetPickerDialog} component.
15
+ *
16
+ * @docsCategory components
17
+ * @docsPage AssetPickerDialog
18
+ */
12
19
  interface AssetPickerDialogProps {
20
+ /**
21
+ * @description
22
+ * Whether the dialog is open.
23
+ */
13
24
  open: boolean;
25
+ /**
26
+ * @description
27
+ * The function to call when the dialog is closed.
28
+ */
14
29
  onClose: () => void;
30
+ /**
31
+ * @description
32
+ * The function to call when assets are selected.
33
+ */
15
34
  onSelect: (assets: Asset[]) => void;
35
+ /**
36
+ * @description
37
+ * Whether multiple assets can be selected.
38
+ */
16
39
  multiSelect?: boolean;
40
+ /**
41
+ * @description
42
+ * The initial assets that should be selected.
43
+ */
17
44
  initialSelectedAssets?: Asset[];
45
+ /**
46
+ * @description
47
+ * The title of the dialog.
48
+ */
18
49
  title?: string;
19
50
  }
20
51
 
52
+ /**
53
+ * @description
54
+ * A dialog which allows the creation and selection of assets.
55
+ *
56
+ * @docsCategory components
57
+ * @docsPage AssetPickerDialog
58
+ * @docsWeight 0
59
+ */
21
60
  export function AssetPickerDialog({
22
61
  open,
23
62
  onClose,
@@ -3,46 +3,32 @@ import { ChevronRight } from 'lucide-react';
3
3
  import { Button } from '../ui/button.js';
4
4
 
5
5
  /**
6
+ * @description
6
7
  * DetailPageButton is a reusable navigation component designed to provide consistent UX
7
8
  * across list views when linking to detail pages. It renders as a ghost button with
8
9
  * a chevron indicator, making it easy for users to identify clickable links that
9
10
  * navigate to detail views.
10
11
  *
11
- * @component
12
12
  * @example
13
+ * ```tsx
13
14
  * // Basic usage with ID (relative navigation)
14
15
  * <DetailPageButton id="123" label="Product Name" />
16
+ *
15
17
  *
16
18
  * @example
19
+ * ```tsx
17
20
  * // Custom href with search params
18
21
  * <DetailPageButton
19
22
  * href="/products/detail/456"
20
23
  * label="Custom Product"
21
24
  * search={{ tab: 'variants' }}
22
25
  * />
26
+ * ```
23
27
  *
24
- * @example
25
- * // Disabled state
26
- * <DetailPageButton
27
- * id="789"
28
- * label="Unavailable Item"
29
- * disabled={true}
30
- * />
31
- *
32
- * @param {Object} props - Component props
33
- * @param {string|React.ReactNode} props.label - The text or content to display in the button
34
- * @param {string} [props.id] - The ID for relative navigation (creates href as `./${id}`)
35
- * @param {string} [props.href] - Custom href for navigation (takes precedence over id)
36
- * @param {boolean} [props.disabled=false] - Whether the button is disabled (prevents navigation)
37
- * @param {Record<string, string>} [props.search] - Search parameters to include in the navigation
38
- *
39
- * @returns {React.ReactElement} A styled button component that navigates to detail pages
40
28
  *
41
- * @remarks
42
- * - Uses TanStack Router's Link component for client-side navigation
43
- * - Includes a chevron icon (hidden when disabled) to indicate navigation
44
- * - Preloading is disabled by default for performance optimization
45
- * - Styled as a ghost button variant for subtle, consistent appearance
29
+ * @docsCategory components
30
+ * @docsPage DetailPageButton
31
+ * @since 3.4.0
46
32
  */
47
33
  export function DetailPageButton({
48
34
  id,
@@ -20,6 +20,13 @@ interface FacetValueChipProps {
20
20
  onRemove?: (id: string) => void;
21
21
  }
22
22
 
23
+ /**
24
+ * @description
25
+ * A component for displaying a facet value as a chip.
26
+ *
27
+ * @docsCategory components
28
+ * @since 3.4.0
29
+ */
23
30
  export function FacetValueChip({
24
31
  facetValue,
25
32
  removable = true,
@@ -29,10 +29,51 @@ export interface Facet {
29
29
  code: string;
30
30
  }
31
31
 
32
+ /**
33
+ * @description
34
+ * A component for selecting facet values.
35
+ *
36
+ * @docsCategory components
37
+ * @docsPage FacetValueSelector
38
+ * @since 3.4.0
39
+ */
32
40
  interface FacetValueSelectorProps {
41
+ /**
42
+ * @description
43
+ * The function to call when a facet value is selected.
44
+ *
45
+ * The `value` will have the following structure:
46
+ *
47
+ * ```ts
48
+ * {
49
+ * id: string;
50
+ * name: string;
51
+ * code: string;
52
+ * facet: {
53
+ * id: string;
54
+ * name: string;
55
+ * code: string;
56
+ * };
57
+ * }
58
+ * ```
59
+ */
33
60
  onValueSelect: (value: FacetValue) => void;
61
+ /**
62
+ * @description
63
+ * Whether the selector is disabled.
64
+ */
34
65
  disabled?: boolean;
66
+ /**
67
+ * @description
68
+ * The placeholder text for the selector.
69
+ */
35
70
  placeholder?: string;
71
+ /**
72
+ * @description
73
+ * The number of facet values to display per page.
74
+ *
75
+ * @default 4
76
+ */
36
77
  pageSize?: number;
37
78
  }
38
79
 
@@ -85,6 +126,20 @@ const getFacetValuesForFacetDocument = graphql(`
85
126
  }
86
127
  `);
87
128
 
129
+ /**
130
+ * @description
131
+ * A component for selecting facet values.
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * <FacetValueSelector onValueSelect={onValueSelectHandler} disabled={disabled} />
136
+ * ```
137
+ *
138
+ * @docsCategory components
139
+ * @docsPage FacetValueSelector
140
+ * @docsWeight 0
141
+ * @since 3.4.0
142
+ */
88
143
  export function FacetValueSelector({
89
144
  onValueSelect,
90
145
  disabled,
@@ -3,11 +3,27 @@ import { LocationWrapper } from '@/vdb/framework/layout-engine/location-wrapper.
3
3
  import { FieldPath, FieldValues } from 'react-hook-form';
4
4
  import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../ui/form.js';
5
5
 
6
+ /**
7
+ * @description
8
+ * The props for the FormFieldWrapper component.
9
+ *
10
+ * @docsCategory form-components
11
+ * @docsPage FormFieldWrapper
12
+ * @since 3.4.0
13
+ */
6
14
  export type FormFieldWrapperProps<
7
15
  TFieldValues extends FieldValues = FieldValues,
8
16
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
9
17
  > = React.ComponentProps<typeof FormField<TFieldValues, TName>> & {
18
+ /**
19
+ * @description
20
+ * The label for the form field.
21
+ */
10
22
  label?: React.ReactNode;
23
+ /**
24
+ * @description
25
+ * The description for the form field.
26
+ */
11
27
  description?: React.ReactNode;
12
28
  /**
13
29
  * @description
@@ -15,11 +31,46 @@ export type FormFieldWrapperProps<
15
31
  * If false, the form control will not be rendered.
16
32
  * This is useful when you want to render the form control in a custom way, e.g. for <Select/> components,
17
33
  * where the FormControl needs to nested in the root component.
34
+ *
18
35
  * @default true
19
36
  */
20
37
  renderFormControl?: boolean;
21
38
  };
22
39
 
40
+ /**
41
+ * @description
42
+ * This is a wrapper that can be used in all forms to wrap the actual form control, and provide a label, description and error message.
43
+ *
44
+ * Use this instead of the default Shadcn FormField (etc.) components, as it also supports
45
+ * overridden form components.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <PageBlock column="main" blockId="main-form">
50
+ * <DetailFormGrid>
51
+ * <FormFieldWrapper
52
+ * control={form.control}
53
+ * name="description"
54
+ * label={<Trans>Description</Trans>}
55
+ * render={({ field }) => <Input {...field} />}
56
+ * />
57
+ * <FormFieldWrapper
58
+ * control={form.control}
59
+ * name="code"
60
+ * label={<Trans>Code</Trans>}
61
+ * render={({ field }) => <Input {...field} />}
62
+ * />
63
+ * </DetailFormGrid>
64
+ * </PageBlock>
65
+ * ```
66
+ *
67
+ * If you are dealing with translatable fields, use the {@link TranslatableFormFieldWrapper} component instead.
68
+ *
69
+ * @docsCategory form-components
70
+ * @docsPage FormFieldWrapper
71
+ * @docsWeight 0
72
+ * @since 3.4.0
73
+ */
23
74
  export function FormFieldWrapper<
24
75
  TFieldValues extends FieldValues = FieldValues,
25
76
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,