@vendure/dashboard 3.3.6-master-202507030234 → 3.3.6-master-202507030732

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.3.6-master-202507030234",
4
+ "version": "3.3.6-master-202507030732",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -86,8 +86,8 @@
86
86
  "@types/react-dom": "^19.0.4",
87
87
  "@types/react-grid-layout": "^1.3.5",
88
88
  "@uidotdev/usehooks": "^2.4.1",
89
- "@vendure/common": "^3.3.6-master-202507030234",
90
- "@vendure/core": "^3.3.6-master-202507030234",
89
+ "@vendure/common": "^3.3.6-master-202507030732",
90
+ "@vendure/core": "^3.3.6-master-202507030732",
91
91
  "@vitejs/plugin-react": "^4.3.4",
92
92
  "awesome-graphql-client": "^2.1.0",
93
93
  "class-variance-authority": "^0.7.1",
@@ -130,5 +130,5 @@
130
130
  "lightningcss-linux-arm64-musl": "^1.29.3",
131
131
  "lightningcss-linux-x64-musl": "^1.29.1"
132
132
  },
133
- "gitHead": "ca527d7a89a9cff83d965e384e5f3d27c24efe82"
133
+ "gitHead": "0caf3b1c7a4e75924dab2e3e92673c34dadc36f9"
134
134
  }
@@ -1,4 +1,4 @@
1
- import { RichTextInput } from '@/vdb/components/data-input/richt-text-input.js';
1
+ import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
2
2
  import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
3
3
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
4
4
  import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
@@ -1,4 +1,4 @@
1
- import { RichTextInput } from '@/vdb/components/data-input/richt-text-input.js';
1
+ import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
2
2
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
3
3
  import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
4
4
  import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
@@ -1,4 +1,4 @@
1
- import { RichTextInput } from '@/vdb/components/data-input/richt-text-input.js';
1
+ import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
2
2
  import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-values.js';
3
3
  import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
4
4
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
@@ -1,5 +1,5 @@
1
1
  import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
2
- import { RichTextInput } from '@/vdb/components/data-input/richt-text-input.js';
2
+ import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
3
3
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
4
4
  import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
5
5
  import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
@@ -0,0 +1,11 @@
1
+ // Existing data input components
2
+ export * from './affixed-input.js';
3
+ export * from './customer-group-input.js';
4
+ export * from './datetime-input.js';
5
+ export * from './facet-value-input.js';
6
+ export * from './money-input.js';
7
+ export * from './rich-text-input.js';
8
+
9
+ // Relation selector components
10
+ export * from './relation-input.js';
11
+ export * from './relation-selector.js';
@@ -1,18 +1,11 @@
1
+ import { DataInputComponentProps } from '@/vdb/framework/component-registry/component-registry.js';
1
2
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
2
3
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
3
4
  import { useEffect, useMemo, useState } from 'react';
4
5
  import { AffixedInput } from './affixed-input.js';
5
6
 
6
7
  // Original component
