@vendure/dashboard 3.6.0-minor-202511061555 → 3.6.0-minor-202512161454
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/dist/plugin/constants.js +2 -2
- package/dist/vite/constants.js +1 -0
- package/dist/vite/utils/compiler.d.ts +1 -0
- package/dist/vite/utils/compiler.js +5 -4
- package/dist/vite/utils/get-dashboard-paths.d.ts +5 -0
- package/dist/vite/utils/get-dashboard-paths.js +20 -0
- package/dist/vite/vite-plugin-dashboard-metadata.js +2 -1
- package/dist/vite/vite-plugin-tailwind-source.js +2 -15
- package/dist/vite/vite-plugin-translations.d.ts +10 -1
- package/dist/vite/vite-plugin-translations.js +156 -45
- package/dist/vite/vite-plugin-vendure-dashboard.d.ts +12 -0
- package/dist/vite/vite-plugin-vendure-dashboard.js +1 -0
- package/lingui.config.js +1 -0
- package/package.json +7 -7
- package/src/app/routeTree.gen.ts +1221 -0
- package/src/app/routes/_authenticated/_administrators/administrators.tsx +9 -12
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +6 -9
- package/src/app/routes/_authenticated/_channels/channels.tsx +9 -12
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_collections/collections.tsx +9 -12
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_countries/countries.tsx +9 -12
- package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +9 -12
- package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +0 -1
- package/src/app/routes/_authenticated/_customers/customers.tsx +9 -12
- package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +10 -13
- package/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx +139 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +3 -0
- package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +3 -1
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +3 -3
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +41 -41
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +49 -11
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +4 -1
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +2 -3
- package/src/app/routes/_authenticated/_orders/orders.tsx +3 -3
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +12 -3
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +27 -30
- package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +23 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +9 -12
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +3 -3
- package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +2 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +10 -12
- package/src/app/routes/_authenticated/_products/products.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_products/products.tsx +15 -18
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_profile/profile.tsx +3 -3
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +9 -12
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_roles/roles.tsx +9 -12
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_sellers/sellers.tsx +9 -12
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +11 -12
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +19 -20
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +9 -12
- package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_system/healthchecks.tsx +2 -3
- package/src/app/routes/_authenticated/_system/job-queue.tsx +3 -3
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -12
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +49 -1
- package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +34 -16
- package/src/app/routes/_authenticated/_zones/zones.tsx +9 -12
- package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +9 -12
- package/src/app/routes/_authenticated/index.tsx +5 -3
- package/src/i18n/locales/bg.po +3436 -0
- package/src/lib/components/data-input/datetime-input.tsx +1 -1
- package/src/lib/components/data-input/default-relation-input.tsx +1 -1
- package/src/lib/components/data-input/relation-selector.tsx +1 -1
- package/src/lib/components/data-input/string-list-input.tsx +188 -26
- package/src/lib/components/data-input/struct-form-input.tsx +175 -174
- package/src/lib/components/data-table/column-header-wrapper.tsx +1 -1
- package/src/lib/components/data-table/data-table-filter-badge.tsx +2 -2
- package/src/lib/components/data-table/data-table.tsx +1 -1
- package/src/lib/components/data-table/use-generated-columns.tsx +1 -1
- package/src/lib/components/layout/channel-switcher.tsx +6 -2
- package/src/lib/components/layout/content-language-selector.tsx +6 -7
- package/src/lib/components/layout/dev-mode-indicator.tsx +7 -3
- package/src/lib/components/layout/language-dialog.tsx +26 -13
- package/src/lib/components/layout/manage-languages-dialog.tsx +10 -29
- package/src/lib/components/layout/nav-item-wrapper.tsx +1 -1
- package/src/lib/components/shared/asset/asset-gallery.tsx +8 -3
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +14 -16
- package/src/lib/components/shared/custom-fields-form.tsx +14 -9
- package/src/lib/components/shared/language-selector.tsx +14 -6
- package/src/lib/components/shared/multi-select.tsx +1 -1
- package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
- package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +4 -4
- package/src/lib/components/ui/carousel.tsx +2 -2
- package/src/lib/components/ui/chart.tsx +1 -1
- package/src/lib/components/ui/context-menu.tsx +1 -1
- package/src/lib/components/ui/drawer.tsx +1 -1
- package/src/lib/components/ui/grid-layout.tsx +1 -1
- package/src/lib/components/ui/input-group.tsx +1 -0
- package/src/lib/components/ui/input-otp.tsx +1 -1
- package/src/lib/components/ui/menubar.tsx +1 -1
- package/src/lib/components/ui/navigation-menu.tsx +1 -1
- package/src/lib/components/ui/progress.tsx +1 -1
- package/src/lib/components/ui/radio-group.tsx +1 -1
- package/src/lib/components/ui/resizable.tsx +1 -1
- package/src/lib/components/ui/select.tsx +1 -1
- package/src/lib/components/ui/slider.tsx +1 -1
- package/src/lib/components/ui/toggle-group.tsx +2 -2
- package/src/lib/components/ui/toggle.tsx +1 -1
- package/src/lib/framework/component-registry/component-registry.tsx +2 -6
- package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +907 -1
- package/src/lib/framework/document-introspection/add-custom-fields.ts +248 -119
- package/src/lib/framework/extension-api/display-component-extensions.tsx +4 -3
- package/src/lib/framework/extension-api/logic/detail-forms.ts +0 -13
- package/src/lib/framework/extension-api/logic/navigation.ts +1 -1
- package/src/lib/framework/extension-api/types/data-table.ts +4 -2
- package/src/lib/framework/extension-api/types/layout.ts +34 -1
- package/src/lib/framework/extension-api/types/navigation.ts +7 -2
- package/src/lib/framework/form-engine/use-generated-form.tsx +7 -1
- package/src/lib/framework/history-entry/history-entry.tsx +1 -1
- package/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx +185 -0
- package/src/lib/framework/layout-engine/dev-mode-button.tsx +15 -13
- package/src/lib/framework/layout-engine/location-wrapper.tsx +3 -1
- package/src/lib/framework/layout-engine/page-layout.spec.tsx +138 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +294 -69
- package/src/lib/framework/nav-menu/nav-menu-extensions.ts +1 -1
- package/src/lib/framework/page/detail-page-route-loader.tsx +1 -1
- package/src/lib/framework/page/page-api.ts +1 -1
- package/src/lib/framework/page/use-detail-page.ts +4 -2
- package/src/lib/framework/page/use-extended-router.tsx +20 -16
- package/src/lib/framework/registry/registry-types.ts +2 -1
- package/src/lib/graphql/api.ts +3 -8
- package/src/lib/graphql/graphql-env.d.ts +29 -10
- package/src/lib/hooks/use-permissions.ts +3 -3
- package/src/lib/hooks/use-sorted-languages.ts +41 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/lib/load-i18n-messages.ts +4 -1
- package/src/lib/providers/channel-provider.tsx +11 -7
- package/src/lib/utils/config-utils.ts +19 -0
- package/src/lib/virtual.d.ts +3 -0
- package/LICENSE.md +0 -42
- package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +0 -129
- /package/src/{app/routes/_authenticated/_global-settings → lib}/utils/global-languages.ts +0 -0
|
@@ -567,11 +567,11 @@ export function DefaultRelationInput({
|
|
|
567
567
|
entityType,
|
|
568
568
|
}: Readonly<DefaultRelationInputProps>) {
|
|
569
569
|
const { t } = useLingui();
|
|
570
|
+
const ENTITY_CONFIGS = useMemo(() => createEntityConfigs(t), [t]);
|
|
570
571
|
if (!fieldDef || (!isRelationCustomFieldConfig(fieldDef) && !entityType)) {
|
|
571
572
|
return null;
|
|
572
573
|
}
|
|
573
574
|
const entityName = entityType ?? (fieldDef as RelationCustomFieldConfig).entity;
|
|
574
|
-
const ENTITY_CONFIGS = useMemo(() => createEntityConfigs(t), [t]);
|
|
575
575
|
const config = ENTITY_CONFIGS[entityName as keyof typeof ENTITY_CONFIGS];
|
|
576
576
|
|
|
577
577
|
if (!config) {
|
|
@@ -28,7 +28,7 @@ export interface RelationSelectorConfig<T = any> {
|
|
|
28
28
|
/** Number of items to load per page */
|
|
29
29
|
pageSize?: number;
|
|
30
30
|
/** Placeholder text for the search input */
|
|
31
|
-
placeholder?:
|
|
31
|
+
placeholder?: string;
|
|
32
32
|
/** Whether to enable multi-select mode */
|
|
33
33
|
multiple?: boolean;
|
|
34
34
|
/** Custom filter function for search */
|
|
@@ -1,13 +1,151 @@
|
|
|
1
|
-
import { X } from 'lucide-react';
|
|
2
|
-
import { KeyboardEvent, useId, useRef, useState } from 'react';
|
|
1
|
+
import { GripVertical, X } from 'lucide-react';
|
|
2
|
+
import { KeyboardEvent, useEffect, useId, useRef, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
5
5
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
6
6
|
import type { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
7
7
|
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
8
8
|
import { cn } from '@/vdb/lib/utils.js';
|
|
9
|
+
import {
|
|
10
|
+
closestCenter,
|
|
11
|
+
DndContext,
|
|
12
|
+
type DragEndEvent,
|
|
13
|
+
KeyboardSensor,
|
|
14
|
+
PointerSensor,
|
|
15
|
+
useSensor,
|
|
16
|
+
useSensors,
|
|
17
|
+
} from '@dnd-kit/core';
|
|
18
|
+
import {
|
|
19
|
+
arrayMove,
|
|
20
|
+
SortableContext,
|
|
21
|
+
sortableKeyboardCoordinates,
|
|
22
|
+
useSortable,
|
|
23
|
+
verticalListSortingStrategy,
|
|
24
|
+
} from '@dnd-kit/sortable';
|
|
25
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
9
26
|
import { useLingui } from '@lingui/react';
|
|
10
27
|
|
|
28
|
+
interface SortableItemProps {
|
|
29
|
+
id: string;
|
|
30
|
+
item: string;
|
|
31
|
+
isDisabled: boolean;
|
|
32
|
+
isEditing: boolean;
|
|
33
|
+
onRemove: () => void;
|
|
34
|
+
onEdit: () => void;
|
|
35
|
+
onSave: (newValue: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function SortableItem({ id, item, isDisabled, isEditing, onRemove, onEdit, onSave }: SortableItemProps) {
|
|
39
|
+
const [editValue, setEditValue] = useState(item);
|
|
40
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
41
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
42
|
+
id,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const style = {
|
|
46
|
+
transform: CSS.Transform.toString(transform),
|
|
47
|
+
transition,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleSave = () => {
|
|
51
|
+
const trimmedValue = editValue.trim();
|
|
52
|
+
if (trimmedValue && trimmedValue !== item) {
|
|
53
|
+
onSave(trimmedValue);
|
|
54
|
+
} else {
|
|
55
|
+
setEditValue(item);
|
|
56
|
+
onSave(item);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
61
|
+
if (e.key === 'Enter') {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
handleSave();
|
|
64
|
+
} else if (e.key === 'Escape') {
|
|
65
|
+
setEditValue(item);
|
|
66
|
+
onSave(item);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Focus and select input when entering edit mode
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (isEditing && inputRef.current) {
|
|
73
|
+
inputRef.current.focus();
|
|
74
|
+
inputRef.current.select();
|
|
75
|
+
}
|
|
76
|
+
}, [isEditing]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (item !== editValue) {
|
|
80
|
+
setEditValue(item);
|
|
81
|
+
}
|
|
82
|
+
}, [item]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Badge
|
|
86
|
+
ref={setNodeRef}
|
|
87
|
+
style={style}
|
|
88
|
+
variant="secondary"
|
|
89
|
+
className={cn(
|
|
90
|
+
isDragging && 'opacity-50',
|
|
91
|
+
'flex items-center gap-1',
|
|
92
|
+
isEditing && 'border-muted-foreground/30',
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{!isDisabled && (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
className={cn(
|
|
99
|
+
'cursor-grab active:cursor-grabbing text-muted-foreground',
|
|
100
|
+
'hover:bg-muted rounded p-0.5',
|
|
101
|
+
)}
|
|
102
|
+
{...attributes}
|
|
103
|
+
{...listeners}
|
|
104
|
+
aria-label={`Drag ${item}`}
|
|
105
|
+
>
|
|
106
|
+
<GripVertical className="h-3 w-3" />
|
|
107
|
+
</button>
|
|
108
|
+
)}
|
|
109
|
+
{isEditing ? (
|
|
110
|
+
<input
|
|
111
|
+
ref={inputRef}
|
|
112
|
+
type="text"
|
|
113
|
+
value={editValue}
|
|
114
|
+
onChange={e => setEditValue(e.target.value)}
|
|
115
|
+
onKeyDown={handleKeyDown}
|
|
116
|
+
onBlur={handleSave}
|
|
117
|
+
className="bg-transparent border-none outline-none focus:ring-0 p-0 h-auto min-w-[60px] w-auto"
|
|
118
|
+
style={{ width: `${Math.max(editValue.length * 8, 60)}px` }}
|
|
119
|
+
/>
|
|
120
|
+
) : (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={!isDisabled ? onEdit : undefined}
|
|
124
|
+
className={cn(!isDisabled && 'cursor-text hover:underline')}
|
|
125
|
+
>
|
|
126
|
+
{item}
|
|
127
|
+
</button>
|
|
128
|
+
)}
|
|
129
|
+
{!isDisabled && (
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={e => {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
onRemove();
|
|
135
|
+
}}
|
|
136
|
+
className={cn(
|
|
137
|
+
'ml-1 rounded-full outline-none ring-offset-background text-muted-foreground',
|
|
138
|
+
'hover:bg-muted focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
139
|
+
)}
|
|
140
|
+
aria-label={`Remove ${item}`}
|
|
141
|
+
>
|
|
142
|
+
<X className="h-3 w-3" />
|
|
143
|
+
</button>
|
|
144
|
+
)}
|
|
145
|
+
</Badge>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
11
149
|
export function StringListInput({
|
|
12
150
|
value,
|
|
13
151
|
onChange,
|
|
@@ -17,13 +155,21 @@ export function StringListInput({
|
|
|
17
155
|
fieldDef,
|
|
18
156
|
}: DashboardFormComponentProps) {
|
|
19
157
|
const [inputValue, setInputValue] = useState('');
|
|
158
|
+
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
20
159
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
21
160
|
const { i18n } = useLingui();
|
|
22
|
-
const isDisabled = isReadonlyField(fieldDef) || disabled;
|
|
161
|
+
const isDisabled = isReadonlyField(fieldDef) || disabled || false;
|
|
23
162
|
const id = useId();
|
|
24
163
|
|
|
25
164
|
const items = Array.isArray(value) ? value : [];
|
|
26
165
|
|
|
166
|
+
const sensors = useSensors(
|
|
167
|
+
useSensor(PointerSensor),
|
|
168
|
+
useSensor(KeyboardSensor, {
|
|
169
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
|
|
27
173
|
const addItem = (item: string) => {
|
|
28
174
|
const trimmedItem = item.trim();
|
|
29
175
|
if (trimmedItem) {
|
|
@@ -36,6 +182,24 @@ export function StringListInput({
|
|
|
36
182
|
onChange(items.filter((_, index) => index !== indexToRemove));
|
|
37
183
|
};
|
|
38
184
|
|
|
185
|
+
const editItem = (index: number, newValue: string) => {
|
|
186
|
+
const newItems = [...items];
|
|
187
|
+
newItems[index] = newValue;
|
|
188
|
+
onChange(newItems);
|
|
189
|
+
setEditingIndex(null);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
193
|
+
const { active, over } = event;
|
|
194
|
+
|
|
195
|
+
if (over && active.id !== over.id) {
|
|
196
|
+
const oldIndex = items.findIndex((_, idx) => `${id}-${idx}` === active.id);
|
|
197
|
+
const newIndex = items.findIndex((_, idx) => `${id}-${idx}` === over.id);
|
|
198
|
+
|
|
199
|
+
onChange(arrayMove(items, oldIndex, newIndex));
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
39
203
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
40
204
|
if (e.key === 'Enter' || e.key === ',') {
|
|
41
205
|
e.preventDefault();
|
|
@@ -74,29 +238,27 @@ export function StringListInput({
|
|
|
74
238
|
className="min-w-[120px]"
|
|
75
239
|
/>
|
|
76
240
|
)}
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
))}
|
|
99
|
-
</div>
|
|
241
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
242
|
+
<SortableContext
|
|
243
|
+
items={items.map((_, index) => `${id}-${index}`)}
|
|
244
|
+
strategy={verticalListSortingStrategy}
|
|
245
|
+
>
|
|
246
|
+
<div className="flex flex-wrap gap-1 items-start justify-start">
|
|
247
|
+
{items.map((item, index) => (
|
|
248
|
+
<SortableItem
|
|
249
|
+
key={`${id}-${index}`}
|
|
250
|
+
id={`${id}-${index}`}
|
|
251
|
+
item={item}
|
|
252
|
+
isDisabled={isDisabled}
|
|
253
|
+
isEditing={editingIndex === index}
|
|
254
|
+
onRemove={() => removeItem(index)}
|
|
255
|
+
onEdit={() => setEditingIndex(index)}
|
|
256
|
+
onSave={newValue => editItem(index, newValue)}
|
|
257
|
+
/>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
</SortableContext>
|
|
261
|
+
</DndContext>
|
|
100
262
|
</div>
|
|
101
263
|
);
|
|
102
264
|
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
StructCustomFieldConfig,
|
|
21
21
|
StructField,
|
|
22
22
|
} from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
23
|
-
import { isStructFieldConfig } from '@/vdb/framework/form-engine/utils.js';
|
|
23
|
+
import { isReadonlyField, isStructFieldConfig } from '@/vdb/framework/form-engine/utils.js';
|
|
24
24
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
25
25
|
import { CustomFieldListInput } from './custom-field-list-input.js';
|
|
26
26
|
import { DateTimeInput } from './datetime-input.js';
|
|
@@ -98,185 +98,61 @@ export function StructFormInput({ fieldDef, ...field }: Readonly<DashboardFormCo
|
|
|
98
98
|
return input?.find(t => t.languageCode === displayLanguage)?.value;
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
-
const isReadonly = fieldDef
|
|
102
|
-
|
|
103
|
-
// Helper function to render individual struct field inputs
|
|
104
|
-
const renderStructFieldInput = (
|
|
105
|
-
structField: StructField,
|
|
106
|
-
inputField: ControllerRenderProps<any, any>,
|
|
107
|
-
) => {
|
|
108
|
-
const isList = structField.list ?? false;
|
|
109
|
-
|
|
110
|
-
// Helper function to render single input for a struct field
|
|
111
|
-
const renderSingleStructInput = (singleField: ControllerRenderProps<any, any>) => {
|
|
112
|
-
switch (structField.type) {
|
|
113
|
-
case 'string': {
|
|
114
|
-
// Check if the field has options (dropdown)
|
|
115
|
-
const stringField = structField as any; // GraphQL union types need casting
|
|
116
|
-
if (stringField.options && stringField.options.length > 0) {
|
|
117
|
-
return (
|
|
118
|
-
<SelectWithOptions {...singleField} fieldDef={stringField} isListField={false} />
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
return (
|
|
122
|
-
<Input
|
|
123
|
-
value={singleField.value ?? ''}
|
|
124
|
-
onChange={e => singleField.onChange(e.target.value)}
|
|
125
|
-
onBlur={singleField.onBlur}
|
|
126
|
-
name={singleField.name}
|
|
127
|
-
disabled={isReadonly}
|
|
128
|
-
/>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
case 'int':
|
|
132
|
-
case 'float': {
|
|
133
|
-
const isFloat = structField.type === 'float';
|
|
134
|
-
const numericField = structField as any; // GraphQL union types need casting
|
|
135
|
-
const min = isFloat ? numericField.floatMin : numericField.intMin;
|
|
136
|
-
const max = isFloat ? numericField.floatMax : numericField.intMax;
|
|
137
|
-
const step = isFloat ? numericField.floatStep : numericField.intStep;
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<Input
|
|
141
|
-
type="number"
|
|
142
|
-
value={singleField.value ?? ''}
|
|
143
|
-
onChange={e => {
|
|
144
|
-
const value = e.target.valueAsNumber;
|
|
145
|
-
singleField.onChange(isNaN(value) ? undefined : value);
|
|
146
|
-
}}
|
|
147
|
-
onBlur={singleField.onBlur}
|
|
148
|
-
name={singleField.name}
|
|
149
|
-
disabled={isReadonly}
|
|
150
|
-
min={min}
|
|
151
|
-
max={max}
|
|
152
|
-
step={step}
|
|
153
|
-
/>
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
case 'boolean':
|
|
157
|
-
return (
|
|
158
|
-
<Switch
|
|
159
|
-
checked={singleField.value}
|
|
160
|
-
onCheckedChange={singleField.onChange}
|
|
161
|
-
disabled={isReadonly}
|
|
162
|
-
/>
|
|
163
|
-
);
|
|
164
|
-
case 'datetime':
|
|
165
|
-
return <DateTimeInput {...singleField} />;
|
|
166
|
-
default:
|
|
167
|
-
return (
|
|
168
|
-
<Input
|
|
169
|
-
value={singleField.value ?? ''}
|
|
170
|
-
onChange={e => singleField.onChange(e.target.value)}
|
|
171
|
-
onBlur={singleField.onBlur}
|
|
172
|
-
name={singleField.name}
|
|
173
|
-
disabled={isReadonly}
|
|
174
|
-
/>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// Handle string fields with options (dropdown) - already handles list case with multi-select
|
|
180
|
-
if (structField.type === 'string') {
|
|
181
|
-
const stringField = structField as any; // GraphQL union types need casting
|
|
182
|
-
if (stringField.options && stringField.options.length > 0) {
|
|
183
|
-
return (
|
|
184
|
-
<SelectWithOptions
|
|
185
|
-
{...inputField}
|
|
186
|
-
fieldDef={stringField}
|
|
187
|
-
disabled={isReadonly}
|
|
188
|
-
isListField={isList}
|
|
189
|
-
/>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// For list struct fields, wrap with list input
|
|
195
|
-
if (isList) {
|
|
196
|
-
const getDefaultValue = () => {
|
|
197
|
-
switch (structField.type) {
|
|
198
|
-
case 'string':
|
|
199
|
-
return '';
|
|
200
|
-
case 'int':
|
|
201
|
-
case 'float':
|
|
202
|
-
return 0;
|
|
203
|
-
case 'boolean':
|
|
204
|
-
return false;
|
|
205
|
-
case 'datetime':
|
|
206
|
-
return '';
|
|
207
|
-
default:
|
|
208
|
-
return '';
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// Determine if the field type needs full width
|
|
213
|
-
const needsFullWidth = structField.type === 'text' || structField.type === 'localeText';
|
|
214
|
-
|
|
215
|
-
return (
|
|
216
|
-
<CustomFieldListInput
|
|
217
|
-
{...inputField}
|
|
218
|
-
disabled={isReadonly}
|
|
219
|
-
renderInput={(index, listItemField) => renderSingleStructInput(listItemField)}
|
|
220
|
-
defaultValue={getDefaultValue()}
|
|
221
|
-
/>
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// For non-list fields, render directly
|
|
226
|
-
return renderSingleStructInput(inputField);
|
|
227
|
-
};
|
|
101
|
+
const isReadonly = isReadonlyField(fieldDef);
|
|
228
102
|
|
|
229
103
|
// Edit mode - memoized to prevent focus loss from re-renders
|
|
230
104
|
const EditMode = useMemo(
|
|
231
|
-
() =>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
<
|
|
254
|
-
<div className="flex-
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
105
|
+
() =>
|
|
106
|
+
fieldDef && isStructFieldConfig(fieldDef) ? (
|
|
107
|
+
<div className="space-y-4 border rounded-md p-4">
|
|
108
|
+
{!isReadonly && (
|
|
109
|
+
<div className="flex justify-end">
|
|
110
|
+
<Button
|
|
111
|
+
variant="ghost"
|
|
112
|
+
size="sm"
|
|
113
|
+
onClick={() => setIsEditing(false)}
|
|
114
|
+
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
|
115
|
+
>
|
|
116
|
+
<CheckIcon className="h-4 w-4" />
|
|
117
|
+
<span className="sr-only">Done</span>
|
|
118
|
+
</Button>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
{fieldDef?.fields.map(structField => (
|
|
122
|
+
<FormField
|
|
123
|
+
key={structField.name}
|
|
124
|
+
control={control}
|
|
125
|
+
name={`${field.name}.${structField.name}`}
|
|
126
|
+
render={({ field: structInputField }) => (
|
|
127
|
+
<FormItem>
|
|
128
|
+
<div className="flex items-baseline gap-4">
|
|
129
|
+
<div className="flex-1">
|
|
130
|
+
<FormLabel>
|
|
131
|
+
{getTranslation(structField.label) ?? structField.name}
|
|
132
|
+
</FormLabel>
|
|
133
|
+
{getTranslation(structField.description) && (
|
|
134
|
+
<FormDescription>
|
|
135
|
+
{getTranslation(structField.description)}
|
|
136
|
+
</FormDescription>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
<div className="flex-[2]">
|
|
140
|
+
<FormControl>
|
|
141
|
+
{renderStructFieldInput(structField, structInputField)}
|
|
142
|
+
</FormControl>
|
|
143
|
+
<FormMessage />
|
|
144
|
+
</div>
|
|
263
145
|
</div>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
</FormItem>
|
|
272
|
-
)}
|
|
273
|
-
/>
|
|
274
|
-
))}
|
|
275
|
-
</div>
|
|
276
|
-
),
|
|
277
|
-
[fieldDef, control, field.name, getTranslation, renderStructFieldInput, isReadonly],
|
|
146
|
+
</FormItem>
|
|
147
|
+
)}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
) : null,
|
|
152
|
+
[fieldDef, control, field.name, getTranslation, isReadonly],
|
|
278
153
|
);
|
|
279
154
|
|
|
155
|
+
// Early return if not a struct field config
|
|
280
156
|
if (!fieldDef || !isStructFieldConfig(fieldDef)) {
|
|
281
157
|
return null;
|
|
282
158
|
}
|
|
@@ -317,3 +193,128 @@ export function StructFormInput({ fieldDef, ...field }: Readonly<DashboardFormCo
|
|
|
317
193
|
/>
|
|
318
194
|
);
|
|
319
195
|
}
|
|
196
|
+
|
|
197
|
+
// Helper function to render individual struct field inputs
|
|
198
|
+
const renderStructFieldInput = (
|
|
199
|
+
structField: StructField,
|
|
200
|
+
inputField: ControllerRenderProps<any, any>,
|
|
201
|
+
isReadonly: boolean = false,
|
|
202
|
+
) => {
|
|
203
|
+
const isList = structField.list ?? false;
|
|
204
|
+
|
|
205
|
+
// Helper function to render single input for a struct field
|
|
206
|
+
const renderSingleStructInput = (singleField: ControllerRenderProps<any, any>) => {
|
|
207
|
+
switch (structField.type) {
|
|
208
|
+
case 'string': {
|
|
209
|
+
// Check if the field has options (dropdown)
|
|
210
|
+
const stringField = structField as any; // GraphQL union types need casting
|
|
211
|
+
if (stringField.options && stringField.options.length > 0) {
|
|
212
|
+
return <SelectWithOptions {...singleField} fieldDef={stringField} isListField={false} />;
|
|
213
|
+
}
|
|
214
|
+
return (
|
|
215
|
+
<Input
|
|
216
|
+
value={singleField.value ?? ''}
|
|
217
|
+
onChange={e => singleField.onChange(e.target.value)}
|
|
218
|
+
onBlur={singleField.onBlur}
|
|
219
|
+
name={singleField.name}
|
|
220
|
+
disabled={isReadonly}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
case 'int':
|
|
225
|
+
case 'float': {
|
|
226
|
+
const isFloat = structField.type === 'float';
|
|
227
|
+
const numericField = structField as any; // GraphQL union types need casting
|
|
228
|
+
const min = isFloat ? numericField.floatMin : numericField.intMin;
|
|
229
|
+
const max = isFloat ? numericField.floatMax : numericField.intMax;
|
|
230
|
+
const step = isFloat ? numericField.floatStep : numericField.intStep;
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<Input
|
|
234
|
+
type="number"
|
|
235
|
+
value={singleField.value ?? ''}
|
|
236
|
+
onChange={e => {
|
|
237
|
+
const value = e.target.valueAsNumber;
|
|
238
|
+
singleField.onChange(isNaN(value) ? undefined : value);
|
|
239
|
+
}}
|
|
240
|
+
onBlur={singleField.onBlur}
|
|
241
|
+
name={singleField.name}
|
|
242
|
+
disabled={isReadonly}
|
|
243
|
+
min={min}
|
|
244
|
+
max={max}
|
|
245
|
+
step={step}
|
|
246
|
+
/>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
case 'boolean':
|
|
250
|
+
return (
|
|
251
|
+
<Switch
|
|
252
|
+
checked={singleField.value}
|
|
253
|
+
onCheckedChange={singleField.onChange}
|
|
254
|
+
disabled={isReadonly}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
case 'datetime':
|
|
258
|
+
return <DateTimeInput {...singleField} />;
|
|
259
|
+
default:
|
|
260
|
+
return (
|
|
261
|
+
<Input
|
|
262
|
+
value={singleField.value ?? ''}
|
|
263
|
+
onChange={e => singleField.onChange(e.target.value)}
|
|
264
|
+
onBlur={singleField.onBlur}
|
|
265
|
+
name={singleField.name}
|
|
266
|
+
disabled={isReadonly}
|
|
267
|
+
/>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Handle string fields with options (dropdown) - already handles list case with multi-select
|
|
273
|
+
if (structField.type === 'string') {
|
|
274
|
+
const stringField = structField as any; // GraphQL union types need casting
|
|
275
|
+
if (stringField.options && stringField.options.length > 0) {
|
|
276
|
+
return (
|
|
277
|
+
<SelectWithOptions
|
|
278
|
+
{...inputField}
|
|
279
|
+
fieldDef={stringField}
|
|
280
|
+
disabled={isReadonly}
|
|
281
|
+
isListField={isList}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// For list struct fields, wrap with list input
|
|
288
|
+
if (isList) {
|
|
289
|
+
const getDefaultValue = () => {
|
|
290
|
+
switch (structField.type) {
|
|
291
|
+
case 'string':
|
|
292
|
+
return '';
|
|
293
|
+
case 'int':
|
|
294
|
+
case 'float':
|
|
295
|
+
return 0;
|
|
296
|
+
case 'boolean':
|
|
297
|
+
return false;
|
|
298
|
+
case 'datetime':
|
|
299
|
+
return '';
|
|
300
|
+
default:
|
|
301
|
+
return '';
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Determine if the field type needs full width
|
|
306
|
+
const needsFullWidth = structField.type === 'text' || structField.type === 'localeText';
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<CustomFieldListInput
|
|
310
|
+
{...inputField}
|
|
311
|
+
disabled={isReadonly}
|
|
312
|
+
renderInput={(index, listItemField) => renderSingleStructInput(listItemField)}
|
|
313
|
+
defaultValue={getDefaultValue()}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// For non-list fields, render directly
|
|
319
|
+
return renderSingleStructInput(inputField);
|
|
320
|
+
};
|
|
@@ -72,7 +72,7 @@ export function ColumnHeaderWrapper({ children, columnId }: Readonly<ColumnHeade
|
|
|
72
72
|
>
|
|
73
73
|
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
|
74
74
|
<PopoverTrigger asChild>
|
|
75
|
-
<DevModeButton className={`h-5 w-5`} />
|
|
75
|
+
<DevModeButton className={`h-5 w-5 end-1 top-1`} />
|
|
76
76
|
</PopoverTrigger>
|
|
77
77
|
<PopoverContent className="w-48 p-3">
|
|
78
78
|
<div className="space-y-2">
|