@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 +4 -4
- package/src/app/routes/_authenticated/_customers/components/customer-order-table.tsx +2 -2
- package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +2 -2
- package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/orders.tsx +2 -2
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +1 -1
- package/src/lib/components/shared/copyable-text.tsx +1 -1
- package/src/lib/components/shared/form-field-wrapper.tsx +26 -12
- package/src/lib/components/shared/translatable-form-field.tsx +26 -12
- package/src/lib/framework/form-engine/overridden-form-component.tsx +51 -0
- package/src/lib/framework/form-engine/use-generated-form.tsx +5 -2
- package/src/lib/framework/form-engine/utils.spec.ts +37 -0
- package/src/lib/framework/form-engine/utils.ts +47 -0
- package/src/lib/framework/layout-engine/location-wrapper.tsx +99 -69
- package/src/lib/framework/layout-engine/page-layout.tsx +8 -8
- package/src/lib/framework/page/detail-page.tsx +3 -31
- package/src/lib/hooks/use-page-block.tsx +10 -2
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-
|
|
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-
|
|
90
|
-
"@vendure/core": "^3.3.6-master-
|
|
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": "
|
|
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}
|
|
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}
|
|
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}
|
|
13
|
+
<Money value={priceWithTax} currency={currencyCode} />
|
|
14
14
|
</div>
|
|
15
15
|
<div className="text-xs text-muted-foreground">
|
|
16
|
-
<Money value={price}
|
|
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}
|
|
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}
|
|
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}
|
|
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: `../$
|
|
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
|
|
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
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
import {
|
|
8
|
+
import { CodeXmlIcon } from 'lucide-react';
|
|
9
|
+
import React, { useEffect, useState } from 'react';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
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 [
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
<
|
|
47
|
-
|
|
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={
|
|
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
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
</
|
|
91
|
-
</
|
|
92
|
-
</
|
|
93
|
-
{children}
|
|
120
|
+
</div>
|
|
121
|
+
</PopoverContent>
|
|
122
|
+
</Popover>
|
|
94
123
|
</div>
|
|
95
|
-
|
|
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
|
-
<
|
|
368
|
-
<
|
|
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
|
-
</
|
|
379
|
-
</
|
|
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
|
-
<
|
|
401
|
-
<
|
|
400
|
+
<PageBlockContext.Provider value={{ blockId, column: 'main' }}>
|
|
401
|
+
<LocationWrapper>
|
|
402
402
|
<div className={cn('w-full', className)}>{children}</div>
|
|
403
|
-
</
|
|
404
|
-
</
|
|
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
|
|
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
|
-
|
|
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;
|