@vendure/dashboard 3.3.6-master-202507041203 → 3.3.6-master-202507080235

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-202507041203",
4
+ "version": "3.3.6-master-202507080235",
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-202507041203",
90
- "@vendure/core": "^3.3.6-master-202507041203",
89
+ "@vendure/common": "^3.3.6-master-202507080235",
90
+ "@vendure/core": "^3.3.6-master-202507080235",
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": "bf87fca72814e2d3eb0a12b0d6575f234f1be67c"
133
+ "gitHead": "f089b48761d79df27110796160c255bb9d816884"
134
134
  }
@@ -40,7 +40,7 @@ export function CustomerOrderTable({ customerId }: Readonly<CustomerOrderTablePr
40
40
  cell: ({ cell, row }) => {
41
41
  const value = cell.getValue();
42
42
  const currencyCode = row.original.currencyCode;
43
- return <Money value={value} currencyCode={currencyCode} />;
43
+ return <Money value={value} currency={currencyCode} />;
44
44
  },
45
45
  },
46
46
  totalWithTax: {
@@ -48,7 +48,7 @@ export function CustomerOrderTable({ customerId }: Readonly<CustomerOrderTablePr
48
48
  cell: ({ cell, row }) => {
49
49
  const value = cell.getValue();
50
50
  const currencyCode = row.original.currencyCode;
51
- return <Money value={value} currencyCode={currencyCode} />;
51
+ return <Money value={value} currency={currencyCode} />;
52
52
  },
53
53
  },
54
54
  state: {
@@ -10,10 +10,10 @@ export function MoneyGrossNet({ priceWithTax, price, currencyCode }: Readonly<Mo
10
10
  return (
11
11
  <div className="flex flex-col gap-1">
12
12
  <div>
13
- <Money value={priceWithTax} currencyCode={currencyCode} />
13
+ <Money value={priceWithTax} currency={currencyCode} />
14
14
  </div>
15
15
  <div className="text-xs text-muted-foreground">
16
- <Money value={price} currencyCode={currencyCode} />
16
+ <Money value={price} currency={currencyCode} />
17
17
  </div>
18
18
  </div>
19
19
  );
@@ -48,7 +48,7 @@ export function ShippingMethodSelector({
48
48
  <span className="text-sm font-medium">
49
49
  <Trans>Price</Trans>
50
50
  </span>
51
- <Money value={method.priceWithTax} currencyCode={currencyCode} />
51
+ <Money value={method.priceWithTax} currency={currencyCode} />
52
52
  </div>
53
53
  </div>
54
54
  </CardContent>
@@ -61,7 +61,7 @@ function OrderListPage() {
61
61
  cell: ({ cell, row }) => {
62
62
  const value = cell.getValue();
63
63
  const currencyCode = row.original.currencyCode;
64
- return <Money value={value} currencyCode={currencyCode} />;
64
+ return <Money value={value} currency={currencyCode} />;
65
65
  },
66
66
  },
67
67
  totalWithTax: {
@@ -69,7 +69,7 @@ function OrderListPage() {
69
69
  cell: ({ cell, row }) => {
70
70
  const value = cell.getValue();
71
71
  const currencyCode = row.original.currencyCode;
72
- return <Money value={value} currencyCode={currencyCode} />;
72
+ return <Money value={value} currency={currencyCode} />;
73
73
  },
74
74
  },
75
75
  state: {
@@ -84,7 +84,7 @@ function ProductDetailPage() {
84
84
  toast.success(i18n.t('Successfully updated product'));
85
85
  resetForm();
86
86
  if (creatingNewEntity) {
87
- await navigate({ to: `../${data.id}`, from: Route.id });
87
+ await navigate({ to: `../$id`, params: { id: data.id } });
88
88
  }
89
89
  },
90
90
  onError: err => {
@@ -14,7 +14,7 @@ export function CopyableText({ text }: Readonly<{ text: string }>) {
14
14
 
15
15
  return (
16
16
  <div className="flex items-center gap-2">
17
- <div className="font-mono text-sm">{text}</div>
17
+ <div className="font-mono">{text}</div>
18
18
  <button
19
19
  onClick={() => handleCopy(text, 'page')}
20
20
  className="p-1 hover:bg-muted rounded-md transition-colors"
@@ -1,3 +1,5 @@
1
+ import { OverriddenFormComponent } from '@/vdb/framework/form-engine/overridden-form-component.js';
2
+ import { LocationWrapper } from '@/vdb/framework/layout-engine/location-wrapper.js';
1
3
  import { FieldPath, FieldValues } from 'react-hook-form';
2
4
  import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../ui/form.js';
3
5
 
@@ -30,17 +32,29 @@ export function FormFieldWrapper<
30
32
  renderFormControl = true,
31
33
  }: FormFieldWrapperProps<TFieldValues, TName>) {
32
34
  return (
33
- <FormField
34
- control={control}
35
- name={name}
36
- render={renderArgs => (
37
- <FormItem>
38
- {label && <FormLabel>{label}</FormLabel>}
39
- {renderFormControl ? <FormControl>{render(renderArgs)}</FormControl> : render(renderArgs)}
40
- {description && <FormDescription>{description}</FormDescription>}
41
- <FormMessage />
42
- </FormItem>
43
- )}
44
- />
35
+ <LocationWrapper identifier={name}>
36
+ <FormField
37
+ control={control}
38
+ name={name}
39
+ render={renderArgs => (
40
+ <FormItem>
41
+ {label && <FormLabel>{label}</FormLabel>}
42
+ {renderFormControl ? (
43
+ <FormControl>
44
+ <OverriddenFormComponent field={renderArgs.field} fieldName={name}>
45
+ {render(renderArgs)}
46
+ </OverriddenFormComponent>
47
+ </FormControl>
48
+ ) : (
49
+ <OverriddenFormComponent field={renderArgs.field} fieldName={name}>
50
+ {render(renderArgs)}
51
+ </OverriddenFormComponent>
52
+ )}
53
+ {description && <FormDescription>{description}</FormDescription>}
54
+ <FormMessage />
55
+ </FormItem>
56
+ )}
57
+ />
58
+ </LocationWrapper>
45
59
  );
46
60
  }
@@ -1,3 +1,5 @@
1
+ import { OverriddenFormComponent } from '@/vdb/framework/form-engine/overridden-form-component.js';
2
+ import { LocationWrapper } from '@/vdb/framework/layout-engine/location-wrapper.js';
1
3
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
2
4
  import { Controller, ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
3
5
  import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '../ui/form.js';
@@ -53,17 +55,29 @@ export const TranslatableFormFieldWrapper = <
53
55
  ...props
54
56
  }: TranslatableFormFieldWrapperProps<TFieldValues>) => {
55
57
  return (
56
- <TranslatableFormField
57
- control={props.control}
58
- name={name}
59
- render={renderArgs => (
60
- <FormItem>
61
- {label && <FormLabel>{label}</FormLabel>}
62
- {renderFormControl ? <FormControl>{render(renderArgs)}</FormControl> : render(renderArgs)}
63
- {description && <FormDescription>{description}</FormDescription>}
64
- <FormMessage />
65
- </FormItem>
66
- )}
67
- />
58
+ <LocationWrapper identifier={name as string}>
59
+ <TranslatableFormField
60
+ control={props.control}
61
+ name={name}
62
+ render={renderArgs => (
63
+ <FormItem>
64
+ {label && <FormLabel>{label}</FormLabel>}
65
+ {renderFormControl ? (
66
+ <FormControl>
67
+ <OverriddenFormComponent field={renderArgs.field} fieldName={name as string}>
68
+ {render(renderArgs)}
69
+ </OverriddenFormComponent>
70
+ </FormControl>
71
+ ) : (
72
+ <OverriddenFormComponent field={renderArgs.field} fieldName={name as string}>
73
+ {render(renderArgs)}
74
+ </OverriddenFormComponent>
75
+ )}
76
+ {description && <FormDescription>{description}</FormDescription>}
77
+ <FormMessage />
78
+ </FormItem>
79
+ )}
80
+ />
81
+ </LocationWrapper>
68
82
  );
69
83
  };
@@ -0,0 +1,51 @@
1
+ import {
2
+ DataDisplayComponent,
3
+ DataInputComponent,
4
+ useComponentRegistry,
5
+ } from '@/vdb/framework/component-registry/component-registry.js';
6
+ import { generateInputComponentKey } from '@/vdb/framework/extension-api/input-component-extensions.js';
7
+ import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
8
+ import { usePage } from '@/vdb/hooks/use-page.js';
9
+ import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
10
+
11
+ export interface OverriddenFormComponent<
12
+ TFieldValues extends FieldValues = any,
13
+ TName extends FieldPath<TFieldValues> = any,
14
+ > {
15
+ fieldName: string;
16
+ field: ControllerRenderProps<TFieldValues, TName>;
17
+ children?: React.ReactNode;
18
+ }
19
+
20
+ /**
21
+ * @description
22
+ * Based on the pageId and blockId of where this is placed, it will check whether any custom components
23
+ * are registered and render them if so. Otherwise, it will render the children, which act as the
24
+ * default if this location has not been overridden.
25
+ *
26
+ * ```tsx
27
+ * <OverriddenFormComponent fieldName="myField" field={field}>
28
+ * <Input {...field} />
29
+ * </OverriddenFormComponent>
30
+ * ```
31
+ */
32
+ export function OverriddenFormComponent({ fieldName, field, children }: Readonly<OverriddenFormComponent>) {
33
+ const page = usePage();
34
+ const pageBlock = usePageBlock({ optional: true });
35
+ const componentRegistry = useComponentRegistry();
36
+ let DisplayComponent: DataDisplayComponent | undefined;
37
+ let InputComponent: DataInputComponent | undefined;
38
+ if (page.pageId && pageBlock?.blockId) {
39
+ const customInputComponentKey = generateInputComponentKey(page.pageId, pageBlock.blockId, fieldName);
40
+ DisplayComponent = componentRegistry.getDisplayComponent(customInputComponentKey);
41
+ InputComponent = componentRegistry.getInputComponent(customInputComponentKey);
42
+ }
43
+ if (DisplayComponent) {
44
+ return <DisplayComponent {...field} />;
45
+ }
46
+
47
+ if (InputComponent) {
48
+ return <InputComponent {...field} />;
49
+ }
50
+ return children ?? null;
51
+ }
@@ -7,7 +7,7 @@ import { useChannel } from '../../hooks/use-channel.js';
7
7
  import { useServerConfig } from '../../hooks/use-server-config.js';
8
8
  import { getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
9
9
  import { createFormSchemaFromFields, getDefaultValuesFromFields } from './form-schema-tools.js';
10
- import { transformRelationFields } from './utils.js';
10
+ import { removeEmptyIdFields, transformRelationFields } from './utils.js';
11
11
 
12
12
  export interface GeneratedFormOptions<
13
13
  T extends TypedDocumentNode<any, any>,
@@ -64,7 +64,10 @@ export function useGeneratedForm<
64
64
  };
65
65
  if (onSubmit) {
66
66
  submitHandler = (event: FormEvent) => {
67
- form.handleSubmit(onSubmit as any)(event);
67
+ const onSubmitWrapper = (values: any) => {
68
+ onSubmit(removeEmptyIdFields(values, updateFields));
69
+ };
70
+ form.handleSubmit(onSubmitWrapper)(event);
68
71
  };
69
72
  }
70
73
 
@@ -0,0 +1,37 @@
1
+ import { graphql, VariablesOf } from 'gql.tada';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
5
+
6
+ import { removeEmptyIdFields } from './utils.js';
7
+
8
+ const createProductDocument = graphql(`
9
+ mutation CreateProduct($input: CreateProductInput!) {
10
+ createProduct(input: $input) {
11
+ id
12
+ }
13
+ }
14
+ `);
15
+
16
+ type CreateProductInput = VariablesOf<typeof createProductDocument>;
17
+
18
+ describe('removeEmptyIdFields', () => {
19
+ it('should remove empty translation id field', () => {
20
+ const values: CreateProductInput = {
21
+ input: { translations: [{ id: '', languageCode: 'en' }] },
22
+ };
23
+ const fields = getOperationVariablesFields(createProductDocument);
24
+ const result = removeEmptyIdFields(values, fields);
25
+
26
+ expect(result).toEqual({ input: { translations: [{ languageCode: 'en' }] } });
27
+ });
28
+
29
+ it('should remove empty featuredAsset id field', () => {
30
+ const values: CreateProductInput = {
31
+ input: { featuredAssetId: '', translations: [] },
32
+ };
33
+ const fields = getOperationVariablesFields(createProductDocument);
34
+ const result = removeEmptyIdFields(values, fields);
35
+ expect(result).toEqual({ input: { translations: [] } });
36
+ });
37
+ });
@@ -56,3 +56,50 @@ export function transformRelationFields<E extends Record<string, any>>(fields: F
56
56
 
57
57
  return processedEntity;
58
58
  }
59
+
60
+ /**
61
+ * @description
62
+ * Due to the schema types, sometimes "create" mutations will have a default empty "id"
63
+ * field which can cause issues if we actually send them with a "create" mutation to the server.
64
+ * This function deletes any empty ID fields on the entity or its nested objects.
65
+ */
66
+ export function removeEmptyIdFields<T extends Record<string, any>>(values: T, fields: FieldInfo[]): T {
67
+ if (!values) {
68
+ return values;
69
+ }
70
+
71
+ // Create a deep copy to avoid mutating the original values
72
+ const result = structuredClone(values);
73
+
74
+ function recursiveRemove(obj: any, fieldDefs: FieldInfo[]) {
75
+ if (Array.isArray(obj)) {
76
+ for (const item of obj) {
77
+ recursiveRemove(item, fieldDefs);
78
+ }
79
+ } else if (typeof obj === 'object' && obj !== null) {
80
+ for (const field of fieldDefs) {
81
+ // Remove empty string ID fields at this level
82
+ if (field.type === 'ID' && typeof obj[field.name] === 'string' && obj[field.name] === '') {
83
+ delete obj[field.name];
84
+ }
85
+ // If the field is an object or array, recurse into it
86
+ if (Array.isArray(obj[field.name])) {
87
+ if (field.typeInfo) {
88
+ for (const item of obj[field.name]) {
89
+ recursiveRemove(item, field.typeInfo);
90
+ }
91
+ }
92
+ } else if (
93
+ typeof obj[field.name] === 'object' &&
94
+ obj[field.name] !== null &&
95
+ field.typeInfo
96
+ ) {
97
+ recursiveRemove(obj[field.name], field.typeInfo);
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ recursiveRemove(result, fields);
104
+ return result;
105
+ }
@@ -1,98 +1,128 @@
1
1
  import { CopyableText } from '@/vdb/components/shared/copyable-text.js';
2
2
  import { Button } from '@/vdb/components/ui/button.js';
3
3
  import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
4
+ import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
4
5
  import { usePage } from '@/vdb/hooks/use-page.js';
5
6
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
6
- import { Trans } from '@/vdb/lib/trans.js';
7
7
  import { cn } from '@/vdb/lib/utils.js';
8
- import { CodeXmlIcon, InfoIcon } from 'lucide-react';
9
- import { createContext, useContext, useState } from 'react';
8
+ import { CodeXmlIcon } from 'lucide-react';
9
+ import React, { useEffect, useState } from 'react';
10
10
 
11
- const LocationWrapperContext = createContext<{
12
- parentId: string | null;
13
- hoveredId: string | null;
14
- setHoveredId: ((id: string | null) => void) | null;
15
- }>({
16
- parentId: null,
17
- hoveredId: null,
18
- setHoveredId: null,
19
- });
11
+ // Singleton state for hover tracking
12
+ let globalHoveredId: string | null = null;
13
+ const hoverListeners: Set<(id: string | null) => void> = new Set();
20
14
 
21
- export function LocationWrapper({
22
- children,
23
- blockId,
24
- }: Readonly<{ children: React.ReactNode; blockId?: string }>) {
15
+ const setGlobalHoveredId = (id: string | null) => {
16
+ globalHoveredId = id;
17
+ hoverListeners.forEach(listener => listener(id));
18
+ };
19
+
20
+ export interface LocationWrapperProps {
21
+ children: React.ReactNode;
22
+ identifier?: string;
23
+ }
24
+
25
+ export function LocationWrapper({ children, identifier }: Readonly<LocationWrapperProps>) {
25
26
  const page = usePage();
27
+ const pageBlock = usePageBlock({ optional: true });
26
28
  const { settings } = useUserSettings();
27
29
  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
30
+ const blockId = pageBlock?.blockId ?? null;
28
31
  const isPageWrapper = !blockId;
29
32
 
30
- const [hoveredIdTopLevel, setHoveredIdTopLevel] = useState<string | null>(null);
31
- const { hoveredId, setHoveredId, parentId } = useContext(LocationWrapperContext);
32
- const id = `${page.pageId}-${blockId ?? 'page'}`;
33
- const isHovered = hoveredId === id || hoveredIdTopLevel === id;
33
+ const [hoveredId, setHoveredId] = useState<string | null>(globalHoveredId);
34
+ const id = `${page.pageId}-${blockId ?? 'page'}-${identifier ?? ''}`;
35
+ const isHovered = hoveredId === id;
36
+
37
+ // Subscribe to global hover changes
38
+ useEffect(() => {
39
+ const listener = (newHoveredId: string | null) => {
40
+ setHoveredId(newHoveredId);
41
+ };
42
+ hoverListeners.add(listener);
43
+ return () => {
44
+ hoverListeners.delete(listener);
45
+ };
46
+ }, []);
34
47
 
35
48
  const setHoverId = (id: string | null) => {
36
- if (setHoveredId) {
37
- setHoveredId(id);
38
- } else {
39
- setHoveredIdTopLevel(id);
49
+ setGlobalHoveredId(id);
50
+ };
51
+
52
+ const handleMouseEnter = () => {
53
+ // Set this element as hovered
54
+ setHoverId(id);
55
+ };
56
+
57
+ const handleMouseLeave = () => {
58
+ // If we're at the top level (page wrapper), go to null
59
+ // If we're at block level, go to page level
60
+ // If we're at identifier level, go to block level
61
+ if (isPageWrapper) {
62
+ setHoverId(null);
63
+ } else if (blockId && !identifier) {
64
+ // Block level - go to page level
65
+ setHoverId(`${page.pageId}-page-`);
66
+ } else if (identifier) {
67
+ // Identifier level - go to block level
68
+ setHoverId(`${page.pageId}-${blockId}-`);
40
69
  }
41
70
  };
42
71
 
43
72
  if (settings.devMode) {
44
73
  const pageId = page.pageId;
45
74
  return (
46
- <LocationWrapperContext.Provider
47
- value={{ hoveredId: hoveredIdTopLevel, setHoveredId: setHoveredIdTopLevel, parentId: id }}
75
+ <div
76
+ className={cn(
77
+ `ring-2 transition-all delay-50 relative`,
78
+ isHovered || isPopoverOpen ? 'ring-dev-mode' : 'ring-transparent',
79
+ isPageWrapper ? 'ring-inset' : '',
80
+ identifier ? 'rounded-md' : 'rounded-xl',
81
+ )}
82
+ onMouseEnter={handleMouseEnter}
83
+ onMouseLeave={handleMouseLeave}
48
84
  >
49
85
  <div
50
- className={cn(
51
- `ring-2 rounded-xl transition-all delay-50 relative`,
52
- isHovered || isPopoverOpen ? 'ring-dev-mode' : 'ring-transparent',
53
- isPageWrapper ? 'ring-inset' : '',
54
- )}
55
- onMouseEnter={() => setHoverId(id)}
56
- onMouseLeave={() => setHoverId(parentId)}
86
+ className={`absolute top-1 right-1 transition-all delay-50 z-10 ${isHovered || isPopoverOpen ? 'visible' : 'invisible'}`}
57
87
  >
58
- <div
59
- className={`absolute top-0.5 right-0.5 transition-all delay-50 z-10 ${isHovered || isPopoverOpen ? 'visible' : 'invisible'}`}
60
- >
61
- <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
62
- <PopoverTrigger asChild>
63
- <Button variant="ghost" size="icon" className="rounded-lg">
64
- <CodeXmlIcon className="text-dev-mode w-5 h-5" />
65
- </Button>
66
- </PopoverTrigger>
67
- <PopoverContent className="w-60">
68
- <div className="space-y-2">
69
- <div className="flex items-center gap-2">
70
- <InfoIcon className="h-4 w-4 text-dev-mode" />
71
- <span className="font-medium">
72
- <Trans>Location Details</Trans>
73
- </span>
74
- </div>
75
- <div className="space-y-1.5">
76
- {pageId && (
77
- <div>
78
- <div className="text-xs text-muted-foreground">pageId</div>
79
- <CopyableText text={pageId} />
80
- </div>
81
- )}
82
- {blockId && (
83
- <div>
84
- <div className="text-xs text-muted-foreground">blockId</div>
85
- <CopyableText text={blockId} />
86
- </div>
87
- )}
88
- </div>
88
+ <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
89
+ <PopoverTrigger asChild>
90
+ <Button
91
+ variant="secondary"
92
+ size="icon"
93
+ className="h-8 w-8 rounded-full bg-dev-mode/10 hover:bg-dev-mode/20 border border-dev-mode/20 shadow-sm"
94
+ >
95
+ <CodeXmlIcon className="text-dev-mode w-4 h-4" />
96
+ </Button>
97
+ </PopoverTrigger>
98
+ <PopoverContent className="w-48 p-3">
99
+ <div className="space-y-2">
100
+ <div className="space-y-1">
101
+ {pageId && (
102
+ <div className="text-xs">
103
+ <div className="text-muted-foreground mb-0.5">pageId</div>
104
+ <CopyableText text={pageId} />
105
+ </div>
106
+ )}
107
+ {blockId && (
108
+ <div className="text-xs">
109
+ <div className="text-muted-foreground mb-0.5">blockId</div>
110
+ <CopyableText text={blockId} />
111
+ </div>
112
+ )}
113
+ {identifier && (
114
+ <div className="text-xs">
115
+ <div className="text-muted-foreground mb-0.5">identifier</div>
116
+ <CopyableText text={identifier} />
117
+ </div>
118
+ )}
89
119
  </div>
90
- </PopoverContent>
91
- </Popover>
92
- </div>
93
- {children}
120
+ </div>
121
+ </PopoverContent>
122
+ </Popover>
94
123
  </div>
95
- </LocationWrapperContext.Provider>
124
+ {children}
125
+ </div>
96
126
  );
97
127
  }
98
128
  return children;
@@ -364,8 +364,8 @@ export function PageBlock({
364
364
  column,
365
365
  }: Readonly<PageBlockProps>) {
366
366
  return (
367
- <LocationWrapper blockId={blockId}>
368
- <PageBlockContext.Provider value={{ blockId, title, description, column }}>
367
+ <PageBlockContext.Provider value={{ blockId, title, description, column }}>
368
+ <LocationWrapper>
369
369
  <Card className={cn('w-full', className)}>
370
370
  {title || description ? (
371
371
  <CardHeader>
@@ -375,8 +375,8 @@ export function PageBlock({
375
375
  ) : null}
376
376
  <CardContent className={cn(!title ? 'pt-6' : '')}>{children}</CardContent>
377
377
  </Card>
378
- </PageBlockContext.Provider>
379
- </LocationWrapper>
378
+ </LocationWrapper>
379
+ </PageBlockContext.Provider>
380
380
  );
381
381
  }
382
382
 
@@ -397,11 +397,11 @@ export function FullWidthPageBlock({
397
397
  blockId,
398
398
  }: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
399
399
  return (
400
- <LocationWrapper blockId={blockId}>
401
- <PageBlockContext.Provider value={{ blockId, column: 'main' }}>
400
+ <PageBlockContext.Provider value={{ blockId, column: 'main' }}>
401
+ <LocationWrapper>
402
402
  <div className={cn('w-full', className)}>{children}</div>
403
- </PageBlockContext.Provider>
404
- </LocationWrapper>
403
+ </LocationWrapper>
404
+ </PageBlockContext.Provider>
405
405
  );
406
406
  }
407
407
 
@@ -19,8 +19,6 @@ import {
19
19
  import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
20
20
  import { FormControl } from '@/vdb/components/ui/form.js';
21
21
  import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
22
- import { useComponentRegistry } from '../component-registry/component-registry.js';
23
- import { generateInputComponentKey } from '../extension-api/input-component-extensions.js';
24
22
  import {
25
23
  CustomFieldsPageBlock,
26
24
  DetailFormGrid,
@@ -96,8 +94,6 @@ export interface DetailPageFieldProps<
96
94
  > {
97
95
  fieldInfo: FieldInfo;
98
96
  field: ControllerRenderProps<TFieldValues, TName>;
99
- blockId: string;
100
- pageId: string;
101
97
  }
102
98
 
103
99
  /**
@@ -106,21 +102,7 @@ export interface DetailPageFieldProps<
106
102
  function FieldInputRenderer<
107
103
  TFieldValues extends FieldValues = FieldValues,
108
104
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
109
- >({ fieldInfo, field, blockId, pageId }: DetailPageFieldProps<TFieldValues, TName>) {
110
- const componentRegistry = useComponentRegistry();
111
- const customInputComponentKey = generateInputComponentKey(pageId, blockId, fieldInfo.name);
112
-
113
- const DisplayComponent = componentRegistry.getDisplayComponent(customInputComponentKey);
114
- const InputComponent = componentRegistry.getInputComponent(customInputComponentKey);
115
-
116
- if (DisplayComponent) {
117
- return <DisplayComponent {...field} />;
118
- }
119
-
120
- if (InputComponent) {
121
- return <InputComponent {...field} />;
122
- }
123
-
105
+ >({ fieldInfo, field }: DetailPageFieldProps<TFieldValues, TName>) {
124
106
  switch (fieldInfo.type) {
125
107
  case 'Int':
126
108
  case 'Float':
@@ -244,12 +226,7 @@ export function DetailPage<
244
226
  label={fieldInfo.name}
245
227
  renderFormControl={false}
246
228
  render={({ field }) => (
247
- <FieldInputRenderer
248
- fieldInfo={fieldInfo}
249
- field={field}
250
- blockId="main-form"
251
- pageId={pageId}
252
- />
229
+ <FieldInputRenderer fieldInfo={fieldInfo} field={field} />
253
230
  )}
254
231
  />
255
232
  );
@@ -267,12 +244,7 @@ export function DetailPage<
267
244
  label={fieldInfo.name}
268
245
  renderFormControl={false}
269
246
  render={({ field }) => (
270
- <FieldInputRenderer
271
- fieldInfo={fieldInfo}
272
- field={field}
273
- blockId="main-form"
274
- pageId={pageId}
275
- />
247
+ <FieldInputRenderer fieldInfo={fieldInfo} field={field} />
276
248
  )}
277
249
  />
278
250
  );
@@ -1,9 +1,17 @@
1
1
  import { PageBlockContext } from '@/vdb/framework/layout-engine/page-block-provider.js';
2
2
  import { useContext } from 'react';
3
3
 
4
- export function usePageBlock() {
4
+ /**
5
+ * @description
6
+ * Returns the current PageBlock context, which means there must be
7
+ * a PageBlock ancestor component higher in the tree.
8
+ *
9
+ * If `optional` is set to true, the hook will not throw if no PageBlock
10
+ * exists higher in the tree, but will just return undefined.
11
+ */
12
+ export function usePageBlock({ optional }: { optional?: boolean } = {}) {
5
13
  const pageBlock = useContext(PageBlockContext);
6
- if (!pageBlock) {
14
+ if (!pageBlock && !optional) {
7
15
  throw new Error('PageBlockProvider not found');
8
16
  }
9
17
  return pageBlock;