@vendure/dashboard 3.5.2-master-202512170238 → 3.5.2-master-202512180239
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/dashboard.plugin.js +1 -1
- package/package.json +3 -3
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +249 -167
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +8 -0
- package/src/app/routes/_authenticated/_collections/components/move-collections-dialog.tsx +4 -0
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +2 -9
- package/src/lib/components/data-input/number-input.tsx +24 -5
- package/src/lib/components/data-table/data-table-utils.ts +241 -1
- package/src/lib/components/data-table/data-table.tsx +189 -60
- package/src/lib/components/shared/paginated-list-data-table.tsx +19 -0
- package/src/lib/components/ui/alert.tsx +1 -1
- package/src/lib/framework/page/list-page.tsx +62 -38
- package/src/lib/hooks/use-drag-and-drop.ts +86 -0
|
@@ -151,7 +151,7 @@ let DashboardPlugin = DashboardPlugin_1 = class DashboardPlugin {
|
|
|
151
151
|
createStaticServer(dashboardPath) {
|
|
152
152
|
const limiter = (0, express_rate_limit_1.rateLimit)({
|
|
153
153
|
windowMs: 60 * 1000,
|
|
154
|
-
limit: process.env.NODE_ENV === 'production' ? 500 :
|
|
154
|
+
limit: process.env.NODE_ENV === 'production' ? 500 : 1000000,
|
|
155
155
|
standardHeaders: true,
|
|
156
156
|
legacyHeaders: false,
|
|
157
157
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.5.2-master-
|
|
4
|
+
"version": "3.5.2-master-202512180239",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -156,8 +156,8 @@
|
|
|
156
156
|
"@storybook/addon-vitest": "^10.0.0-beta.9",
|
|
157
157
|
"@storybook/react-vite": "^10.0.0-beta.9",
|
|
158
158
|
"@types/node": "^22.13.4",
|
|
159
|
-
"@vendure/common": "^3.5.2-master-
|
|
160
|
-
"@vendure/core": "^3.5.2-master-
|
|
159
|
+
"@vendure/common": "^3.5.2-master-202512180239",
|
|
160
|
+
"@vendure/core": "^3.5.2-master-202512180239",
|
|
161
161
|
"@vitest/browser": "^3.2.4",
|
|
162
162
|
"@vitest/coverage-v8": "^3.2.4",
|
|
163
163
|
"eslint": "^9.19.0",
|
|
@@ -4,18 +4,25 @@ import { Button } from '@/vdb/components/ui/button.js';
|
|
|
4
4
|
import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
|
|
5
5
|
import { ListPage } from '@/vdb/framework/page/list-page.js';
|
|
6
6
|
import { api } from '@/vdb/graphql/api.js';
|
|
7
|
-
import { Trans } from '@lingui/react/macro';
|
|
8
|
-
import { FetchQueryOptions, useQueries } from '@tanstack/react-query';
|
|
7
|
+
import { Trans, useLingui } from '@lingui/react/macro';
|
|
8
|
+
import { FetchQueryOptions, useQueries, useQueryClient } from '@tanstack/react-query';
|
|
9
9
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
10
10
|
import { ExpandedState, getExpandedRowModel } from '@tanstack/react-table';
|
|
11
11
|
import { TableOptions } from '@tanstack/table-core';
|
|
12
12
|
import { ResultOf } from 'gql.tada';
|
|
13
13
|
import { Folder, FolderOpen, PlusIcon } from 'lucide-react';
|
|
14
14
|
import { useState } from 'react';
|
|
15
|
+
import { toast } from 'sonner';
|
|
15
16
|
|
|
16
17
|
import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
|
|
17
18
|
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
calculateDragTargetPosition,
|
|
21
|
+
calculateSiblingIndex,
|
|
22
|
+
getItemParentId,
|
|
23
|
+
isCircularReference,
|
|
24
|
+
} from '@/vdb/components/data-table/data-table-utils.js';
|
|
25
|
+
import { collectionListDocument, moveCollectionDocument } from './collections.graphql.js';
|
|
19
26
|
import {
|
|
20
27
|
AssignCollectionsToChannelBulkAction,
|
|
21
28
|
DeleteCollectionsBulkAction,
|
|
@@ -25,15 +32,21 @@ import {
|
|
|
25
32
|
} from './components/collection-bulk-actions.js';
|
|
26
33
|
import { CollectionContentsSheet } from './components/collection-contents-sheet.js';
|
|
27
34
|
|
|
35
|
+
|
|
28
36
|
export const Route = createFileRoute('/_authenticated/_collections/collections')({
|
|
29
37
|
component: CollectionListPage,
|
|
30
38
|
loader: () => ({ breadcrumb: () => <Trans>Collections</Trans> }),
|
|
31
39
|
});
|
|
32
40
|
|
|
41
|
+
|
|
33
42
|
type Collection = ResultOf<typeof collectionListDocument>['collections']['items'][number];
|
|
34
43
|
|
|
35
44
|
function CollectionListPage() {
|
|
45
|
+
const { t } = useLingui();
|
|
46
|
+
const queryClient = useQueryClient();
|
|
36
47
|
const [expanded, setExpanded] = useState<ExpandedState>({});
|
|
48
|
+
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
49
|
+
|
|
37
50
|
const childrenQueries = useQueries({
|
|
38
51
|
queries: Object.entries(expanded).map(([collectionId, isExpanded]) => {
|
|
39
52
|
return {
|
|
@@ -50,6 +63,7 @@ function CollectionListPage() {
|
|
|
50
63
|
} satisfies FetchQueryOptions;
|
|
51
64
|
}),
|
|
52
65
|
});
|
|
66
|
+
|
|
53
67
|
const childCollectionsByParentId = childrenQueries.reduce(
|
|
54
68
|
(acc, query, index) => {
|
|
55
69
|
const collectionId = Object.keys(expanded)[index];
|
|
@@ -79,177 +93,245 @@ function CollectionListPage() {
|
|
|
79
93
|
return allRows;
|
|
80
94
|
};
|
|
81
95
|
|
|
96
|
+
const handleReorder = async (oldIndex: number, newIndex: number, item: Collection, allItems?: Collection[]) => {
|
|
97
|
+
try {
|
|
98
|
+
const items = allItems || [];
|
|
99
|
+
const sourceParentId = getItemParentId(item);
|
|
100
|
+
|
|
101
|
+
if (!sourceParentId) {
|
|
102
|
+
throw new Error('Unable to determine parent collection ID');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Calculate target position (parent and index)
|
|
106
|
+
const { targetParentId, adjustedIndex: initialIndex } = calculateDragTargetPosition({
|
|
107
|
+
item,
|
|
108
|
+
oldIndex,
|
|
109
|
+
newIndex,
|
|
110
|
+
items,
|
|
111
|
+
sourceParentId,
|
|
112
|
+
expanded,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Validate no circular references when moving to different parent
|
|
116
|
+
if (targetParentId !== sourceParentId && isCircularReference(item, targetParentId, items)) {
|
|
117
|
+
toast.error(t`Cannot move a collection into its own descendant`);
|
|
118
|
+
throw new Error('Circular reference detected');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Calculate final index (adjust for same-parent moves)
|
|
122
|
+
const adjustedIndex = targetParentId === sourceParentId
|
|
123
|
+
? calculateSiblingIndex({ item, oldIndex, newIndex, items, parentId: sourceParentId })
|
|
124
|
+
: initialIndex;
|
|
125
|
+
|
|
126
|
+
// Perform the move
|
|
127
|
+
await api.mutate(moveCollectionDocument, {
|
|
128
|
+
input: {
|
|
129
|
+
collectionId: item.id,
|
|
130
|
+
parentId: targetParentId,
|
|
131
|
+
index: adjustedIndex,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Invalidate queries and show success message
|
|
136
|
+
const queriesToInvalidate = [
|
|
137
|
+
queryClient.invalidateQueries({ queryKey: ['childCollections', sourceParentId] }),
|
|
138
|
+
queryClient.invalidateQueries({ queryKey: ['PaginatedListDataTable'] }),
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
if (targetParentId === sourceParentId) {
|
|
142
|
+
await Promise.all(queriesToInvalidate);
|
|
143
|
+
toast.success(t`Collection position updated`);
|
|
144
|
+
} else {
|
|
145
|
+
queriesToInvalidate.push(
|
|
146
|
+
queryClient.invalidateQueries({ queryKey: ['childCollections', targetParentId] })
|
|
147
|
+
);
|
|
148
|
+
await Promise.all(queriesToInvalidate);
|
|
149
|
+
toast.success(t`Collection moved to new parent`);
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Failed to reorder collection:', error);
|
|
153
|
+
if (error instanceof Error && error.message !== 'Circular reference detected') {
|
|
154
|
+
toast.error(t`Failed to update collection position`);
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
82
160
|
return (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
options
|
|
93
|
-
|
|
94
|
-
topLevelOnly: !isFiltering,
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}}
|
|
98
|
-
customizeColumns={{
|
|
99
|
-
name: {
|
|
100
|
-
meta: {
|
|
101
|
-
// This column needs the following fields to always be available
|
|
102
|
-
// in order to correctly render.
|
|
103
|
-
dependencies: ['children', 'breadcrumbs'],
|
|
104
|
-
},
|
|
105
|
-
cell: ({ row }) => {
|
|
106
|
-
const isExpanded = row.getIsExpanded();
|
|
107
|
-
const hasChildren = !!row.original.children?.length;
|
|
108
|
-
return (
|
|
109
|
-
<div
|
|
110
|
-
style={{ marginLeft: (row.original.breadcrumbs?.length - 2) * 20 + 'px' }}
|
|
111
|
-
className="flex gap-2 items-center"
|
|
112
|
-
>
|
|
113
|
-
<Button
|
|
114
|
-
size="icon"
|
|
115
|
-
variant="secondary"
|
|
116
|
-
onClick={row.getToggleExpandedHandler()}
|
|
117
|
-
disabled={!hasChildren}
|
|
118
|
-
className={!hasChildren ? 'opacity-20' : ''}
|
|
119
|
-
>
|
|
120
|
-
{isExpanded ? <FolderOpen /> : <Folder />}
|
|
121
|
-
</Button>
|
|
122
|
-
<DetailPageButton id={row.original.id} label={row.original.name} />
|
|
123
|
-
</div>
|
|
124
|
-
);
|
|
125
|
-
},
|
|
161
|
+
<ListPage
|
|
162
|
+
pageId="collection-list"
|
|
163
|
+
title={<Trans>Collections</Trans>}
|
|
164
|
+
listQuery={collectionListDocument}
|
|
165
|
+
transformVariables={input => {
|
|
166
|
+
const filterTerm = input.options?.filter?.name?.contains;
|
|
167
|
+
const isFiltering = !!filterTerm;
|
|
168
|
+
return {
|
|
169
|
+
options: {
|
|
170
|
+
...input.options,
|
|
171
|
+
topLevelOnly: !isFiltering,
|
|
126
172
|
},
|
|
127
|
-
|
|
128
|
-
|
|
173
|
+
};
|
|
174
|
+
}}
|
|
175
|
+
customizeColumns={{
|
|
176
|
+
name: {
|
|
177
|
+
meta: {
|
|
178
|
+
dependencies: ['children', 'breadcrumbs'],
|
|
129
179
|
},
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
productVariants: {
|
|
147
|
-
header: () => <Trans>Contents</Trans>,
|
|
148
|
-
cell: ({ row }) => {
|
|
149
|
-
return (
|
|
150
|
-
<CollectionContentsSheet
|
|
151
|
-
collectionId={row.original.id}
|
|
152
|
-
collectionName={row.original.name}
|
|
180
|
+
cell: ({ row }) => {
|
|
181
|
+
const isExpanded = row.getIsExpanded();
|
|
182
|
+
const hasChildren = !!row.original.children?.length;
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
style={{ marginLeft: (row.original.breadcrumbs?.length - 2) * 20 + 'px' }}
|
|
186
|
+
className="flex gap-2 items-center"
|
|
187
|
+
>
|
|
188
|
+
<Button
|
|
189
|
+
size="icon"
|
|
190
|
+
variant="secondary"
|
|
191
|
+
onClick={row.getToggleExpandedHandler()}
|
|
192
|
+
disabled={!hasChildren}
|
|
193
|
+
className={!hasChildren ? 'opacity-20' : ''}
|
|
153
194
|
>
|
|
154
|
-
<
|
|
155
|
-
</
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
children: {
|
|
160
|
-
cell: ({ row }) => {
|
|
161
|
-
const children = row.original.children ?? [];
|
|
162
|
-
const count = children.length;
|
|
163
|
-
const maxDisplay = 5;
|
|
164
|
-
const leftOver = Math.max(count - maxDisplay, 0);
|
|
165
|
-
return (
|
|
166
|
-
<div className="flex flex-wrap gap-2">
|
|
167
|
-
{children.slice(0, maxDisplay).map(child => (
|
|
168
|
-
<Badge variant="outline">{child.name}</Badge>
|
|
169
|
-
))}
|
|
170
|
-
{leftOver > 0 ? (
|
|
171
|
-
<Badge variant="outline">
|
|
172
|
-
<Trans>+ {leftOver} more</Trans>
|
|
173
|
-
</Badge>
|
|
174
|
-
) : null}
|
|
175
|
-
</div>
|
|
176
|
-
);
|
|
177
|
-
},
|
|
195
|
+
{isExpanded ? <FolderOpen /> : <Folder />}
|
|
196
|
+
</Button>
|
|
197
|
+
<DetailPageButton id={row.original.id} label={row.original.name} />
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
178
200
|
},
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
options.getExpandedRowModel = getExpandedRowModel();
|
|
198
|
-
options.getRowCanExpand = () => true;
|
|
199
|
-
options.getRowId = row => {
|
|
200
|
-
return row.id;
|
|
201
|
-
};
|
|
202
|
-
return options;
|
|
203
|
-
}}
|
|
204
|
-
defaultVisibility={{
|
|
205
|
-
id: false,
|
|
206
|
-
createdAt: false,
|
|
207
|
-
updatedAt: false,
|
|
208
|
-
position: false,
|
|
209
|
-
parentId: false,
|
|
210
|
-
children: false,
|
|
211
|
-
description: false,
|
|
212
|
-
}}
|
|
213
|
-
onSearchTermChange={searchTerm => {
|
|
214
|
-
return {
|
|
215
|
-
name: { contains: searchTerm },
|
|
216
|
-
};
|
|
217
|
-
}}
|
|
218
|
-
route={Route}
|
|
219
|
-
bulkActions={[
|
|
220
|
-
{
|
|
221
|
-
component: AssignCollectionsToChannelBulkAction,
|
|
222
|
-
order: 100,
|
|
201
|
+
},
|
|
202
|
+
description: {
|
|
203
|
+
cell: RichTextDescriptionCell,
|
|
204
|
+
},
|
|
205
|
+
breadcrumbs: {
|
|
206
|
+
cell: ({ cell }) => {
|
|
207
|
+
const value = cell.getValue();
|
|
208
|
+
if (!Array.isArray(value)) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return (
|
|
212
|
+
<div>
|
|
213
|
+
{value
|
|
214
|
+
.slice(1)
|
|
215
|
+
.map(breadcrumb => breadcrumb.name)
|
|
216
|
+
.join(' / ')}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
223
219
|
},
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
220
|
+
},
|
|
221
|
+
productVariants: {
|
|
222
|
+
header: () => <Trans>Contents</Trans>,
|
|
223
|
+
cell: ({ row }) => {
|
|
224
|
+
return (
|
|
225
|
+
<CollectionContentsSheet
|
|
226
|
+
collectionId={row.original.id}
|
|
227
|
+
collectionName={row.original.name}
|
|
228
|
+
>
|
|
229
|
+
<Trans>{row.original.productVariants?.totalItems} variants</Trans>
|
|
230
|
+
</CollectionContentsSheet>
|
|
231
|
+
);
|
|
227
232
|
},
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
},
|
|
234
|
+
children: {
|
|
235
|
+
cell: ({ row }) => {
|
|
236
|
+
const children = row.original.children ?? [];
|
|
237
|
+
const count = children.length;
|
|
238
|
+
const maxDisplay = 5;
|
|
239
|
+
const leftOver = Math.max(count - maxDisplay, 0);
|
|
240
|
+
return (
|
|
241
|
+
<div className="flex flex-wrap gap-2">
|
|
242
|
+
{children.slice(0, maxDisplay).map(child => (
|
|
243
|
+
<Badge key={child.id} variant="outline">{child.name}</Badge>
|
|
244
|
+
))}
|
|
245
|
+
{leftOver > 0 ? (
|
|
246
|
+
<Badge variant="outline">
|
|
247
|
+
<Trans>+ {leftOver} more</Trans>
|
|
248
|
+
</Badge>
|
|
249
|
+
) : null}
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
231
252
|
},
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
},
|
|
254
|
+
}}
|
|
255
|
+
defaultColumnOrder={[
|
|
256
|
+
'featuredAsset',
|
|
257
|
+
'name',
|
|
258
|
+
'slug',
|
|
259
|
+
'breadcrumbs',
|
|
260
|
+
'productVariants',
|
|
261
|
+
]}
|
|
262
|
+
transformData={data => {
|
|
263
|
+
return addSubCollections(data);
|
|
264
|
+
}}
|
|
265
|
+
setTableOptions={(options: TableOptions<any>) => {
|
|
266
|
+
options.state = {
|
|
267
|
+
...options.state,
|
|
268
|
+
expanded: expanded,
|
|
269
|
+
};
|
|
270
|
+
options.onExpandedChange = setExpanded;
|
|
271
|
+
options.getExpandedRowModel = getExpandedRowModel();
|
|
272
|
+
options.getRowCanExpand = () => true;
|
|
273
|
+
options.getRowId = row => {
|
|
274
|
+
return row.id;
|
|
275
|
+
};
|
|
276
|
+
options.meta = {
|
|
277
|
+
...options.meta,
|
|
278
|
+
resetExpanded: () => setExpanded({}),
|
|
279
|
+
};
|
|
280
|
+
return options;
|
|
281
|
+
}}
|
|
282
|
+
defaultVisibility={{
|
|
283
|
+
id: false,
|
|
284
|
+
createdAt: false,
|
|
285
|
+
updatedAt: false,
|
|
286
|
+
position: false,
|
|
287
|
+
parentId: false,
|
|
288
|
+
children: false,
|
|
289
|
+
description: false,
|
|
290
|
+
isPrivate: false,
|
|
291
|
+
}}
|
|
292
|
+
onSearchTermChange={searchTerm => {
|
|
293
|
+
setSearchTerm(searchTerm);
|
|
294
|
+
return {
|
|
295
|
+
name: { contains: searchTerm },
|
|
296
|
+
};
|
|
297
|
+
}}
|
|
298
|
+
route={Route}
|
|
299
|
+
bulkActions={[
|
|
300
|
+
{
|
|
301
|
+
component: AssignCollectionsToChannelBulkAction,
|
|
302
|
+
order: 100,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
component: RemoveCollectionsFromChannelBulkAction,
|
|
306
|
+
order: 200,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
component: DuplicateCollectionsBulkAction,
|
|
310
|
+
order: 300,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
component: MoveCollectionsBulkAction,
|
|
314
|
+
order: 400,
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
component: DeleteCollectionsBulkAction,
|
|
318
|
+
order: 500,
|
|
319
|
+
},
|
|
320
|
+
]}
|
|
321
|
+
onReorder={handleReorder}
|
|
322
|
+
disableDragAndDrop={!!searchTerm} // Disable dragging while searching
|
|
323
|
+
>
|
|
324
|
+
<PageActionBarRight>
|
|
325
|
+
<PermissionGuard requires={['CreateCollection', 'CreateCatalog']}>
|
|
326
|
+
<Button asChild>
|
|
327
|
+
<Link to="./new">
|
|
328
|
+
<PlusIcon className="mr-2 h-4 w-4" />
|
|
329
|
+
<Trans>New Collection</Trans>
|
|
330
|
+
</Link>
|
|
331
|
+
</Button>
|
|
332
|
+
</PermissionGuard>
|
|
333
|
+
</PageActionBarRight>
|
|
334
|
+
</ListPage>
|
|
254
335
|
);
|
|
255
336
|
}
|
|
337
|
+
|
|
@@ -103,6 +103,13 @@ export const MoveCollectionsBulkAction: BulkActionComponent<any> = ({ selection,
|
|
|
103
103
|
table.resetRowSelection();
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
const handleResetExpanded = () => {
|
|
107
|
+
const resetExpanded = (table.options.meta as { resetExpanded: () => void })?.resetExpanded;
|
|
108
|
+
if (resetExpanded) {
|
|
109
|
+
resetExpanded();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
106
113
|
return (
|
|
107
114
|
<>
|
|
108
115
|
<DataTableBulkActionItem
|
|
@@ -116,6 +123,7 @@ export const MoveCollectionsBulkAction: BulkActionComponent<any> = ({ selection,
|
|
|
116
123
|
onOpenChange={setDialogOpen}
|
|
117
124
|
collectionsToMove={selection}
|
|
118
125
|
onSuccess={handleSuccess}
|
|
126
|
+
onResetExpanded={handleResetExpanded}
|
|
119
127
|
/>
|
|
120
128
|
</>
|
|
121
129
|
);
|
|
@@ -34,6 +34,7 @@ interface MoveCollectionsDialogProps {
|
|
|
34
34
|
onOpenChange: (open: boolean) => void;
|
|
35
35
|
collectionsToMove: Collection[];
|
|
36
36
|
onSuccess?: () => void;
|
|
37
|
+
onResetExpanded?: () => void;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
interface CollectionTreeNodeProps {
|
|
@@ -209,6 +210,7 @@ export function MoveCollectionsDialog({
|
|
|
209
210
|
onOpenChange,
|
|
210
211
|
collectionsToMove,
|
|
211
212
|
onSuccess,
|
|
213
|
+
onResetExpanded,
|
|
212
214
|
}: Readonly<MoveCollectionsDialogProps>) {
|
|
213
215
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
|
214
216
|
const [selectedCollectionId, setSelectedCollectionId] = useState<string>();
|
|
@@ -282,6 +284,8 @@ export function MoveCollectionsDialog({
|
|
|
282
284
|
toast.success(t`Collections moved successfully`);
|
|
283
285
|
queryClient.invalidateQueries({ queryKey: collectionForMoveKey });
|
|
284
286
|
queryClient.invalidateQueries({ queryKey: childCollectionsForMoveKey() });
|
|
287
|
+
queryClient.invalidateQueries({ queryKey: ['PaginatedListDataTable'] });
|
|
288
|
+
onResetExpanded?.();
|
|
285
289
|
onSuccess?.();
|
|
286
290
|
onOpenChange(false);
|
|
287
291
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NumberInput } from '@/vdb/components/data-input/number-input.js';
|
|
2
2
|
import { ErrorPage } from '@/vdb/components/shared/error-page.js';
|
|
3
3
|
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
4
4
|
import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
|
|
@@ -121,14 +121,7 @@ function TaxRateDetailPage() {
|
|
|
121
121
|
name="value"
|
|
122
122
|
label={<Trans>Rate</Trans>}
|
|
123
123
|
render={({ field }) => (
|
|
124
|
-
<
|
|
125
|
-
{...field}
|
|
126
|
-
type="number"
|
|
127
|
-
suffix="%"
|
|
128
|
-
min={0}
|
|
129
|
-
value={field.value}
|
|
130
|
-
onChange={e => field.onChange(e.target.valueAsNumber)}
|
|
131
|
-
/>
|
|
124
|
+
<NumberInput {...field} value={field.value} min={0} step={0.01} suffix="%" />
|
|
132
125
|
)}
|
|
133
126
|
/>
|
|
134
127
|
<FormFieldWrapper
|
|
@@ -3,11 +3,14 @@ import { Input } from '@/vdb/components/ui/input.js';
|
|
|
3
3
|
|
|
4
4
|
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
5
5
|
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
6
|
+
import { ReactNode } from 'react';
|
|
6
7
|
|
|
7
8
|
export type NumberInputProps = DashboardFormComponentProps & {
|
|
8
9
|
min?: number;
|
|
9
10
|
max?: number;
|
|
10
11
|
step?: number;
|
|
12
|
+
prefix?: ReactNode;
|
|
13
|
+
suffix?: ReactNode;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
/**
|
|
@@ -17,28 +20,43 @@ export type NumberInputProps = DashboardFormComponentProps & {
|
|
|
17
20
|
* @docsCategory form-components
|
|
18
21
|
* @docsPage NumberInput
|
|
19
22
|
*/
|
|
20
|
-
export function NumberInput({
|
|
23
|
+
export function NumberInput({
|
|
24
|
+
fieldDef,
|
|
25
|
+
onChange,
|
|
26
|
+
prefix: overridePrefix,
|
|
27
|
+
suffix: overrideSuffix,
|
|
28
|
+
...fieldProps
|
|
29
|
+
}: Readonly<NumberInputProps>) {
|
|
21
30
|
const readOnly = fieldProps.disabled || isReadonlyField(fieldDef);
|
|
22
31
|
const isFloat = fieldDef ? fieldDef.type === 'float' : false;
|
|
23
32
|
const min = fieldProps.min ?? fieldDef?.ui?.min;
|
|
24
33
|
const max = fieldProps.max ?? fieldDef?.ui?.max;
|
|
25
34
|
const step = fieldProps.step ?? (fieldDef?.ui?.step || (isFloat ? 0.01 : 1));
|
|
26
|
-
const prefix = fieldDef?.ui?.prefix;
|
|
27
|
-
const suffix = fieldDef?.ui?.suffix;
|
|
35
|
+
const prefix = overridePrefix ?? fieldDef?.ui?.prefix;
|
|
36
|
+
const suffix = overrideSuffix ?? fieldDef?.ui?.suffix;
|
|
28
37
|
const shouldUseAffixedInput = prefix || suffix;
|
|
38
|
+
const value = fieldProps.value ?? '';
|
|
29
39
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
30
40
|
if (readOnly) return;
|
|
31
|
-
|
|
41
|
+
|
|
42
|
+
let numValue = e.target.valueAsNumber;
|
|
43
|
+
|
|
44
|
+
if (Number.isNaN(numValue) && e.target.value) {
|
|
45
|
+
const normalized = e.target.value.replace(',', '.');
|
|
46
|
+
numValue = Number(normalized);
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
if (Number.isNaN(numValue)) {
|
|
33
50
|
onChange(null);
|
|
34
51
|
} else {
|
|
35
|
-
onChange(
|
|
52
|
+
onChange(numValue);
|
|
36
53
|
}
|
|
37
54
|
};
|
|
38
55
|
if (shouldUseAffixedInput) {
|
|
39
56
|
return (
|
|
40
57
|
<AffixedInput
|
|
41
58
|
{...fieldProps}
|
|
59
|
+
value={value}
|
|
42
60
|
type="number"
|
|
43
61
|
onChange={handleChange}
|
|
44
62
|
min={min}
|
|
@@ -57,6 +75,7 @@ export function NumberInput({ fieldDef, onChange, ...fieldProps }: Readonly<Numb
|
|
|
57
75
|
type="number"
|
|
58
76
|
onChange={handleChange}
|
|
59
77
|
{...fieldProps}
|
|
78
|
+
value={value}
|
|
60
79
|
min={min}
|
|
61
80
|
max={max}
|
|
62
81
|
step={step}
|