@vendure/dashboard 3.6.0-minor-202511061555 → 3.6.0-minor-202512161252

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.
Files changed (152) hide show
  1. package/dist/plugin/constants.js +2 -2
  2. package/dist/vite/constants.js +1 -0
  3. package/dist/vite/utils/compiler.d.ts +1 -0
  4. package/dist/vite/utils/compiler.js +5 -4
  5. package/dist/vite/utils/get-dashboard-paths.d.ts +5 -0
  6. package/dist/vite/utils/get-dashboard-paths.js +20 -0
  7. package/dist/vite/vite-plugin-dashboard-metadata.js +2 -1
  8. package/dist/vite/vite-plugin-tailwind-source.js +2 -15
  9. package/dist/vite/vite-plugin-translations.d.ts +10 -1
  10. package/dist/vite/vite-plugin-translations.js +156 -45
  11. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +12 -0
  12. package/dist/vite/vite-plugin-vendure-dashboard.js +1 -0
  13. package/lingui.config.js +1 -0
  14. package/package.json +7 -7
  15. package/src/app/routeTree.gen.ts +1221 -0
  16. package/src/app/routes/_authenticated/_administrators/administrators.tsx +9 -12
  17. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +9 -12
  18. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +6 -9
  19. package/src/app/routes/_authenticated/_channels/channels.tsx +9 -12
  20. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +9 -12
  21. package/src/app/routes/_authenticated/_collections/collections.tsx +9 -12
  22. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +9 -12
  23. package/src/app/routes/_authenticated/_countries/countries.tsx +9 -12
  24. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -12
  25. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +9 -12
  26. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +9 -12
  27. package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +0 -1
  28. package/src/app/routes/_authenticated/_customers/customers.tsx +9 -12
  29. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +9 -12
  30. package/src/app/routes/_authenticated/_facets/facets.tsx +9 -12
  31. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +9 -12
  32. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +9 -12
  33. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +10 -13
  34. package/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx +139 -0
  35. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +3 -0
  36. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +3 -1
  37. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +3 -3
  38. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +41 -41
  39. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
  40. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +49 -11
  41. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +4 -1
  42. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +2 -3
  43. package/src/app/routes/_authenticated/_orders/orders.tsx +3 -3
  44. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +12 -3
  45. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +27 -30
  46. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +23 -0
  47. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +9 -12
  48. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +9 -12
  49. package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +3 -3
  50. package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +2 -2
  51. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
  52. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +10 -12
  53. package/src/app/routes/_authenticated/_products/products.graphql.ts +1 -0
  54. package/src/app/routes/_authenticated/_products/products.tsx +15 -18
  55. package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -12
  56. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +9 -12
  57. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +9 -12
  58. package/src/app/routes/_authenticated/_profile/profile.tsx +3 -3
  59. package/src/app/routes/_authenticated/_promotions/promotions.tsx +9 -12
  60. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +9 -12
  61. package/src/app/routes/_authenticated/_roles/roles.tsx +9 -12
  62. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +9 -12
  63. package/src/app/routes/_authenticated/_sellers/sellers.tsx +9 -12
  64. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +9 -12
  65. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +11 -12
  66. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +19 -20
  67. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +9 -12
  68. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +9 -12
  69. package/src/app/routes/_authenticated/_system/healthchecks.tsx +2 -3
  70. package/src/app/routes/_authenticated/_system/job-queue.tsx +3 -3
  71. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +9 -12
  72. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -12
  73. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -12
  74. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +9 -12
  75. package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +49 -1
  76. package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +34 -16
  77. package/src/app/routes/_authenticated/_zones/zones.tsx +9 -12
  78. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +9 -12
  79. package/src/app/routes/_authenticated/index.tsx +5 -3
  80. package/src/i18n/locales/bg.po +3436 -0
  81. package/src/lib/components/data-input/datetime-input.tsx +1 -1
  82. package/src/lib/components/data-input/default-relation-input.tsx +1 -1
  83. package/src/lib/components/data-input/relation-selector.tsx +1 -1
  84. package/src/lib/components/data-input/string-list-input.tsx +188 -26
  85. package/src/lib/components/data-input/struct-form-input.tsx +175 -174
  86. package/src/lib/components/data-table/column-header-wrapper.tsx +1 -1
  87. package/src/lib/components/data-table/data-table-filter-badge.tsx +2 -2
  88. package/src/lib/components/data-table/data-table.tsx +1 -1
  89. package/src/lib/components/data-table/use-generated-columns.tsx +1 -1
  90. package/src/lib/components/layout/channel-switcher.tsx +6 -2
  91. package/src/lib/components/layout/content-language-selector.tsx +6 -7
  92. package/src/lib/components/layout/dev-mode-indicator.tsx +7 -3
  93. package/src/lib/components/layout/language-dialog.tsx +26 -13
  94. package/src/lib/components/layout/manage-languages-dialog.tsx +10 -29
  95. package/src/lib/components/layout/nav-item-wrapper.tsx +1 -1
  96. package/src/lib/components/shared/asset/asset-gallery.tsx +8 -3
  97. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +14 -16
  98. package/src/lib/components/shared/custom-fields-form.tsx +14 -9
  99. package/src/lib/components/shared/language-selector.tsx +14 -6
  100. package/src/lib/components/shared/multi-select.tsx +1 -1
  101. package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
  102. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +4 -4
  103. package/src/lib/components/ui/carousel.tsx +2 -2
  104. package/src/lib/components/ui/chart.tsx +1 -1
  105. package/src/lib/components/ui/context-menu.tsx +1 -1
  106. package/src/lib/components/ui/drawer.tsx +1 -1
  107. package/src/lib/components/ui/grid-layout.tsx +1 -1
  108. package/src/lib/components/ui/input-group.tsx +1 -0
  109. package/src/lib/components/ui/input-otp.tsx +1 -1
  110. package/src/lib/components/ui/menubar.tsx +1 -1
  111. package/src/lib/components/ui/navigation-menu.tsx +1 -1
  112. package/src/lib/components/ui/progress.tsx +1 -1
  113. package/src/lib/components/ui/radio-group.tsx +1 -1
  114. package/src/lib/components/ui/resizable.tsx +1 -1
  115. package/src/lib/components/ui/select.tsx +1 -1
  116. package/src/lib/components/ui/slider.tsx +1 -1
  117. package/src/lib/components/ui/toggle-group.tsx +2 -2
  118. package/src/lib/components/ui/toggle.tsx +1 -1
  119. package/src/lib/framework/component-registry/component-registry.tsx +2 -6
  120. package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +907 -1
  121. package/src/lib/framework/document-introspection/add-custom-fields.ts +248 -119
  122. package/src/lib/framework/extension-api/display-component-extensions.tsx +4 -3
  123. package/src/lib/framework/extension-api/logic/detail-forms.ts +0 -13
  124. package/src/lib/framework/extension-api/logic/navigation.ts +1 -1
  125. package/src/lib/framework/extension-api/types/data-table.ts +4 -2
  126. package/src/lib/framework/extension-api/types/layout.ts +34 -1
  127. package/src/lib/framework/extension-api/types/navigation.ts +7 -2
  128. package/src/lib/framework/form-engine/use-generated-form.tsx +7 -1
  129. package/src/lib/framework/history-entry/history-entry.tsx +1 -1
  130. package/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx +185 -0
  131. package/src/lib/framework/layout-engine/dev-mode-button.tsx +15 -13
  132. package/src/lib/framework/layout-engine/location-wrapper.tsx +3 -1
  133. package/src/lib/framework/layout-engine/page-layout.spec.tsx +138 -0
  134. package/src/lib/framework/layout-engine/page-layout.tsx +294 -69
  135. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +1 -1
  136. package/src/lib/framework/page/detail-page-route-loader.tsx +1 -1
  137. package/src/lib/framework/page/page-api.ts +1 -1
  138. package/src/lib/framework/page/use-detail-page.ts +4 -2
  139. package/src/lib/framework/page/use-extended-router.tsx +20 -16
  140. package/src/lib/framework/registry/registry-types.ts +2 -1
  141. package/src/lib/graphql/api.ts +3 -8
  142. package/src/lib/graphql/graphql-env.d.ts +29 -10
  143. package/src/lib/hooks/use-permissions.ts +3 -3
  144. package/src/lib/hooks/use-sorted-languages.ts +41 -0
  145. package/src/lib/index.ts +1 -0
  146. package/src/lib/lib/load-i18n-messages.ts +4 -1
  147. package/src/lib/providers/channel-provider.tsx +11 -7
  148. package/src/lib/utils/config-utils.ts +19 -0
  149. package/src/lib/virtual.d.ts +3 -0
  150. package/LICENSE.md +0 -42
  151. package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +0 -129
  152. /package/src/{app/routes/_authenticated/_global-settings → lib}/utils/global-languages.ts +0 -0