7
- function MoneyInputInternal({
8
- value,
9
- currency,
10
- onChange,
11
- }: {
12
- value: number;
13
- currency: string;
14
- onChange: (value: number) => void;
15
- }) {
8
+ function MoneyInputInternal({ value, currency, onChange }: DataInputComponentProps) {
16
9
  const {
17
10
  settings: { displayLanguage, displayLocale },
18
11
  } = useUserSettings();
@@ -0,0 +1,156 @@
1
+ import { graphql } from '@/vdb/graphql/graphql.js';
2
+ import { createRelationSelectorConfig, RelationSelector } from './relation-selector.js';
3
+
4
+ // Re-export for convenience
5
+ export { createRelationSelectorConfig };
6
+
7
+ /**
8
+ * Single relation input component
9
+ */
10
+ export interface SingleRelationInputProps<T = any> {
11
+ value: string;
12
+ onChange: (value: string) => void;
13
+ config: Parameters<typeof createRelationSelectorConfig<T>>[0];
14
+ disabled?: boolean;
15
+ className?: string;
16
+ }
17
+
18
+ export function SingleRelationInput<T>({
19
+ value,
20
+ onChange,
21
+ config,
22
+ disabled,
23
+ className,
24
+ }: Readonly<SingleRelationInputProps<T>>) {
25
+ const singleConfig = createRelationSelectorConfig<T>({
26
+ ...config,
27
+ multiple: false,
28
+ });
29
+
30
+ return (
31
+ <RelationSelector
32
+ config={singleConfig}
33
+ value={value}
34
+ onChange={newValue => onChange(newValue as string)}
35
+ disabled={disabled}
36
+ className={className}
37
+ />
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Multi relation input component
43
+ */
44
+ export interface MultiRelationInputProps<T = any> {
45
+ value: string[];
46
+ onChange: (value: string[]) => void;
47
+ config: Parameters<typeof createRelationSelectorConfig<T>>[0];
48
+ disabled?: boolean;
49
+ className?: string;
50
+ }
51
+
52
+ export function MultiRelationInput<T>({
53
+ value,
54
+ onChange,
55
+ config,
56
+ disabled,
57
+ className,
58
+ }: Readonly<MultiRelationInputProps<T>>) {
59
+ const multiConfig = createRelationSelectorConfig<T>({
60
+ ...config,
61
+ multiple: true,
62
+ });
63
+
64
+ return (
65
+ <RelationSelector
66
+ config={multiConfig}
67
+ value={value}
68
+ onChange={newValue => onChange(newValue as string[])}
69
+ disabled={disabled}
70
+ className={className}
71
+ />
72
+ );
73
+ }
74
+
75
+ // Example configurations for common entities
76
+
77
+ /**
78
+ * Product relation selector configuration
79
+ */
80
+ export const productRelationConfig = createRelationSelectorConfig({
81
+ listQuery: graphql(`
82
+ query GetProductsForRelationSelector($options: ProductListOptions) {
83
+ products(options: $options) {
84
+ items {
85
+ id
86
+ name
87
+ slug
88
+ featuredAsset {
89
+ id
90
+ preview
91
+ }
92
+ }
93
+ totalItems
94
+ }
95
+ }
96
+ `),
97
+ idKey: 'id' as const,
98
+ labelKey: 'name' as const,
99
+ placeholder: 'Search products...',
100
+ buildSearchFilter: (term: string) => ({
101
+ name: { contains: term },
102
+ }),
103
+ });
104
+
105
+ /**
106
+ * Customer relation selector configuration
107
+ */
108
+ export const customerRelationConfig = createRelationSelectorConfig({
109
+ listQuery: graphql(`
110
+ query GetCustomersForRelationSelector($options: CustomerListOptions) {
111
+ customers(options: $options) {
112
+ items {
113
+ id
114
+ firstName
115
+ lastName
116
+ emailAddress
117
+ }
118
+ totalItems
119
+ }
120
+ }
121
+ `),
122
+ idKey: 'id' as const,
123
+ labelKey: 'emailAddress' as const,
124
+ placeholder: 'Search customers...',
125
+ buildSearchFilter: (term: string) => ({
126
+ emailAddress: { contains: term },
127
+ }),
128
+ });
129
+
130
+ /**
131
+ * Collection relation selector configuration
132
+ */
133
+ export const collectionRelationConfig = createRelationSelectorConfig({
134
+ listQuery: graphql(`
135
+ query GetCollectionsForRelationSelector($options: CollectionListOptions) {
136
+ collections(options: $options) {
137
+ items {
138
+ id
139
+ name
140
+ slug
141
+ featuredAsset {
142
+ id
143
+ preview
144
+ }
145
+ }
146
+ totalItems
147
+ }
148
+ }
149
+ `),
150
+ idKey: 'id' as const,
151
+ labelKey: 'name' as const,
152
+ placeholder: 'Search collections...',
153
+ buildSearchFilter: (term: string) => ({
154
+ name: { contains: term },
155
+ }),
156
+ });
@@ -0,0 +1,350 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import { Checkbox } from '@/vdb/components/ui/checkbox.js';
3
+ import {
4
+ Command,
5
+ CommandEmpty,
6
+ CommandInput,
7
+ CommandItem,
8
+ CommandList,
9
+ } from '@/vdb/components/ui/command.js';
10
+ import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
11
+ import { getQueryName } from '@/vdb/framework/document-introspection/get-document-structure.js';
12
+ import { api } from '@/vdb/graphql/api.js';
13
+ import { Trans } from '@/vdb/lib/trans.js';
14
+ import { useInfiniteQuery } from '@tanstack/react-query';
15
+ import { useDebounce } from '@uidotdev/usehooks';
16
+ import type { DocumentNode } from 'graphql';
17
+ import { CheckIcon, Loader2, Plus, X } from 'lucide-react';
18
+ import React, { useState } from 'react';
19
+
20
+ export interface RelationSelectorConfig<T = any> {
21
+ /** The GraphQL query document for fetching items */
22
+ listQuery: DocumentNode;
23
+ /** The property key for the entity ID */
24
+ idKey: keyof T;
25
+ /** The property key for the display label */
26
+ labelKey: keyof T;
27
+ /** Number of items to load per page */
28
+ pageSize?: number;
29
+ /** Placeholder text for the search input */
30
+ placeholder?: string;
31
+ /** Whether to enable multi-select mode */
32
+ multiple?: boolean;
33
+ /** Custom filter function for search */
34
+ buildSearchFilter?: (searchTerm: string) => any;
35
+ /** Custom label renderer function for rich display */
36
+ label?: (item: T) => React.ReactNode;
37
+ }
38
+
39
+ export interface RelationSelectorProps<T = any> {
40
+ config: RelationSelectorConfig<T>;
41
+ value?: string | string[];
42
+ onChange: (value: string | string[]) => void;
43
+ disabled?: boolean;
44
+ className?: string;
45
+ }
46
+
47
+ export interface RelationSelectorItemProps<T = any> {
48
+ item: T;
49
+ config: RelationSelectorConfig<T>;
50
+ isSelected: boolean;
51
+ onSelect: () => void;
52
+ showCheckbox?: boolean;
53
+ }
54
+
55
+ /**
56
+ * Abstract relation selector item component
57
+ */
58
+ export function RelationSelectorItem<T>({
59
+ item,
60
+ config,
61
+ isSelected,
62
+ onSelect,
63
+ showCheckbox = false,
64
+ }: Readonly<RelationSelectorItemProps<T>>) {
65
+ const id = String(item[config.idKey]);
66
+ const label = config.label ? config.label(item) : String(item[config.labelKey]);
67
+
68
+ return (
69
+ <CommandItem key={id} value={id} onSelect={onSelect} className="flex items-center gap-2">
70
+ {showCheckbox && (
71
+ <Checkbox
72
+ checked={isSelected}
73
+ onChange={onSelect}
74
+ onClick={(e: React.MouseEvent) => e.stopPropagation()}
75
+ />
76
+ )}
77
+ {isSelected && !showCheckbox && <CheckIcon className="h-4 w-4" />}
78
+ <span className="flex-1">{label}</span>
79
+ </CommandItem>
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Hook for managing relation selector state and queries
85
+ */
86
+ export function useRelationSelector<T>(config: RelationSelectorConfig<T>) {
87
+ const [searchTerm, setSearchTerm] = useState('');
88
+ const debouncedSearch = useDebounce(searchTerm, 300);
89
+
90
+ const pageSize = config.pageSize ?? 25;
91
+
92
+ // Build the default search filter if none provided
93
+ const buildFilter =
94
+ config.buildSearchFilter ??
95
+ ((term: string) => ({
96
+ [config.labelKey]: { contains: term },
97
+ }));
98
+
99
+ const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, error } = useInfiniteQuery({
100
+ queryKey: ['relationSelector', getQueryName(config.listQuery), debouncedSearch],
101
+ queryFn: async ({ pageParam = 0 }) => {
102
+ const variables: any = {
103
+ options: {
104
+ skip: pageParam * pageSize,
105
+ take: pageSize,
106
+ sort: { [config.labelKey]: 'ASC' },
107
+ },
108
+ };
109
+
110
+ // Add search filter if there's a search term
111
+ if (debouncedSearch.trim().length > 0) {
112
+ variables.options.filter = buildFilter(debouncedSearch.trim());
113
+ }
114
+
115
+ const response = (await api.query(config.listQuery, variables)) as any;
116
+ return response[getQueryName(config.listQuery)];
117
+ },
118
+ getNextPageParam: (lastPage, allPages) => {
119
+ if (!lastPage) return undefined;
120
+ const totalFetched = allPages.length * pageSize;
121
+ return totalFetched < lastPage.totalItems ? allPages.length : undefined;
122
+ },
123
+ initialPageParam: 0,
124
+ });
125
+
126
+ const items = data?.pages.flatMap(page => page?.items ?? []) ?? [];
127
+
128
+ return {
129
+ items,
130
+ isLoading,
131
+ fetchNextPage,
132
+ hasNextPage,
133
+ isFetchingNextPage,
134
+ error,
135
+ searchTerm,
136
+ setSearchTerm,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Abstract relation selector component
142
+ */
143
+ export function RelationSelector<T>({
144
+ config,
145
+ value,
146
+ onChange,
147
+ disabled,
148
+ className,
149
+ }: Readonly<RelationSelectorProps<T>>) {
150
+ const [open, setOpen] = useState(false);
151
+ const [selectedItemsCache, setSelectedItemsCache] = useState<T[]>([]);
152
+ const isMultiple = config.multiple ?? false;
153
+
154
+ const { items, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, searchTerm, setSearchTerm } =
155
+ useRelationSelector(config);
156
+
157
+ // Normalize value to always be an array for easier handling
158
+ const selectedIds = React.useMemo(() => {
159
+ if (isMultiple) {
160
+ return Array.isArray(value) ? value : value ? [value] : [];
161
+ }
162
+ return value ? [String(value)] : [];
163
+ }, [value, isMultiple]);
164
+
165
+ const handleSelect = (item: T) => {
166
+ const itemId = String(item[config.idKey]);
167
+
168
+ if (isMultiple) {
169
+ const isCurrentlySelected = selectedIds.includes(itemId);
170
+ const newSelectedIds = isCurrentlySelected
171
+ ? selectedIds.filter(id => id !== itemId)
172
+ : [...selectedIds, itemId];
173
+
174
+ // Update cache: add item if selecting, remove if deselecting
175
+ setSelectedItemsCache(prev => {
176
+ if (isCurrentlySelected) {
177
+ return prev.filter(prevItem => String(prevItem[config.idKey]) !== itemId);
178
+ } else {
179
+ // Only add if not already in cache
180
+ const alreadyInCache = prev.some(prevItem => String(prevItem[config.idKey]) === itemId);
181
+ return alreadyInCache ? prev : [...prev, item];
182
+ }
183
+ });
184
+
185
+ onChange(newSelectedIds);
186
+ } else {
187
+ // For single select, update cache with the new item
188
+ setSelectedItemsCache([item]);
189
+ onChange(itemId);
190
+ setOpen(false);
191
+ setSearchTerm('');
192
+ }
193
+ };
194
+
195
+ const handleRemove = (itemId: string) => {
196
+ if (isMultiple) {
197
+ const newSelectedIds = selectedIds.filter(id => id !== itemId);
198
+ // Remove from cache as well
199
+ setSelectedItemsCache(prev => prev.filter(prevItem => String(prevItem[config.idKey]) !== itemId));
200
+ onChange(newSelectedIds);
201
+ } else {
202
+ // Clear cache for single select
203
+ setSelectedItemsCache([]);
204
+ onChange('');
205
+ }
206
+ };
207
+
208
+ const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
209
+ const target = e.currentTarget;
210
+ const scrolledToBottom = Math.abs(target.scrollHeight - target.clientHeight - target.scrollTop) < 1;
211
+
212
+ if (scrolledToBottom && hasNextPage && !isFetchingNextPage) {
213
+ fetchNextPage();
214
+ }
215
+ };
216
+
217
+ // Clean up cache when selectedIds change externally (e.g., form reset)
218
+ React.useEffect(() => {
219
+ setSelectedItemsCache(prev => prev.filter(item => selectedIds.includes(String(item[config.idKey]))));
220
+ }, [selectedIds, config.idKey]);
221
+
222
+ // Populate cache with items from search results that are selected but not yet cached
223
+ React.useEffect(() => {
224
+ const itemsToAdd = items.filter(item => {
225
+ const itemId = String(item[config.idKey]);
226
+ const isSelected = selectedIds.includes(itemId);
227
+ const isAlreadyCached = selectedItemsCache.some(
228
+ cachedItem => String(cachedItem[config.idKey]) === itemId,
229
+ );
230
+ return isSelected && !isAlreadyCached;
231
+ });
232
+
233
+ if (itemsToAdd.length > 0) {
234
+ setSelectedItemsCache(prev => [...prev, ...itemsToAdd]);
235
+ }
236
+ }, [items, selectedIds, selectedItemsCache, config.idKey]);
237
+
238
+ // Get selected items for display from cache, filtered by current selection
239
+ const selectedItems = React.useMemo(() => {
240
+ return selectedItemsCache.filter(item => selectedIds.includes(String(item[config.idKey])));
241
+ }, [selectedItemsCache, selectedIds, config.idKey]);
242
+
243
+ return (
244
+ <div className={className}>
245
+ {/* Display selected items */}
246
+ {selectedItems.length > 0 && (
247
+ <div className="flex flex-wrap gap-2 mb-2">
248
+ {selectedItems.map(item => {
249
+ const itemId = String(item[config.idKey]);
250
+ const label = config.label ? config.label(item) : String(item[config.labelKey]);
251
+ return (
252
+ <div
253
+ key={itemId}
254
+ className="inline-flex items-center gap-1 bg-secondary text-secondary-foreground px-2 py-1 rounded-md text-sm"
255
+ >
256
+ <span>{label}</span>
257
+ {!disabled && (
258
+ <button
259
+ type="button"
260
+ onClick={() => handleRemove(itemId)}
261
+ className="text-secondary-foreground/70 hover:text-secondary-foreground"
262
+ >
263
+ <X className="h-3 w-3" />
264
+ </button>
265
+ )}
266
+ </div>
267
+ );
268
+ })}
269
+ </div>
270
+ )}
271
+
272
+ {/* Selector trigger */}
273
+ <Popover open={open} onOpenChange={setOpen}>
274
+ <PopoverTrigger asChild>
275
+ <Button variant="outline" size="sm" type="button" disabled={disabled} className="gap-2">
276
+ <Plus className="h-4 w-4" />
277
+ <Trans>
278
+ {isMultiple
279
+ ? selectedItems.length > 0
280
+ ? `Add more (${selectedItems.length} selected)`
281
+ : 'Select items'
282
+ : selectedItems.length > 0
283
+ ? 'Change selection'
284
+ : 'Select item'}
285
+ </Trans>
286
+ </Button>
287
+ </PopoverTrigger>
288
+ <PopoverContent className="p-0 w-[400px]" align="start">
289
+ <Command shouldFilter={false}>
290
+ <CommandInput
291
+ placeholder={config.placeholder ?? 'Search...'}
292
+ value={searchTerm}
293
+ onValueChange={setSearchTerm}
294
+ disabled={disabled}
295
+ />
296
+ <CommandList className="h-[300px] overflow-y-auto" onScroll={handleScroll}>
297
+ <CommandEmpty>
298
+ {isLoading ? (
299
+ <div className="flex items-center justify-center py-6">
300
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
301
+ <Trans>Loading...</Trans>
302
+ </div>
303
+ ) : (
304
+ <Trans>No results found</Trans>
305
+ )}
306
+ </CommandEmpty>
307
+
308
+ {items.map(item => {
309
+ const itemId = String(item[config.idKey]);
310
+ const isSelected = selectedIds.includes(itemId);
311
+
312
+ return (
313
+ <RelationSelectorItem
314
+ key={itemId}
315
+ item={item}
316
+ config={config}
317
+ isSelected={isSelected}
318
+ onSelect={() => handleSelect(item)}
319
+ showCheckbox={isMultiple}
320
+ />
321
+ );
322
+ })}
323
+
324
+ {(isFetchingNextPage || isLoading) && (
325
+ <div className="flex items-center justify-center py-2">
326
+ <Loader2 className="h-4 w-4 animate-spin" />
327
+ </div>
328
+ )}
329
+
330
+ {!hasNextPage && items.length > 0 && (
331
+ <div className="text-center py-2 text-sm text-muted-foreground">
332
+ <Trans>No more items</Trans>
333
+ </div>
334
+ )}
335
+ </CommandList>
336
+ </Command>
337
+ </PopoverContent>
338
+ </Popover>
339
+ </div>
340
+ );
341
+ }
342
+
343
+ /**
344
+ * Utility function to create a relation selector configuration
345
+ */
346
+ export function createRelationSelectorConfig<T>(
347
+ config: Readonly<RelationSelectorConfig<T>>,
348
+ ): RelationSelectorConfig<T> {
349
+ return config;
350
+ }
@@ -1,25 +1,34 @@
1
- import {
2
- FormControl,
3
- FormDescription,
4
- FormItem,
5
- FormLabel,
6
- FormMessage,
7
- FormField,
8
- } from '../ui/form.js';
9
- import { FieldValues, FieldPath } from 'react-hook-form';
1
+ import { FieldPath, FieldValues } from 'react-hook-form';
2
+ import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../ui/form.js';
10
3
 
11
4
  export type FormFieldWrapperProps<
12
5
  TFieldValues extends FieldValues = FieldValues,
13
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
6
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
14
7
  > = React.ComponentProps<typeof FormField<TFieldValues, TName>> & {
15
8
  label?: React.ReactNode;
16
9
  description?: React.ReactNode;
10
+ /**
11
+ * @description
12
+ * Whether to render the form control.
13
+ * If false, the form control will not be rendered.
14
+ * This is useful when you want to render the form control in a custom way, e.g. for <Select/> components,
15
+ * where the FormControl needs to nested in the root component.
16
+ * @default true
17
+ */
18
+ renderFormControl?: boolean;
17
19
  };
18
20
 
19
21
  export function FormFieldWrapper<
20
22
  TFieldValues extends FieldValues = FieldValues,
21
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
22
- >({ control, name, render, label, description }: FormFieldWrapperProps<TFieldValues, TName>) {
23
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
24
+ >({
25
+ control,
26
+ name,
27
+ render,
28
+ label,
29
+ description,
30
+ renderFormControl = true,
31
+ }: FormFieldWrapperProps<TFieldValues, TName>) {
23
32
  return (
24
33
  <FormField
25
34
  control={control}
@@ -27,7 +36,7 @@ export function FormFieldWrapper<
27
36
  render={renderArgs => (
28
37
  <FormItem>
29
38
  {label && <FormLabel>{label}</FormLabel>}
30
- <FormControl>{render(renderArgs)}</FormControl>
39
+ {renderFormControl ? <FormControl>{render(renderArgs)}</FormControl> : render(renderArgs)}
31
40
  {description && <FormDescription>{description}</FormDescription>}
32
41
  <FormMessage />
33
42
  </FormItem>
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
2
3
  import { addDisplayComponent, getDisplayComponent } from '../extension-api/display-component-extensions.js';
3
4
  import { addInputComponent, getInputComponent } from '../extension-api/input-component-extensions.js';
4
5
 
@@ -13,9 +14,10 @@ export interface DataDisplayComponentProps {
13
14
  [key: string]: any;
14
15
  }
15
16
 
16
- export interface DataInputComponentProps {
17
- value: any;
18
- onChange: (value: any) => void;
17
+ export interface DataInputComponentProps<
18
+ TFieldValues extends FieldValues = FieldValues,
19
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
20
+ > extends ControllerRenderProps<TFieldValues, TName> {
19
21
  [key: string]: any;
20
22
  }
21
23
 
@@ -17,6 +17,7 @@ import {
17
17
  } from '../document-introspection/get-document-structure.js';
18
18
 
19
19
  import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
20
+ import { FormControl } from '@/vdb/components/ui/form.js';
20
21
  import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
21
22
  import { useComponentRegistry } from '../component-registry/component-registry.js';
22
23
  import { generateInputComponentKey } from '../extension-api/input-component-extensions.js';
@@ -120,23 +121,31 @@ function FieldInputRenderer<
120
121
  return <InputComponent {...field} />;
121
122
  }
122
123
 
123
- switch (fieldInfo.type) {
124
- case 'Int':
125
- case 'Float':
126
- return (
127
- <Input
128
- type="number"
129
- value={field.value}
130
- onChange={e => field.onChange(e.target.valueAsNumber)}
131
- />
132
- );
133
- case 'DateTime':
134
- return <DateTimeInput {...field} />;
135
- case 'Boolean':
136
- return <Checkbox value={field.value} onCheckedChange={field.onChange} />;
137
- default:
138
- return <Input {...field} />;
139
- }
124
+ const DefaultComponent = () => {
125
+ switch (fieldInfo.type) {
126
+ case 'Int':
127
+ case 'Float':
128
+ return (
129
+ <Input
130
+ type="number"
131
+ value={field.value}
132
+ onChange={e => field.onChange(e.target.valueAsNumber)}
133
+ />
134
+ );
135
+ case 'DateTime':
136
+ return <DateTimeInput {...field} />;
137
+ case 'Boolean':
138
+ return <Checkbox value={field.value} onCheckedChange={field.onChange} />;
139
+ default:
140
+ return <Input {...field} />;
141
+ }
142
+ };
143
+
144
+ return (
145
+ <FormControl>
146
+ <DefaultComponent />
147
+ </FormControl>
148
+ );
140
149
  }
141
150
 
142
151
  /**
@@ -227,6 +236,7 @@ export function DetailPage<
227
236
  control={form.control}
228
237
  name={fieldInfo.name as never}
229
238
  label={fieldInfo.name}
239
+ renderFormControl={false}
230
240
  render={({ field }) => (
231
241
  <FieldInputRenderer
232
242
  fieldInfo={fieldInfo}
@@ -249,6 +259,7 @@ export function DetailPage<
249
259
  control={form.control}
250
260
  name={fieldInfo.name as never}
251
261
  label={fieldInfo.name}
262
+ renderFormControl={false}
252
263
  render={({ field }) => (
253
264
  <FieldInputRenderer
254
265
  fieldInfo={fieldInfo}
package/src/lib/index.ts CHANGED
@@ -9,7 +9,9 @@ export * from './components/data-input/customer-group-input.js';
9
9
  export * from './components/data-input/datetime-input.js';
10
10
  export * from './components/data-input/facet-value-input.js';
11
11
  export * from './components/data-input/money-input.js';
12
- export * from './components/data-input/richt-text-input.js';
12
+ export * from './components/data-input/relation-input.js';
13
+ export * from './components/data-input/relation-selector.js';
14
+ export * from './components/data-input/rich-text-input.js';
13
15
  export * from './components/data-table/add-filter-menu.js';
14
16
  export * from './components/data-table/data-table-bulk-action-item.js';
15
17
  export * from './components/data-table/data-table-bulk-actions.js';