@vendure/dashboard 3.3.6-master-202507040234 → 3.3.6-master-202507050232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/app/common/delete-bulk-action.tsx +2 -1
- package/src/app/common/duplicate-bulk-action.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +4 -1
- package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +2 -1
- package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx +2 -1
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +3 -1
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +3 -1
- package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +1 -1
- package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +2 -1
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx +2 -1
- package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx +2 -1
- package/src/lib/components/data-input/relation-selector.tsx +144 -26
- package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +2 -1
- package/src/lib/components/shared/copyable-text.tsx +1 -1
- package/src/lib/components/shared/form-field-wrapper.tsx +26 -12
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -1
- package/src/lib/components/shared/translatable-form-field.tsx +26 -12
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +2 -1
- package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +2 -1
- package/src/lib/framework/form-engine/overridden-form-component.tsx +51 -0
- package/src/lib/framework/layout-engine/location-wrapper.tsx +99 -69
- package/src/lib/framework/layout-engine/page-layout.tsx +8 -8
- package/src/lib/framework/page/detail-page.tsx +3 -31
- package/src/lib/hooks/use-page-block.tsx +10 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.3.6-master-
|
|
4
|
+
"version": "3.3.6-master-202507050232",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"@types/react-dom": "^19.0.4",
|
|
87
87
|
"@types/react-grid-layout": "^1.3.5",
|
|
88
88
|
"@uidotdev/usehooks": "^2.4.1",
|
|
89
|
-
"@vendure/common": "^3.3.6-master-
|
|
90
|
-
"@vendure/core": "^3.3.6-master-
|
|
89
|
+
"@vendure/common": "^3.3.6-master-202507050232",
|
|
90
|
+
"@vendure/core": "^3.3.6-master-202507050232",
|
|
91
91
|
"@vitejs/plugin-react": "^4.3.4",
|
|
92
92
|
"awesome-graphql-client": "^2.1.0",
|
|
93
93
|
"class-variance-authority": "^0.7.1",
|
|
@@ -130,5 +130,5 @@
|
|
|
130
130
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
131
131
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
132
132
|
},
|
|
133
|
-
"gitHead": "
|
|
133
|
+
"gitHead": "aa108b47a462b2ff023250e4f9e75a02529feb8a"
|
|
134
134
|
}
|
|
@@ -3,8 +3,9 @@ import { TrashIcon } from 'lucide-react';
|
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
4
|
|
|
5
5
|
import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
|
|
6
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
7
|
+
import { getMutationName } from '@/vdb/framework/document-introspection/get-document-structure.js';
|
|
6
8
|
import { api } from '@/vdb/graphql/api.js';
|
|
7
|
-
import { getMutationName, usePaginatedList } from '@/vdb/index.js';
|
|
8
9
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
9
10
|
|
|
10
11
|
interface DeleteBulkActionProps {
|
|
@@ -4,9 +4,9 @@ import { useState } from 'react';
|
|
|
4
4
|
import { toast } from 'sonner';
|
|
5
5
|
|
|
6
6
|
import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
|
|
7
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
7
8
|
import { api } from '@/vdb/graphql/api.js';
|
|
8
9
|
import { duplicateEntityDocument } from '@/vdb/graphql/common-operations.js';
|
|
9
|
-
import { usePaginatedList } from '@/vdb/index.js';
|
|
10
10
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
11
11
|
|
|
12
12
|
interface DuplicateBulkActionProps {
|
|
@@ -6,7 +6,10 @@ import { Trans } from '@/vdb/lib/trans.js';
|
|
|
6
6
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
7
7
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
8
8
|
import { api } from '@/vdb/graphql/api.js';
|
|
9
|
-
import { BulkActionComponent
|
|
9
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
10
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
11
|
+
import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
|
|
12
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
10
13
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
11
14
|
import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
|
|
12
15
|
import {
|
|
@@ -4,7 +4,8 @@ import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-cha
|
|
|
4
4
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
5
5
|
import { api } from '@/vdb/graphql/api.js';
|
|
6
6
|
import { ResultOf } from '@/vdb/graphql/graphql.js';
|
|
7
|
-
import { BulkActionComponent
|
|
7
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
8
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
8
9
|
import { useLingui } from '@/vdb/lib/trans.js';
|
|
9
10
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
10
11
|
import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
|
package/src/app/routes/_authenticated/_payment-methods/components/payment-method-bulk-actions.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
2
2
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
3
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
3
4
|
import { api } from '@/vdb/graphql/api.js';
|
|
4
|
-
import {
|
|
5
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
5
6
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
6
7
|
|
|
7
8
|
import {
|
package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx
CHANGED
|
@@ -6,7 +6,9 @@ import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-cha
|
|
|
6
6
|
import { usePriceFactor } from '@/vdb/components/shared/assign-to-channel-dialog.js';
|
|
7
7
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
8
8
|
import { api } from '@/vdb/graphql/api.js';
|
|
9
|
-
import { BulkActionComponent
|
|
9
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
10
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
11
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
10
12
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
11
13
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
12
14
|
|
|
@@ -4,9 +4,11 @@ import { useState } from 'react';
|
|
|
4
4
|
import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
|
|
5
5
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
6
6
|
import { usePriceFactor } from '@/vdb/components/shared/assign-to-channel-dialog.js';
|
|
7
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
7
8
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
9
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
8
10
|
import { api } from '@/vdb/graphql/api.js';
|
|
9
|
-
import {
|
|
11
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
10
12
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
11
13
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
12
14
|
import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Money } from '@/vdb/components/data-display/money.js';
|
|
2
|
+
import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
|
|
2
3
|
import {
|
|
3
4
|
PaginatedListDataTable,
|
|
4
5
|
PaginatedListRefresherRegisterFn,
|
|
@@ -6,7 +7,6 @@ import {
|
|
|
6
7
|
import { StockLevelLabel } from '@/vdb/components/shared/stock-level-label.js';
|
|
7
8
|
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
8
9
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
9
|
-
import { DetailPageButton } from '@/vdb/index.js';
|
|
10
10
|
import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
|
|
11
11
|
import { useState } from 'react';
|
|
12
12
|
import { productVariantListDocument } from '../products.graphql.js';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
2
2
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
3
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
3
4
|
import { api } from '@/vdb/graphql/api.js';
|
|
4
|
-
import {
|
|
5
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
5
6
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
6
7
|
import { DuplicateBulkAction } from '../../../../common/duplicate-bulk-action.js';
|
|
7
8
|
|
package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-bulk-actions.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
2
2
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
3
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
3
4
|
import { api } from '@/vdb/graphql/api.js';
|
|
4
|
-
import {
|
|
5
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
5
6
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
6
7
|
|
|
7
8
|
import {
|
package/src/app/routes/_authenticated/_stock-locations/components/stock-location-bulk-actions.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
|
|
2
2
|
import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
|
|
3
3
|
import { api } from '@/vdb/graphql/api.js';
|
|
4
|
-
import { BulkActionComponent
|
|
4
|
+
import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
|
|
5
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
5
6
|
import { DeleteBulkAction } from '../../../../common/delete-bulk-action.js';
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -32,6 +32,8 @@ export interface RelationSelectorConfig<T = any> {
|
|
|
32
32
|
multiple?: boolean;
|
|
33
33
|
/** Custom filter function for search */
|
|
34
34
|
buildSearchFilter?: (searchTerm: string) => any;
|
|
35
|
+
/** Custom filter function for fetching by IDs */
|
|
36
|
+
buildIdsFilter?: (ids: string[]) => any;
|
|
35
37
|
/** Custom label renderer function for rich display */
|
|
36
38
|
label?: (item: T) => React.ReactNode;
|
|
37
39
|
}
|
|
@@ -96,6 +98,19 @@ export function useRelationSelector<T>(config: RelationSelectorConfig<T>) {
|
|
|
96
98
|
[config.labelKey]: { contains: term },
|
|
97
99
|
}));
|
|
98
100
|
|
|
101
|
+
// Build the default IDs filter if none provided
|
|
102
|
+
const buildIdsFilter = React.useCallback(
|
|
103
|
+
(ids: string[]) => {
|
|
104
|
+
if (config.buildIdsFilter) {
|
|
105
|
+
return config.buildIdsFilter(ids);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
[config.idKey]: { in: ids },
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
[config.idKey, config.buildIdsFilter],
|
|
112
|
+
);
|
|
113
|
+
|
|
99
114
|
const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, error } = useInfiniteQuery({
|
|
100
115
|
queryKey: ['relationSelector', getQueryName(config.listQuery), debouncedSearch],
|
|
101
116
|
queryFn: async ({ pageParam = 0 }) => {
|
|
@@ -125,6 +140,30 @@ export function useRelationSelector<T>(config: RelationSelectorConfig<T>) {
|
|
|
125
140
|
|
|
126
141
|
const items = data?.pages.flatMap(page => page?.items ?? []) ?? [];
|
|
127
142
|
|
|
143
|
+
// Function to fetch items by IDs
|
|
144
|
+
const fetchItemsByIds = React.useCallback(
|
|
145
|
+
async (ids: string[]): Promise<T[]> => {
|
|
146
|
+
if (ids.length === 0) return [];
|
|
147
|
+
|
|
148
|
+
const variables: any = {
|
|
149
|
+
options: {
|
|
150
|
+
take: ids.length,
|
|
151
|
+
filter: buildIdsFilter(ids),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const response = (await api.query(config.listQuery, variables)) as any;
|
|
157
|
+
const result = response[getQueryName(config.listQuery)];
|
|
158
|
+
return result?.items ?? [];
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error fetching items by IDs:', error);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
[buildIdsFilter, config.listQuery],
|
|
165
|
+
);
|
|
166
|
+
|
|
128
167
|
return {
|
|
129
168
|
items,
|
|
130
169
|
isLoading,
|
|
@@ -134,6 +173,7 @@ export function useRelationSelector<T>(config: RelationSelectorConfig<T>) {
|
|
|
134
173
|
error,
|
|
135
174
|
searchTerm,
|
|
136
175
|
setSearchTerm,
|
|
176
|
+
fetchItemsByIds,
|
|
137
177
|
};
|
|
138
178
|
}
|
|
139
179
|
|
|
@@ -149,19 +189,104 @@ export function RelationSelector<T>({
|
|
|
149
189
|
}: Readonly<RelationSelectorProps<T>>) {
|
|
150
190
|
const [open, setOpen] = useState(false);
|
|
151
191
|
const [selectedItemsCache, setSelectedItemsCache] = useState<T[]>([]);
|
|
192
|
+
const fetchedIdsRef = React.useRef<Set<string>>(new Set());
|
|
193
|
+
const fetchingIdsRef = React.useRef<Set<string>>(new Set());
|
|
152
194
|
const isMultiple = config.multiple ?? false;
|
|
153
195
|
|
|
154
|
-
const {
|
|
155
|
-
|
|
196
|
+
const {
|
|
197
|
+
items,
|
|
198
|
+
isLoading,
|
|
199
|
+
fetchNextPage,
|
|
200
|
+
hasNextPage,
|
|
201
|
+
isFetchingNextPage,
|
|
202
|
+
searchTerm,
|
|
203
|
+
setSearchTerm,
|
|
204
|
+
fetchItemsByIds,
|
|
205
|
+
} = useRelationSelector(config);
|
|
206
|
+
|
|
207
|
+
// Store a stable reference to fetchItemsByIds
|
|
208
|
+
const fetchItemsByIdsRef = React.useRef(fetchItemsByIds);
|
|
209
|
+
fetchItemsByIdsRef.current = fetchItemsByIds;
|
|
156
210
|
|
|
157
211
|
// Normalize value to always be an array for easier handling
|
|
158
212
|
const selectedIds = React.useMemo(() => {
|
|
159
213
|
if (isMultiple) {
|
|
160
214
|
return Array.isArray(value) ? value : value ? [value] : [];
|
|
161
215
|
}
|
|
162
|
-
|
|
216
|
+
// For single select, ensure we only have at most one ID
|
|
217
|
+
const singleValue = Array.isArray(value) ? value[0] : value;
|
|
218
|
+
return singleValue ? [String(singleValue)] : [];
|
|
163
219
|
}, [value, isMultiple]);
|
|
164
220
|
|
|
221
|
+
// Fetch selected items by IDs on mount and when selectedIds change
|
|
222
|
+
React.useEffect(() => {
|
|
223
|
+
const fetchSelectedItems = async () => {
|
|
224
|
+
if (selectedIds.length === 0) {
|
|
225
|
+
setSelectedItemsCache([]);
|
|
226
|
+
fetchedIdsRef.current.clear();
|
|
227
|
+
fetchingIdsRef.current.clear();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Find which selected IDs we haven't fetched yet and aren't currently fetching
|
|
232
|
+
const missingIds = selectedIds.filter(
|
|
233
|
+
id => !fetchedIdsRef.current.has(id) && !fetchingIdsRef.current.has(id),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (missingIds.length > 0) {
|
|
237
|
+
// Mark these IDs as being fetched
|
|
238
|
+
missingIds.forEach(id => fetchingIdsRef.current.add(id));
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const fetchedItems = await fetchItemsByIdsRef.current(missingIds);
|
|
242
|
+
|
|
243
|
+
// Mark these IDs as fetched and remove from fetching
|
|
244
|
+
missingIds.forEach(id => {
|
|
245
|
+
fetchedIdsRef.current.add(id);
|
|
246
|
+
fetchingIdsRef.current.delete(id);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
setSelectedItemsCache(prev => {
|
|
250
|
+
if (!isMultiple) {
|
|
251
|
+
// For single select, replace the entire cache
|
|
252
|
+
return fetchedItems;
|
|
253
|
+
}
|
|
254
|
+
// For multi-select, filter and append
|
|
255
|
+
const stillSelected = prev.filter(item =>
|
|
256
|
+
selectedIds.includes(String(item[config.idKey])),
|
|
257
|
+
);
|
|
258
|
+
return [...stillSelected, ...fetchedItems];
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
// Remove from fetching set on error
|
|
262
|
+
missingIds.forEach(id => fetchingIdsRef.current.delete(id));
|
|
263
|
+
console.error('Error fetching items by IDs:', error);
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
// Just filter out items that are no longer selected
|
|
267
|
+
setSelectedItemsCache(prev => {
|
|
268
|
+
if (!isMultiple) {
|
|
269
|
+
// For single select, if no items need fetching but we have a selection,
|
|
270
|
+
// keep only items that are in the current selection
|
|
271
|
+
return prev.filter(item => selectedIds.includes(String(item[config.idKey])));
|
|
272
|
+
}
|
|
273
|
+
// For multi-select, normal filtering
|
|
274
|
+
return prev.filter(item => selectedIds.includes(String(item[config.idKey])));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Clean up fetched IDs that are no longer selected
|
|
279
|
+
const selectedIdsSet = new Set(selectedIds);
|
|
280
|
+
for (const fetchedId of fetchedIdsRef.current) {
|
|
281
|
+
if (!selectedIdsSet.has(fetchedId)) {
|
|
282
|
+
fetchedIdsRef.current.delete(fetchedId);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
fetchSelectedItems();
|
|
288
|
+
}, [selectedIds, config.idKey, isMultiple]);
|
|
289
|
+
|
|
165
290
|
const handleSelect = (item: T) => {
|
|
166
291
|
const itemId = String(item[config.idKey]);
|
|
167
292
|
|
|
@@ -182,10 +307,20 @@ export function RelationSelector<T>({
|
|
|
182
307
|
}
|
|
183
308
|
});
|
|
184
309
|
|
|
310
|
+
// Mark the item as fetched to prevent duplicate fetching
|
|
311
|
+
if (!isCurrentlySelected) {
|
|
312
|
+
fetchedIdsRef.current.add(itemId);
|
|
313
|
+
} else {
|
|
314
|
+
fetchedIdsRef.current.delete(itemId);
|
|
315
|
+
}
|
|
316
|
+
|
|
185
317
|
onChange(newSelectedIds);
|
|
186
318
|
} else {
|
|
187
319
|
// For single select, update cache with the new item
|
|
188
320
|
setSelectedItemsCache([item]);
|
|
321
|
+
// Mark as fetched for single select too
|
|
322
|
+
fetchedIdsRef.current.clear();
|
|
323
|
+
fetchedIdsRef.current.add(itemId);
|
|
189
324
|
onChange(itemId);
|
|
190
325
|
setOpen(false);
|
|
191
326
|
setSearchTerm('');
|
|
@@ -214,31 +349,14 @@ export function RelationSelector<T>({
|
|
|
214
349
|
}
|
|
215
350
|
};
|
|
216
351
|
|
|
217
|
-
// Clean up cache when selectedIds change externally (e.g., form reset)
|
|
218
|
-
React.useEffect(() => {
|
|
219
|
-
setSelectedItemsCache(prev => prev.filter(item => selectedIds.includes(String(item[config.idKey]))));
|
|
220
|
-
}, [selectedIds, config.idKey]);
|
|
221
|
-
|
|
222
|
-
// Populate cache with items from search results that are selected but not yet cached
|
|
223
|
-
React.useEffect(() => {
|
|
224
|
-
const itemsToAdd = items.filter(item => {
|
|
225
|
-
const itemId = String(item[config.idKey]);
|
|
226
|
-
const isSelected = selectedIds.includes(itemId);
|
|
227
|
-
const isAlreadyCached = selectedItemsCache.some(
|
|
228
|
-
cachedItem => String(cachedItem[config.idKey]) === itemId,
|
|
229
|
-
);
|
|
230
|
-
return isSelected && !isAlreadyCached;
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
if (itemsToAdd.length > 0) {
|
|
234
|
-
setSelectedItemsCache(prev => [...prev, ...itemsToAdd]);
|
|
235
|
-
}
|
|
236
|
-
}, [items, selectedIds, selectedItemsCache, config.idKey]);
|
|
237
|
-
|
|
238
352
|
// Get selected items for display from cache, filtered by current selection
|
|
239
353
|
const selectedItems = React.useMemo(() => {
|
|
240
|
-
|
|
241
|
-
|
|
354
|
+
const filteredItems = selectedItemsCache.filter(item =>
|
|
355
|
+
selectedIds.includes(String(item[config.idKey])),
|
|
356
|
+
);
|
|
357
|
+
// For single select, ensure we only display one item
|
|
358
|
+
return isMultiple ? filteredItems : filteredItems.slice(0, 1);
|
|
359
|
+
}, [selectedItemsCache, selectedIds, config.idKey, isMultiple]);
|
|
242
360
|
|
|
243
361
|
return (
|
|
244
362
|
<div className={className}>
|
|
@@ -3,7 +3,8 @@ import { useState } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
|
|
5
5
|
import { AssignToChannelDialog } from '@/vdb/components/shared/assign-to-channel-dialog.js';
|
|
6
|
-
import {
|
|
6
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
7
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
7
8
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
8
9
|
|
|
9
10
|
interface AssignToChannelBulkActionProps {
|
|
@@ -14,7 +14,7 @@ export function CopyableText({ text }: Readonly<{ text: string }>) {
|
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<div className="flex items-center gap-2">
|
|
17
|
-
<div className="font-mono
|
|
17
|
+
<div className="font-mono">{text}</div>
|
|
18
18
|
<button
|
|
19
19
|
onClick={() => handleCopy(text, 'page')}
|
|
20
20
|
className="p-1 hover:bg-muted rounded-md transition-colors"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { OverriddenFormComponent } from '@/vdb/framework/form-engine/overridden-form-component.js';
|
|
2
|
+
import { LocationWrapper } from '@/vdb/framework/layout-engine/location-wrapper.js';
|
|
1
3
|
import { FieldPath, FieldValues } from 'react-hook-form';
|
|
2
4
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../ui/form.js';
|
|
3
5
|
|
|
@@ -30,17 +32,29 @@ export function FormFieldWrapper<
|
|
|
30
32
|
renderFormControl = true,
|
|
31
33
|
}: FormFieldWrapperProps<TFieldValues, TName>) {
|
|
32
34
|
return (
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
<LocationWrapper identifier={name}>
|
|
36
|
+
<FormField
|
|
37
|
+
control={control}
|
|
38
|
+
name={name}
|
|
39
|
+
render={renderArgs => (
|
|
40
|
+
<FormItem>
|
|
41
|
+
{label && <FormLabel>{label}</FormLabel>}
|
|
42
|
+
{renderFormControl ? (
|
|
43
|
+
<FormControl>
|
|
44
|
+
<OverriddenFormComponent field={renderArgs.field} fieldName={name}>
|
|
45
|
+
{render(renderArgs)}
|
|
46
|
+
</OverriddenFormComponent>
|
|
47
|
+
</FormControl>
|
|
48
|
+
) : (
|
|
49
|
+
<OverriddenFormComponent field={renderArgs.field} fieldName={name}>
|
|
50
|
+
{render(renderArgs)}
|
|
51
|
+
</OverriddenFormComponent>
|
|
52
|
+
)}
|
|
53
|
+
{description && <FormDescription>{description}</FormDescription>}
|
|
54
|
+
<FormMessage />
|
|
55
|
+
</FormItem>
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
</LocationWrapper>
|
|
45
59
|
);
|
|
46
60
|
}
|
|
@@ -3,8 +3,9 @@ import { LayersIcon } from 'lucide-react';
|
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
4
|
|
|
5
5
|
import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
|
|
6
|
+
import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
6
7
|
import { ResultOf } from '@/vdb/graphql/graphql.js';
|
|
7
|
-
import { useChannel
|
|
8
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
8
9
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
9
10
|
|
|
10
11
|
interface RemoveFromChannelBulkActionProps {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { OverriddenFormComponent } from '@/vdb/framework/form-engine/overridden-form-component.js';
|
|
2
|
+
import { LocationWrapper } from '@/vdb/framework/layout-engine/location-wrapper.js';
|
|
1
3
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
2
4
|
import { Controller, ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
|
|
3
5
|
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '../ui/form.js';
|
|
@@ -53,17 +55,29 @@ export const TranslatableFormFieldWrapper = <
|
|
|
53
55
|
...props
|
|
54
56
|
}: TranslatableFormFieldWrapperProps<TFieldValues>) => {
|
|
55
57
|
return (
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
<LocationWrapper identifier={name as string}>
|
|
59
|
+
<TranslatableFormField
|
|
60
|
+
control={props.control}
|
|
61
|
+
name={name}
|
|
62
|
+
render={renderArgs => (
|
|
63
|
+
<FormItem>
|
|
64
|
+
{label && <FormLabel>{label}</FormLabel>}
|
|
65
|
+
{renderFormControl ? (
|
|
66
|
+
<FormControl>
|
|
67
|
+
<OverriddenFormComponent field={renderArgs.field} fieldName={name as string}>
|
|
68
|
+
{render(renderArgs)}
|
|
69
|
+
</OverriddenFormComponent>
|
|
70
|
+
</FormControl>
|
|
71
|
+
) : (
|
|
72
|
+
<OverriddenFormComponent field={renderArgs.field} fieldName={name as string}>
|
|
73
|
+
{render(renderArgs)}
|
|
74
|
+
</OverriddenFormComponent>
|
|
75
|
+
)}
|
|
76
|
+
{description && <FormDescription>{description}</FormDescription>}
|
|
77
|
+
<FormMessage />
|
|
78
|
+
</FormItem>
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
</LocationWrapper>
|
|
68
82
|
);
|
|
69
83
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { PaginatedListDataTable } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
1
2
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
2
|
-
import {
|
|
3
|
+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
3
4
|
import { Link } from '@tanstack/react-router';
|
|
4
5
|
import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
|
|
5
6
|
import { formatRelative } from 'date-fns';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AnimatedCurrency, AnimatedNumber } from '@/vdb/components/shared/animated-number.js';
|
|
2
2
|
import { Tabs, TabsList, TabsTrigger } from '@/vdb/components/ui/tabs.js';
|
|
3
3
|
import { api } from '@/vdb/graphql/api.js';
|
|
4
|
-
import { useChannel
|
|
4
|
+
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
5
|
+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
5
6
|
import { useQuery } from '@tanstack/react-query';
|
|
6
7
|
import { endOfDay, endOfMonth, startOfDay, startOfMonth, subDays, subMonths } from 'date-fns';
|
|
7
8
|
import { useMemo, useState } from 'react';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DataDisplayComponent,
|
|
3
|
+
DataInputComponent,
|
|
4
|
+
useComponentRegistry,
|
|
5
|
+
} from '@/vdb/framework/component-registry/component-registry.js';
|
|
6
|
+
import { generateInputComponentKey } from '@/vdb/framework/extension-api/input-component-extensions.js';
|
|
7
|
+
import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
|
|
8
|
+
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
9
|
+
import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
|
|
10
|
+
|
|
11
|
+
export interface OverriddenFormComponent<
|
|
12
|
+
TFieldValues extends FieldValues = any,
|
|
13
|
+
TName extends FieldPath<TFieldValues> = any,
|
|
14
|
+
> {
|
|
15
|
+
fieldName: string;
|
|
16
|
+
field: ControllerRenderProps<TFieldValues, TName>;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @description
|
|
22
|
+
* Based on the pageId and blockId of where this is placed, it will check whether any custom components
|
|
23
|
+
* are registered and render them if so. Otherwise, it will render the children, which act as the
|
|
24
|
+
* default if this location has not been overridden.
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <OverriddenFormComponent fieldName="myField" field={field}>
|
|
28
|
+
* <Input {...field} />
|
|
29
|
+
* </OverriddenFormComponent>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function OverriddenFormComponent({ fieldName, field, children }: Readonly<OverriddenFormComponent>) {
|
|
33
|
+
const page = usePage();
|
|
34
|
+
const pageBlock = usePageBlock({ optional: true });
|
|
35
|
+
const componentRegistry = useComponentRegistry();
|
|
36
|
+
let DisplayComponent: DataDisplayComponent | undefined;
|
|
37
|
+
let InputComponent: DataInputComponent | undefined;
|
|
38
|
+
if (page.pageId && pageBlock?.blockId) {
|
|
39
|
+
const customInputComponentKey = generateInputComponentKey(page.pageId, pageBlock.blockId, fieldName);
|
|
40
|
+
DisplayComponent = componentRegistry.getDisplayComponent(customInputComponentKey);
|
|
41
|
+
InputComponent = componentRegistry.getInputComponent(customInputComponentKey);
|
|
42
|
+
}
|
|
43
|
+
if (DisplayComponent) {
|
|
44
|
+
return <DisplayComponent {...field} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (InputComponent) {
|
|
48
|
+
return <InputComponent {...field} />;
|
|
49
|
+
}
|
|
50
|
+
return children ?? null;
|
|
51
|
+
}
|
|
@@ -1,98 +1,128 @@
|
|
|
1
1
|
import { CopyableText } from '@/vdb/components/shared/copyable-text.js';
|
|
2
2
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
3
|
import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
|
|
4
|
+
import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
|
|
4
5
|
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
5
6
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
6
|
-
import { Trans } from '@/vdb/lib/trans.js';
|
|
7
7
|
import { cn } from '@/vdb/lib/utils.js';
|
|
8
|
-
import { CodeXmlIcon
|
|
9
|
-
import {
|
|
8
|
+
import { CodeXmlIcon } from 'lucide-react';
|
|
9
|
+
import React, { useEffect, useState } from 'react';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
setHoveredId: ((id: string | null) => void) | null;
|
|
15
|
-
}>({
|
|
16
|
-
parentId: null,
|
|
17
|
-
hoveredId: null,
|
|
18
|
-
setHoveredId: null,
|
|
19
|
-
});
|
|
11
|
+
// Singleton state for hover tracking
|
|
12
|
+
let globalHoveredId: string | null = null;
|
|
13
|
+
const hoverListeners: Set<(id: string | null) => void> = new Set();
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
15
|
+
const setGlobalHoveredId = (id: string | null) => {
|
|
16
|
+
globalHoveredId = id;
|
|
17
|
+
hoverListeners.forEach(listener => listener(id));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface LocationWrapperProps {
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
identifier?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function LocationWrapper({ children, identifier }: Readonly<LocationWrapperProps>) {
|
|
25
26
|
const page = usePage();
|
|
27
|
+
const pageBlock = usePageBlock({ optional: true });
|
|
26
28
|
const { settings } = useUserSettings();
|
|
27
29
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
30
|
+
const blockId = pageBlock?.blockId ?? null;
|
|
28
31
|
const isPageWrapper = !blockId;
|
|
29
32
|
|
|
30
|
-
const [
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
33
|
+
const [hoveredId, setHoveredId] = useState<string | null>(globalHoveredId);
|
|
34
|
+
const id = `${page.pageId}-${blockId ?? 'page'}-${identifier ?? ''}`;
|
|
35
|
+
const isHovered = hoveredId === id;
|
|
36
|
+
|
|
37
|
+
// Subscribe to global hover changes
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const listener = (newHoveredId: string | null) => {
|
|
40
|
+
setHoveredId(newHoveredId);
|
|
41
|
+
};
|
|
42
|
+
hoverListeners.add(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
hoverListeners.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
34
47
|
|
|
35
48
|
const setHoverId = (id: string | null) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
setGlobalHoveredId(id);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleMouseEnter = () => {
|
|
53
|
+
// Set this element as hovered
|
|
54
|
+
setHoverId(id);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleMouseLeave = () => {
|
|
58
|
+
// If we're at the top level (page wrapper), go to null
|
|
59
|
+
// If we're at block level, go to page level
|
|
60
|
+
// If we're at identifier level, go to block level
|
|
61
|
+
if (isPageWrapper) {
|
|
62
|
+
setHoverId(null);
|
|
63
|
+
} else if (blockId && !identifier) {
|
|
64
|
+
// Block level - go to page level
|
|
65
|
+
setHoverId(`${page.pageId}-page-`);
|
|
66
|
+
} else if (identifier) {
|
|
67
|
+
// Identifier level - go to block level
|
|
68
|
+
setHoverId(`${page.pageId}-${blockId}-`);
|
|
40
69
|
}
|
|
41
70
|
};
|
|
42
71
|
|
|
43
72
|
if (settings.devMode) {
|
|
44
73
|
const pageId = page.pageId;
|
|
45
74
|
return (
|
|
46
|
-
<
|
|
47
|
-
|
|
75
|
+
<div
|
|
76
|
+
className={cn(
|
|
77
|
+
`ring-2 transition-all delay-50 relative`,
|
|
78
|
+
isHovered || isPopoverOpen ? 'ring-dev-mode' : 'ring-transparent',
|
|
79
|
+
isPageWrapper ? 'ring-inset' : '',
|
|
80
|
+
identifier ? 'rounded-md' : 'rounded-xl',
|
|
81
|
+
)}
|
|
82
|
+
onMouseEnter={handleMouseEnter}
|
|
83
|
+
onMouseLeave={handleMouseLeave}
|
|
48
84
|
>
|
|
49
85
|
<div
|
|
50
|
-
className={
|
|
51
|
-
`ring-2 rounded-xl transition-all delay-50 relative`,
|
|
52
|
-
isHovered || isPopoverOpen ? 'ring-dev-mode' : 'ring-transparent',
|
|
53
|
-
isPageWrapper ? 'ring-inset' : '',
|
|
54
|
-
)}
|
|
55
|
-
onMouseEnter={() => setHoverId(id)}
|
|
56
|
-
onMouseLeave={() => setHoverId(parentId)}
|
|
86
|
+
className={`absolute top-1 right-1 transition-all delay-50 z-10 ${isHovered || isPopoverOpen ? 'visible' : 'invisible'}`}
|
|
57
87
|
>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
|
89
|
+
<PopoverTrigger asChild>
|
|
90
|
+
<Button
|
|
91
|
+
variant="secondary"
|
|
92
|
+
size="icon"
|
|
93
|
+
className="h-8 w-8 rounded-full bg-dev-mode/10 hover:bg-dev-mode/20 border border-dev-mode/20 shadow-sm"
|
|
94
|
+
>
|
|
95
|
+
<CodeXmlIcon className="text-dev-mode w-4 h-4" />
|
|
96
|
+
</Button>
|
|
97
|
+
</PopoverTrigger>
|
|
98
|
+
<PopoverContent className="w-48 p-3">
|
|
99
|
+
<div className="space-y-2">
|
|
100
|
+
<div className="space-y-1">
|
|
101
|
+
{pageId && (
|
|
102
|
+
<div className="text-xs">
|
|
103
|
+
<div className="text-muted-foreground mb-0.5">pageId</div>
|
|
104
|
+
<CopyableText text={pageId} />
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
{blockId && (
|
|
108
|
+
<div className="text-xs">
|
|
109
|
+
<div className="text-muted-foreground mb-0.5">blockId</div>
|
|
110
|
+
<CopyableText text={blockId} />
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
{identifier && (
|
|
114
|
+
<div className="text-xs">
|
|
115
|
+
<div className="text-muted-foreground mb-0.5">identifier</div>
|
|
116
|
+
<CopyableText text={identifier} />
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
89
119
|
</div>
|
|
90
|
-
</
|
|
91
|
-
</
|
|
92
|
-
</
|
|
93
|
-
{children}
|
|
120
|
+
</div>
|
|
121
|
+
</PopoverContent>
|
|
122
|
+
</Popover>
|
|
94
123
|
</div>
|
|
95
|
-
|
|
124
|
+
{children}
|
|
125
|
+
</div>
|
|
96
126
|
);
|
|
97
127
|
}
|
|
98
128
|
return children;
|
|
@@ -364,8 +364,8 @@ export function PageBlock({
|
|
|
364
364
|
column,
|
|
365
365
|
}: Readonly<PageBlockProps>) {
|
|
366
366
|
return (
|
|
367
|
-
<
|
|
368
|
-
<
|
|
367
|
+
<PageBlockContext.Provider value={{ blockId, title, description, column }}>
|
|
368
|
+
<LocationWrapper>
|
|
369
369
|
<Card className={cn('w-full', className)}>
|
|
370
370
|
{title || description ? (
|
|
371
371
|
<CardHeader>
|
|
@@ -375,8 +375,8 @@ export function PageBlock({
|
|
|
375
375
|
) : null}
|
|
376
376
|
<CardContent className={cn(!title ? 'pt-6' : '')}>{children}</CardContent>
|
|
377
377
|
</Card>
|
|
378
|
-
</
|
|
379
|
-
</
|
|
378
|
+
</LocationWrapper>
|
|
379
|
+
</PageBlockContext.Provider>
|
|
380
380
|
);
|
|
381
381
|
}
|
|
382
382
|
|
|
@@ -397,11 +397,11 @@ export function FullWidthPageBlock({
|
|
|
397
397
|
blockId,
|
|
398
398
|
}: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
|
|
399
399
|
return (
|
|
400
|
-
<
|
|
401
|
-
<
|
|
400
|
+
<PageBlockContext.Provider value={{ blockId, column: 'main' }}>
|
|
401
|
+
<LocationWrapper>
|
|
402
402
|
<div className={cn('w-full', className)}>{children}</div>
|
|
403
|
-
</
|
|
404
|
-
</
|
|
403
|
+
</LocationWrapper>
|
|
404
|
+
</PageBlockContext.Provider>
|
|
405
405
|
);
|
|
406
406
|
}
|
|
407
407
|
|
|
@@ -19,8 +19,6 @@ import {
|
|
|
19
19
|
import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
|
|
20
20
|
import { FormControl } from '@/vdb/components/ui/form.js';
|
|
21
21
|
import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
|
|
22
|
-
import { useComponentRegistry } from '../component-registry/component-registry.js';
|
|
23
|
-
import { generateInputComponentKey } from '../extension-api/input-component-extensions.js';
|
|
24
22
|
import {
|
|
25
23
|
CustomFieldsPageBlock,
|
|
26
24
|
DetailFormGrid,
|
|
@@ -96,8 +94,6 @@ export interface DetailPageFieldProps<
|
|
|
96
94
|
> {
|
|
97
95
|
fieldInfo: FieldInfo;
|
|
98
96
|
field: ControllerRenderProps<TFieldValues, TName>;
|
|
99
|
-
blockId: string;
|
|
100
|
-
pageId: string;
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
/**
|
|
@@ -106,21 +102,7 @@ export interface DetailPageFieldProps<
|
|
|
106
102
|
function FieldInputRenderer<
|
|
107
103
|
TFieldValues extends FieldValues = FieldValues,
|
|
108
104
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
109
|
-
>({ fieldInfo, field
|
|
110
|
-
const componentRegistry = useComponentRegistry();
|
|
111
|
-
const customInputComponentKey = generateInputComponentKey(pageId, blockId, fieldInfo.name);
|
|
112
|
-
|
|
113
|
-
const DisplayComponent = componentRegistry.getDisplayComponent(customInputComponentKey);
|
|
114
|
-
const InputComponent = componentRegistry.getInputComponent(customInputComponentKey);
|
|
115
|
-
|
|
116
|
-
if (DisplayComponent) {
|
|
117
|
-
return <DisplayComponent {...field} />;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (InputComponent) {
|
|
121
|
-
return <InputComponent {...field} />;
|
|
122
|
-
}
|
|
123
|
-
|
|
105
|
+
>({ fieldInfo, field }: DetailPageFieldProps<TFieldValues, TName>) {
|
|
124
106
|
switch (fieldInfo.type) {
|
|
125
107
|
case 'Int':
|
|
126
108
|
case 'Float':
|
|
@@ -244,12 +226,7 @@ export function DetailPage<
|
|
|
244
226
|
label={fieldInfo.name}
|
|
245
227
|
renderFormControl={false}
|
|
246
228
|
render={({ field }) => (
|
|
247
|
-
<FieldInputRenderer
|
|
248
|
-
fieldInfo={fieldInfo}
|
|
249
|
-
field={field}
|
|
250
|
-
blockId="main-form"
|
|
251
|
-
pageId={pageId}
|
|
252
|
-
/>
|
|
229
|
+
<FieldInputRenderer fieldInfo={fieldInfo} field={field} />
|
|
253
230
|
)}
|
|
254
231
|
/>
|
|
255
232
|
);
|
|
@@ -267,12 +244,7 @@ export function DetailPage<
|
|
|
267
244
|
label={fieldInfo.name}
|
|
268
245
|
renderFormControl={false}
|
|
269
246
|
render={({ field }) => (
|
|
270
|
-
<FieldInputRenderer
|
|
271
|
-
fieldInfo={fieldInfo}
|
|
272
|
-
field={field}
|
|
273
|
-
blockId="main-form"
|
|
274
|
-
pageId={pageId}
|
|
275
|
-
/>
|
|
247
|
+
<FieldInputRenderer fieldInfo={fieldInfo} field={field} />
|
|
276
248
|
)}
|
|
277
249
|
/>
|
|
278
250
|
);
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { PageBlockContext } from '@/vdb/framework/layout-engine/page-block-provider.js';
|
|
2
2
|
import { useContext } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @description
|
|
6
|
+
* Returns the current PageBlock context, which means there must be
|
|
7
|
+
* a PageBlock ancestor component higher in the tree.
|
|
8
|
+
*
|
|
9
|
+
* If `optional` is set to true, the hook will not throw if no PageBlock
|
|
10
|
+
* exists higher in the tree, but will just return undefined.
|
|
11
|
+
*/
|
|
12
|
+
export function usePageBlock({ optional }: { optional?: boolean } = {}) {
|
|
5
13
|
const pageBlock = useContext(PageBlockContext);
|
|
6
|
-
if (!pageBlock) {
|
|
14
|
+
if (!pageBlock && !optional) {
|
|
7
15
|
throw new Error('PageBlockProvider not found');
|
|
8
16
|
}
|
|
9
17
|
return pageBlock;
|