@@ -185,7 +185,7 @@ function bcpTagToDatePickerLocale(
185
185
  case 'pt-BR':
186
186
  return module.ptBR;
187
187
  default: {
188
- const lang = tag.split('-').at(0);
188
+ const lang = tag.split('-')[0];
189
189
  return lang ? module[lang as keyof typeof module] : undefined;
190
190
  }
191
191
  }
@@ -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?: React.ReactNode;
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
- <div className="flex flex-wrap gap-1 items-start justify-start">
78
- {items.map((item, index) => (
79
- <Badge key={id + index} variant="secondary">
80
- <span>{item}</span>
81
- {!isDisabled && (
82
- <button
83
- type="button"
84
- onClick={e => {
85
- e.stopPropagation();
86
- removeItem(index);
87
- }}
88
- className={cn(
89
- 'ml-1 rounded-full outline-none ring-offset-background',
90
- 'hover:bg-muted focus:ring-2 focus:ring-ring focus:ring-offset-2',
91
- )}
92
- aria-label={`Remove ${item}`}
93
- >
94
- <X className="h-3 w-3" />
95
- </button>
96
- )}
97
- </Badge>
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?.readonly === true;
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
- <div className="space-y-4 border rounded-md p-4">
233
- {!isReadonly && (
234
- <div className="flex justify-end">
235
- <Button
236
- variant="ghost"
237
- size="sm"
238
- onClick={() => setIsEditing(false)}
239
- className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
240
- >
241
- <CheckIcon className="h-4 w-4" />
242
- <span className="sr-only">Done</span>
243
- </Button>
244
- </div>
245
- )}
246
- {fieldDef?.fields?.map(structField => (
247
- <FormField
248
- key={structField.name}
249
- control={control}
250
- name={`${field.name}.${structField.name}`}
251
- render={({ field: structInputField }) => (
252
- <FormItem>
253
- <div className="flex items-baseline gap-4">
254
- <div className="flex-1">
255
- <FormLabel>
256
- {getTranslation(structField.label) ?? structField.name}
257
- </FormLabel>
258
- {getTranslation(structField.description) && (
259
- <FormDescription>
260
- {getTranslation(structField.description)}
261
- </FormDescription>
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
- <div className="flex-[2]">
265
- <FormControl>
266
- {renderStructFieldInput(structField, structInputField)}
267
- </FormControl>
268
- <FormMessage />
269
- </div>
270
- </div>
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">