@vendure/dashboard 3.4.1-master-202508210231 → 3.4.1
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 +152 -152
- package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +2 -1
- package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +1 -0
- package/src/lib/components/data-input/affixed-input.tsx +19 -5
- package/src/lib/components/data-input/boolean-input.tsx +9 -0
- package/src/lib/components/data-input/checkbox-input.tsx +8 -0
- package/src/lib/components/data-input/combination-mode-input.tsx +11 -2
- package/src/lib/components/data-input/configurable-operation-list-input.tsx +26 -401
- package/src/lib/components/data-input/custom-field-list-input.tsx +18 -25
- package/src/lib/components/data-input/customer-group-input.tsx +7 -11
- package/src/lib/components/data-input/datetime-input.tsx +9 -13
- package/src/lib/components/data-input/default-relation-input.tsx +29 -9
- package/src/lib/components/data-input/facet-value-input.tsx +15 -13
- package/src/lib/components/data-input/index.ts +2 -2
- package/src/lib/components/data-input/money-input.tsx +27 -20
- package/src/lib/components/data-input/number-input.tsx +48 -0
- package/src/lib/components/data-input/password-input.tsx +16 -0
- package/src/lib/components/data-input/{product-multi-selector.tsx → product-multi-selector-input.tsx} +8 -15
- package/src/lib/components/data-input/relation-input.tsx +7 -6
- package/src/lib/components/data-input/rich-text-input.tsx +10 -13
- package/src/lib/components/data-input/select-with-options.tsx +29 -17
- package/src/lib/components/data-input/struct-form-input.tsx +54 -59
- package/src/lib/components/data-input/text-input.tsx +9 -0
- package/src/lib/components/data-input/textarea-input.tsx +16 -0
- package/src/lib/components/data-table/filters/data-table-number-filter.tsx +3 -0
- package/src/lib/components/data-table/use-generated-columns.tsx +16 -5
- package/src/lib/components/shared/configurable-operation-arg-input.tsx +3 -10
- package/src/lib/components/shared/configurable-operation-input.tsx +1 -6
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +8 -5
- package/src/lib/components/shared/configurable-operation-selector.tsx +5 -5
- package/src/lib/components/shared/custom-fields-form.tsx +20 -49
- package/src/lib/components/shared/multi-select.tsx +1 -1
- package/src/lib/framework/component-registry/component-registry.tsx +9 -32
- package/src/lib/framework/component-registry/display-component.tsx +28 -0
- package/src/lib/framework/extension-api/display-component-extensions.tsx +0 -14
- package/src/lib/framework/extension-api/input-component-extensions.tsx +52 -34
- package/src/lib/framework/extension-api/logic/data-table.ts +4 -27
- package/src/lib/framework/extension-api/logic/form-components.ts +3 -2
- package/src/lib/framework/extension-api/types/detail-forms.ts +2 -38
- package/src/lib/framework/extension-api/types/form-components.ts +2 -4
- package/src/lib/framework/form-engine/custom-form-component-extensions.ts +0 -23
- package/src/lib/framework/form-engine/custom-form-component.tsx +8 -25
- package/src/lib/framework/form-engine/default-input-for-type.tsx +35 -0
- package/src/lib/framework/form-engine/form-control-adapter.tsx +192 -0
- package/src/lib/framework/form-engine/form-engine-types.ts +163 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +55 -71
- package/src/lib/framework/form-engine/overridden-form-component.tsx +2 -2
- package/src/lib/framework/form-engine/utils.ts +223 -0
- package/src/lib/{components/shared → framework/form-engine}/value-transformers.ts +9 -9
- package/src/lib/framework/registry/registry-types.ts +3 -5
- package/src/lib/graphql/graphql-env.d.ts +11 -7
- package/src/lib/index.ts +28 -1
- package/src/lib/providers/server-config.tsx +1 -0
- package/src/lib/components/shared/direct-form-component-map.tsx +0 -393
- package/src/lib/components/shared/universal-field-definition.ts +0 -118
- package/src/lib/components/shared/universal-form-input.tsx +0 -175
- package/src/lib/components/shared/universal-input-components.tsx +0 -291
- package/src/lib/framework/component-registry/dynamic-component.tsx +0 -58
|
@@ -5,6 +5,9 @@ import { ControllerRenderProps } from 'react-hook-form';
|
|
|
5
5
|
import { MultiRelationInput, SingleRelationInput } from './relation-input.js';
|
|
6
6
|
import { createRelationSelectorConfig } from './relation-selector.js';
|
|
7
7
|
|
|
8
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
9
|
+
import { isRelationCustomFieldConfig } from '@/vdb/framework/form-engine/utils.js';
|
|
10
|
+
|
|
8
11
|
interface PlaceholderIconProps {
|
|
9
12
|
letter: string;
|
|
10
13
|
className?: string;
|
|
@@ -551,8 +554,19 @@ interface DefaultRelationInputProps {
|
|
|
551
554
|
disabled?: boolean;
|
|
552
555
|
}
|
|
553
556
|
|
|
554
|
-
export function DefaultRelationInput({
|
|
557
|
+
export function DefaultRelationInput({
|
|
558
|
+
fieldDef,
|
|
559
|
+
value,
|
|
560
|
+
onChange,
|
|
561
|
+
onBlur,
|
|
562
|
+
name,
|
|
563
|
+
ref,
|
|
564
|
+
disabled,
|
|
565
|
+
}: Readonly<DashboardFormComponentProps>) {
|
|
555
566
|
const { i18n } = useLingui();
|
|
567
|
+
if (!fieldDef || !isRelationCustomFieldConfig(fieldDef)) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
556
570
|
const entityName = fieldDef.entity;
|
|
557
571
|
const ENTITY_CONFIGS = createEntityConfigs(i18n);
|
|
558
572
|
const config = ENTITY_CONFIGS[entityName as keyof typeof ENTITY_CONFIGS];
|
|
@@ -562,10 +576,10 @@ export function DefaultRelationInput({ fieldDef, field, disabled }: Readonly<Def
|
|
|
562
576
|
console.warn(`No relation selector config found for entity: ${entityName}`);
|
|
563
577
|
return (
|
|
564
578
|
<input
|
|
565
|
-
value={
|
|
566
|
-
onChange={e =>
|
|
567
|
-
onBlur={
|
|
568
|
-
name={
|
|
579
|
+
value={value ?? ''}
|
|
580
|
+
onChange={e => onChange(e.target.value)}
|
|
581
|
+
onBlur={onBlur}
|
|
582
|
+
name={name}
|
|
569
583
|
disabled={disabled}
|
|
570
584
|
placeholder={`Enter ${entityName} ID`}
|
|
571
585
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
@@ -578,8 +592,11 @@ export function DefaultRelationInput({ fieldDef, field, disabled }: Readonly<Def
|
|
|
578
592
|
if (isList) {
|
|
579
593
|
return (
|
|
580
594
|
<MultiRelationInput
|
|
581
|
-
|
|
582
|
-
|
|
595
|
+
onBlur={onBlur}
|
|
596
|
+
name={name}
|
|
597
|
+
ref={ref}
|
|
598
|
+
value={value ?? []}
|
|
599
|
+
onChange={onChange}
|
|
583
600
|
config={config}
|
|
584
601
|
disabled={disabled}
|
|
585
602
|
selectorLabel={<Trans>Select {entityName.toLowerCase()}s</Trans>}
|
|
@@ -588,8 +605,11 @@ export function DefaultRelationInput({ fieldDef, field, disabled }: Readonly<Def
|
|
|
588
605
|
} else {
|
|
589
606
|
return (
|
|
590
607
|
<SingleRelationInput
|
|
591
|
-
|
|
592
|
-
|
|
608
|
+
onBlur={onBlur}
|
|
609
|
+
name={name}
|
|
610
|
+
ref={ref}
|
|
611
|
+
value={value ?? ''}
|
|
612
|
+
onChange={onChange}
|
|
593
613
|
config={config}
|
|
594
614
|
disabled={disabled}
|
|
595
615
|
selectorLabel={<Trans>Select {entityName.toLowerCase()}</Trans>}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
1
2
|
import { api } from '@/vdb/graphql/api.js';
|
|
2
3
|
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
3
4
|
import { useQuery } from '@tanstack/react-query';
|
|
@@ -21,14 +22,8 @@ const facetValuesDocument = graphql(`
|
|
|
21
22
|
}
|
|
22
23
|
`);
|
|
23
24
|
|
|
24
|
-
export
|
|
25
|
-
value
|
|
26
|
-
onChange: (value: string) => void;
|
|
27
|
-
readOnly?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function FacetValueInput(props: FacetValueInputProps) {
|
|
31
|
-
const ids = decodeIds(props.value);
|
|
25
|
+
export const FacetValueInput: DashboardFormComponent = ({ value, onChange, disabled }) => {
|
|
26
|
+
const ids = decodeIds(value);
|
|
32
27
|
const { data } = useQuery({
|
|
33
28
|
queryKey: ['facetValues', ids],
|
|
34
29
|
queryFn: () =>
|
|
@@ -43,12 +38,12 @@ export function FacetValueInput(props: FacetValueInputProps) {
|
|
|
43
38
|
|
|
44
39
|
const onValueSelectHandler = (value: FacetValue) => {
|
|
45
40
|
const newIds = new Set([...ids, value.id]);
|
|
46
|
-
|
|
41
|
+
onChange(JSON.stringify(Array.from(newIds)));
|
|
47
42
|
};
|
|
48
43
|
|
|
49
44
|
const onValueRemoveHandler = (id: string) => {
|
|
50
45
|
const newIds = new Set(ids.filter(existingId => existingId !== id));
|
|
51
|
-
|
|
46
|
+
onChange(JSON.stringify(Array.from(newIds)));
|
|
52
47
|
};
|
|
53
48
|
|
|
54
49
|
return (
|
|
@@ -62,12 +57,19 @@ export function FacetValueInput(props: FacetValueInputProps) {
|
|
|
62
57
|
/>
|
|
63
58
|
))}
|
|
64
59
|
</div>
|
|
65
|
-
<FacetValueSelector onValueSelect={onValueSelectHandler} disabled={
|
|
60
|
+
<FacetValueSelector onValueSelect={onValueSelectHandler} disabled={disabled} />
|
|
66
61
|
</div>
|
|
67
62
|
);
|
|
68
|
-
}
|
|
63
|
+
};
|
|
69
64
|
|
|
70
|
-
|
|
65
|
+
FacetValueInput.metadata = {
|
|
66
|
+
isListInput: true,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function decodeIds(idsString: string | string[]): string[] {
|
|
70
|
+
if (Array.isArray(idsString)) {
|
|
71
|
+
return idsString;
|
|
72
|
+
}
|
|
71
73
|
try {
|
|
72
74
|
return JSON.parse(idsString);
|
|
73
75
|
} catch (error) {
|
|
@@ -9,8 +9,8 @@ export * from './select-with-options.js';
|
|
|
9
9
|
|
|
10
10
|
// Enhanced configurable operation input components
|
|
11
11
|
export * from './configurable-operation-list-input.js';
|
|
12
|
-
export * from './customer-group-
|
|
13
|
-
export * from './product-selector-input.js';
|
|
12
|
+
export * from './customer-group-input.js';
|
|
13
|
+
export * from './product-multi-selector-input.js';
|
|
14
14
|
|
|
15
15
|
// Relation selector components
|
|
16
16
|
export * from './relation-input.js';
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import { DataInputComponentProps } from '@/vdb/framework/component-registry/component-registry.js';
|
|
2
1
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
3
2
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
4
3
|
import { useEffect, useMemo, useState } from 'react';
|
|
5
4
|
import { AffixedInput } from './affixed-input.js';
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
7
|
+
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
8
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
9
|
+
|
|
10
|
+
export interface MoneyInputProps extends DashboardFormComponentProps {
|
|
11
|
+
currency?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function MoneyInput(props: Readonly<MoneyInputProps>) {
|
|
15
|
+
const { value, onChange, currency, ...rest } = props;
|
|
16
|
+
const { activeChannel } = useChannel();
|
|
17
|
+
const activeCurrency = currency ?? activeChannel?.defaultCurrencyCode;
|
|
18
|
+
const readOnly = isReadonlyField(props.fieldDef);
|
|
9
19
|
const {
|
|
10
20
|
settings: { displayLanguage, displayLocale },
|
|
11
21
|
} = useUserSettings();
|
|
@@ -19,39 +29,43 @@ function MoneyInputInternal({ value, currency, onChange }: DataInputComponentPro
|
|
|
19
29
|
|
|
20
30
|
// Determine if the currency symbol should be a prefix based on locale
|
|
21
31
|
const shouldPrefix = useMemo(() => {
|
|
22
|
-
if (!
|
|
32
|
+
if (!activeCurrency) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
23
35
|
const locale = displayLocale || displayLanguage.replace(/_/g, '-');
|
|
24
36
|
const parts = new Intl.NumberFormat(locale, {
|
|
25
37
|
style: 'currency',
|
|
26
|
-
currency,
|
|
38
|
+
currency: activeCurrency,
|
|
27
39
|
currencyDisplay: 'symbol',
|
|
28
40
|
}).formatToParts();
|
|
29
41
|
const NaNString = parts.find(p => p.type === 'nan')?.value ?? 'NaN';
|
|
30
42
|
const localised = new Intl.NumberFormat(locale, {
|
|
31
43
|
style: 'currency',
|
|
32
|
-
currency,
|
|
44
|
+
currency: activeCurrency,
|
|
33
45
|
currencyDisplay: 'symbol',
|
|
34
46
|
}).format(undefined as any);
|
|
35
47
|
return localised.indexOf(NaNString) > 0;
|
|
36
|
-
}, [
|
|
48
|
+
}, [activeCurrency, displayLocale, displayLanguage]);
|
|
37
49
|
|
|
38
50
|
// Get the currency symbol
|
|
39
51
|
const currencySymbol = useMemo(() => {
|
|
40
|
-
if (!
|
|
52
|
+
if (!activeCurrency) return '';
|
|
41
53
|
const locale = displayLocale || displayLanguage.replace(/_/g, '-');
|
|
42
54
|
const parts = new Intl.NumberFormat(locale, {
|
|
43
55
|
style: 'currency',
|
|
44
|
-
currency,
|
|
56
|
+
currency: activeCurrency,
|
|
45
57
|
currencyDisplay: 'symbol',
|
|
46
58
|
}).formatToParts();
|
|
47
|
-
return parts.find(p => p.type === 'currency')?.value ??
|
|
48
|
-
}, [
|
|
59
|
+
return parts.find(p => p.type === 'currency')?.value ?? activeCurrency;
|
|
60
|
+
}, [activeCurrency, displayLocale, displayLanguage]);
|
|
49
61
|
|
|
50
62
|
return (
|
|
51
63
|
<AffixedInput
|
|
52
64
|
type="text"
|
|
53
65
|
className="bg-background"
|
|
54
66
|
value={displayValue}
|
|
67
|
+
disabled={readOnly}
|
|
68
|
+
{...rest}
|
|
55
69
|
onChange={e => {
|
|
56
70
|
const inputValue = e.target.value;
|
|
57
71
|
// Allow empty input
|
|
@@ -77,8 +91,8 @@ function MoneyInputInternal({ value, currency, onChange }: DataInputComponentPro
|
|
|
77
91
|
}
|
|
78
92
|
}
|
|
79
93
|
}}
|
|
80
|
-
onBlur={
|
|
81
|
-
const inputValue =
|
|
94
|
+
onBlur={() => {
|
|
95
|
+
const inputValue = displayValue;
|
|
82
96
|
if (inputValue === '') {
|
|
83
97
|
onChange(0);
|
|
84
98
|
setDisplayValue('0');
|
|
@@ -97,10 +111,3 @@ function MoneyInputInternal({ value, currency, onChange }: DataInputComponentPro
|
|
|
97
111
|
/>
|
|
98
112
|
);
|
|
99
113
|
}
|
|
100
|
-
|
|
101
|
-
// Wrapper that makes it compatible with DataInputComponent
|
|
102
|
-
export function MoneyInput(props: { value: any; onChange: (value: any) => void; [key: string]: any }) {
|
|
103
|
-
const { value, onChange, ...rest } = props;
|
|
104
|
-
const currency = rest.currency || 'USD'; // Default currency if none provided
|
|
105
|
-
return <MoneyInputInternal value={value} currency={currency} onChange={onChange} />;
|
|
106
|
-
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AffixedInput } from '@/vdb/components/data-input/affixed-input.js';
|
|
2
|
+
import { Input } from '@/vdb/components/ui/input.js';
|
|
3
|
+
|
|
4
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
5
|
+
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
6
|
+
|
|
7
|
+
export function NumberInput({ fieldDef, onChange, ...fieldProps }: Readonly<DashboardFormComponentProps>) {
|
|
8
|
+
const readOnly = fieldProps.disabled || isReadonlyField(fieldDef);
|
|
9
|
+
const isFloat = fieldDef ? fieldDef.type === 'float' : false;
|
|
10
|
+
const min = fieldDef?.ui?.min;
|
|
11
|
+
const max = fieldDef?.ui?.max;
|
|
12
|
+
const step = fieldDef?.ui?.step || (isFloat ? 0.01 : 1);
|
|
13
|
+
const prefix = fieldDef?.ui?.prefix;
|
|
14
|
+
const suffix = fieldDef?.ui?.suffix;
|
|
15
|
+
const shouldUseAffixedInput = prefix || suffix;
|
|
16
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
17
|
+
if (readOnly) return;
|
|
18
|
+
onChange(e.target.valueAsNumber);
|
|
19
|
+
};
|
|
20
|
+
if (shouldUseAffixedInput) {
|
|
21
|
+
return (
|
|
22
|
+
<AffixedInput
|
|
23
|
+
{...fieldProps}
|
|
24
|
+
type="number"
|
|
25
|
+
onChange={handleChange}
|
|
26
|
+
min={min}
|
|
27
|
+
max={max}
|
|
28
|
+
step={step}
|
|
29
|
+
prefix={prefix}
|
|
30
|
+
suffix={suffix}
|
|
31
|
+
className="bg-background"
|
|
32
|
+
disabled={readOnly}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Input
|
|
39
|
+
type="number"
|
|
40
|
+
onChange={handleChange}
|
|
41
|
+
{...fieldProps}
|
|
42
|
+
min={min}
|
|
43
|
+
max={max}
|
|
44
|
+
step={step}
|
|
45
|
+
disabled={readOnly}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
2
|
+
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
3
|
+
import { Input } from '../ui/input.js';
|
|
4
|
+
|
|
5
|
+
export function PasswordInput(props: Readonly<DashboardFormComponentProps>) {
|
|
6
|
+
const readOnly = props.disabled || isReadonlyField(props.fieldDef);
|
|
7
|
+
return (
|
|
8
|
+
<Input
|
|
9
|
+
type="password"
|
|
10
|
+
ref={props.ref}
|
|
11
|
+
value={props.value}
|
|
12
|
+
onChange={e => props.onChange(e.target.value)}
|
|
13
|
+
disabled={readOnly}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
DialogTitle,
|
|
11
11
|
} from '@/vdb/components/ui/dialog.js';
|
|
12
12
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
13
|
-
import {
|
|
13
|
+
import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
14
14
|
import { api } from '@/vdb/graphql/api.js';
|
|
15
15
|
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
16
16
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
@@ -372,21 +372,12 @@ function ProductMultiSelectorDialog({
|
|
|
372
372
|
);
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
export const ProductMultiInput:
|
|
375
|
+
export const ProductMultiInput: DashboardFormComponent = ({ value, onChange, ...props }) => {
|
|
376
376
|
const [open, setOpen] = useState(false);
|
|
377
|
-
|
|
378
377
|
// Parse the configuration from the field definition
|
|
379
|
-
const mode =
|
|
380
|
-
|
|
378
|
+
const mode = props.fieldDef?.ui?.selectionMode === 'variant' ? 'variant' : 'product';
|
|
381
379
|
// Parse the current value (JSON array of IDs)
|
|
382
|
-
const selectedIds =
|
|
383
|
-
if (!value || typeof value !== 'string') return [];
|
|
384
|
-
try {
|
|
385
|
-
return JSON.parse(value);
|
|
386
|
-
} catch {
|
|
387
|
-
return [];
|
|
388
|
-
}
|
|
389
|
-
}, [value]);
|
|
380
|
+
const selectedIds = value;
|
|
390
381
|
|
|
391
382
|
const handleSelectionChange = useCallback(
|
|
392
383
|
(newSelectedIds: string[]) => {
|
|
@@ -394,11 +385,9 @@ export const ProductMultiInput: DataInputComponent = ({ value, onChange, ...prop
|
|
|
394
385
|
},
|
|
395
386
|
[onChange],
|
|
396
387
|
);
|
|
397
|
-
|
|
398
388
|
const itemType = mode === 'product' ? 'products' : 'variants';
|
|
399
389
|
const buttonText =
|
|
400
390
|
selectedIds.length > 0 ? `Selected ${selectedIds.length} ${itemType}` : `Select ${itemType}`;
|
|
401
|
-
|
|
402
391
|
return (
|
|
403
392
|
<>
|
|
404
393
|
<div className="space-y-2">
|
|
@@ -424,3 +413,7 @@ export const ProductMultiInput: DataInputComponent = ({ value, onChange, ...prop
|
|
|
424
413
|
</>
|
|
425
414
|
);
|
|
426
415
|
};
|
|
416
|
+
|
|
417
|
+
ProductMultiInput.metadata = {
|
|
418
|
+
isListInput: true,
|
|
419
|
+
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { DashboardFormComponent, DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
1
2
|
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
2
3
|
import { createRelationSelectorConfig, RelationSelector } from './relation-selector.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Single relation input component
|
|
6
7
|
*/
|
|
7
|
-
export interface SingleRelationInputProps<T = any> {
|
|
8
|
-
value: string;
|
|
9
|
-
onChange: (value: string) => void;
|
|
8
|
+
export interface SingleRelationInputProps<T = any> extends DashboardFormComponentProps {
|
|
10
9
|
config: Parameters<typeof createRelationSelectorConfig<T>>[0];
|
|
11
10
|
disabled?: boolean;
|
|
12
11
|
className?: string;
|
|
@@ -46,9 +45,7 @@ export function SingleRelationInput<T>({
|
|
|
46
45
|
/**
|
|
47
46
|
* Multi relation input component
|
|
48
47
|
*/
|
|
49
|
-
export interface MultiRelationInputProps<T = any> {
|
|
50
|
-
value: string[];
|
|
51
|
-
onChange: (value: string[]) => void;
|
|
48
|
+
export interface MultiRelationInputProps<T = any> extends DashboardFormComponentProps {
|
|
52
49
|
config: Parameters<typeof createRelationSelectorConfig<T>>[0];
|
|
53
50
|
disabled?: boolean;
|
|
54
51
|
className?: string;
|
|
@@ -80,6 +77,10 @@ export function MultiRelationInput<T>({
|
|
|
80
77
|
);
|
|
81
78
|
}
|
|
82
79
|
|
|
80
|
+
(MultiRelationInput as DashboardFormComponent).metadata = {
|
|
81
|
+
isListInput: true,
|
|
82
|
+
};
|
|
83
|
+
|
|
83
84
|
// Example configurations for common entities
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
1
2
|
import TextStyle from '@tiptap/extension-text-style';
|
|
2
3
|
import { BubbleMenu, Editor, EditorContent, useEditor } from '@tiptap/react';
|
|
3
4
|
import StarterKit from '@tiptap/starter-kit';
|
|
4
5
|
import { BoldIcon, ItalicIcon, StrikethroughIcon } from 'lucide-react';
|
|
5
6
|
import { useLayoutEffect, useRef } from 'react';
|
|
6
7
|
import { Button } from '../ui/button.js';
|
|
8
|
+
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
7
9
|
|
|
8
10
|
// define your extension array
|
|
9
11
|
const extensions = [
|
|
@@ -20,13 +22,8 @@ const extensions = [
|
|
|
20
22
|
}),
|
|
21
23
|
];
|
|
22
24
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
disabled?: boolean;
|
|
26
|
-
onChange: (value: string) => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextInputProps>) {
|
|
25
|
+
export function RichTextInput({ value, onChange, fieldDef }: Readonly<DashboardFormComponentProps>) {
|
|
26
|
+
const readOnly = isReadonlyField(fieldDef);
|
|
30
27
|
const isInternalUpdate = useRef(false);
|
|
31
28
|
|
|
32
29
|
const editor = useEditor({
|
|
@@ -35,16 +32,16 @@ export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextIn
|
|
|
35
32
|
},
|
|
36
33
|
extensions: extensions,
|
|
37
34
|
content: value,
|
|
38
|
-
editable: !
|
|
35
|
+
editable: !readOnly,
|
|
39
36
|
onUpdate: ({ editor }) => {
|
|
40
|
-
if (!
|
|
37
|
+
if (!readOnly) {
|
|
41
38
|
isInternalUpdate.current = true;
|
|
42
39
|
onChange(editor.getHTML());
|
|
43
40
|
}
|
|
44
41
|
},
|
|
45
42
|
editorProps: {
|
|
46
43
|
attributes: {
|
|
47
|
-
class: `border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/10 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[500px] overflow-y-auto ${
|
|
44
|
+
class: `border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/10 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[500px] overflow-y-auto ${readOnly ? 'cursor-not-allowed opacity-50' : ''}`,
|
|
48
45
|
},
|
|
49
46
|
},
|
|
50
47
|
});
|
|
@@ -64,9 +61,9 @@ export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextIn
|
|
|
64
61
|
// Update editor's editable state when disabled prop changes
|
|
65
62
|
useLayoutEffect(() => {
|
|
66
63
|
if (editor) {
|
|
67
|
-
editor.setEditable(!
|
|
64
|
+
editor.setEditable(!readOnly);
|
|
68
65
|
}
|
|
69
|
-
}, [
|
|
66
|
+
}, [readOnly, editor]);
|
|
70
67
|
|
|
71
68
|
if (!editor) {
|
|
72
69
|
return null;
|
|
@@ -75,7 +72,7 @@ export function RichTextInput({ value, onChange, disabled }: Readonly<RichTextIn
|
|
|
75
72
|
return (
|
|
76
73
|
<>
|
|
77
74
|
<EditorContent editor={editor} />
|
|
78
|
-
<CustomBubbleMenu editor={editor} disabled={
|
|
75
|
+
<CustomBubbleMenu editor={editor} disabled={readOnly} />
|
|
79
76
|
</>
|
|
80
77
|
);
|
|
81
78
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
|
|
2
|
+
import {
|
|
3
|
+
DashboardFormComponent,
|
|
4
|
+
DashboardFormComponentProps,
|
|
5
|
+
StringCustomFieldConfig,
|
|
6
|
+
} from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
7
|
+
import { isReadonlyField, isStringFieldWithOptions } from '@/vdb/framework/form-engine/utils.js';
|
|
2
8
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
3
9
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
4
|
-
import { StringFieldOption } from '@vendure/common/lib/generated-types';
|
|
5
10
|
import React from 'react';
|
|
6
|
-
import { ControllerRenderProps } from 'react-hook-form';
|
|
7
11
|
import { MultiSelect } from '../shared/multi-select.js';
|
|
8
12
|
|
|
9
|
-
export interface SelectWithOptionsProps {
|
|
10
|
-
field: ControllerRenderProps<any, any>;
|
|
11
|
-
options: StringFieldOption[];
|
|
12
|
-
disabled?: boolean;
|
|
13
|
+
export interface SelectWithOptionsProps extends DashboardFormComponentProps {
|
|
13
14
|
placeholder?: React.ReactNode;
|
|
14
15
|
isListField?: boolean;
|
|
15
16
|
}
|
|
@@ -22,12 +23,14 @@ export interface SelectWithOptionsProps {
|
|
|
22
23
|
* @since 3.3.0
|
|
23
24
|
*/
|
|
24
25
|
export function SelectWithOptions({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
value,
|
|
27
|
+
onChange,
|
|
28
|
+
fieldDef,
|
|
28
29
|
placeholder,
|
|
29
30
|
isListField = false,
|
|
31
|
+
disabled,
|
|
30
32
|
}: Readonly<SelectWithOptionsProps>) {
|
|
33
|
+
const readOnly = disabled || isReadonlyField(fieldDef);
|
|
31
34
|
const {
|
|
32
35
|
settings: { displayLanguage },
|
|
33
36
|
} = useUserSettings();
|
|
@@ -37,6 +40,11 @@ export function SelectWithOptions({
|
|
|
37
40
|
const translation = label.find(t => t.languageCode === displayLanguage);
|
|
38
41
|
return translation?.value ?? label[0]?.value ?? '';
|
|
39
42
|
};
|
|
43
|
+
if (!fieldDef || !isStringFieldWithOptions(fieldDef)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const options: NonNullable<StringCustomFieldConfig['options']> =
|
|
47
|
+
fieldDef.options ?? fieldDef.ui.options ?? [];
|
|
40
48
|
|
|
41
49
|
// Convert options to MultiSelect format
|
|
42
50
|
const multiSelectItems = options.map(option => ({
|
|
@@ -45,31 +53,31 @@ export function SelectWithOptions({
|
|
|
45
53
|
}));
|
|
46
54
|
|
|
47
55
|
// For list fields, use MultiSelect component
|
|
48
|
-
if (isListField) {
|
|
56
|
+
if (isListField || fieldDef?.list === true) {
|
|
49
57
|
return (
|
|
50
58
|
<MultiSelect
|
|
51
59
|
multiple={true}
|
|
52
|
-
value={
|
|
53
|
-
onChange={
|
|
60
|
+
value={value || []}
|
|
61
|
+
onChange={onChange}
|
|
54
62
|
items={multiSelectItems}
|
|
55
63
|
placeholder={placeholder ? String(placeholder) : 'Select options'}
|
|
56
|
-
className={
|
|
64
|
+
className={readOnly ? 'opacity-50 pointer-events-none' : ''}
|
|
57
65
|
/>
|
|
58
66
|
);
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
// For single fields, use regular Select
|
|
62
|
-
const currentValue =
|
|
70
|
+
const currentValue = value ?? '';
|
|
63
71
|
|
|
64
72
|
const handleValueChange = (value: string) => {
|
|
65
73
|
if (value) {
|
|
66
|
-
|
|
74
|
+
onChange(value);
|
|
67
75
|
}
|
|
68
76
|
};
|
|
69
77
|
|
|
70
78
|
return (
|
|
71
|
-
<Select value={currentValue ?? undefined} onValueChange={handleValueChange} disabled={
|
|
72
|
-
<SelectTrigger>
|
|
79
|
+
<Select value={currentValue ?? undefined} onValueChange={handleValueChange} disabled={readOnly}>
|
|
80
|
+
<SelectTrigger className="mb-0">
|
|
73
81
|
<SelectValue placeholder={placeholder || <Trans>Select an option</Trans>} />
|
|
74
82
|
</SelectTrigger>
|
|
75
83
|
<SelectContent>
|
|
@@ -82,3 +90,7 @@ export function SelectWithOptions({
|
|
|
82
90
|
</Select>
|
|
83
91
|
);
|
|
84
92
|
}
|
|
93
|
+
|
|
94
|
+
(SelectWithOptions as DashboardFormComponent).metadata = {
|
|
95
|
+
isListInput: 'dynamic',
|
|
96
|
+
};
|