@vendure/dashboard 3.5.2-master-202512040233 → 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.
Files changed (69) hide show
  1. package/dist/plugin/constants.js +2 -2
  2. package/dist/plugin/dashboard.plugin.js +1 -1
  3. package/dist/vite/constants.js +1 -0
  4. package/lingui.config.js +1 -0
  5. package/package.json +7 -7
  6. package/src/app/routeTree.gen.ts +1221 -0
  7. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
  8. package/src/app/routes/_authenticated/_collections/collections.tsx +249 -167
  9. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +8 -0
  10. package/src/app/routes/_authenticated/_collections/components/move-collections-dialog.tsx +4 -0
  11. package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +0 -1
  12. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
  13. package/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx +139 -0
  14. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +3 -0
  15. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +3 -3
  16. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +49 -11
  17. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +9 -0
  18. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +23 -0
  19. package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +3 -3
  20. package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +2 -2
  21. package/src/app/routes/_authenticated/_products/products.graphql.ts +1 -0
  22. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +2 -9
  23. package/src/i18n/locales/bg.po +3436 -0
  24. package/src/lib/components/data-input/datetime-input.tsx +1 -1
  25. package/src/lib/components/data-input/number-input.tsx +24 -5
  26. package/src/lib/components/data-input/relation-selector.tsx +1 -1
  27. package/src/lib/components/data-input/struct-form-input.tsx +175 -174
  28. package/src/lib/components/data-table/data-table-utils.ts +241 -1
  29. package/src/lib/components/data-table/data-table.tsx +190 -60
  30. package/src/lib/components/layout/manage-languages-dialog.tsx +2 -25
  31. package/src/lib/components/shared/custom-fields-form.tsx +13 -8
  32. package/src/lib/components/shared/paginated-list-data-table.tsx +19 -0
  33. package/src/lib/components/ui/alert.tsx +1 -1
  34. package/src/lib/components/ui/carousel.tsx +2 -2
  35. package/src/lib/components/ui/chart.tsx +1 -1
  36. package/src/lib/components/ui/context-menu.tsx +1 -1
  37. package/src/lib/components/ui/drawer.tsx +1 -1
  38. package/src/lib/components/ui/grid-layout.tsx +1 -1
  39. package/src/lib/components/ui/input-group.tsx +1 -0
  40. package/src/lib/components/ui/input-otp.tsx +1 -1
  41. package/src/lib/components/ui/menubar.tsx +1 -1
  42. package/src/lib/components/ui/navigation-menu.tsx +1 -1
  43. package/src/lib/components/ui/progress.tsx +1 -1
  44. package/src/lib/components/ui/radio-group.tsx +1 -1
  45. package/src/lib/components/ui/resizable.tsx +1 -1
  46. package/src/lib/components/ui/select.tsx +1 -1
  47. package/src/lib/components/ui/slider.tsx +1 -1
  48. package/src/lib/components/ui/toggle-group.tsx +2 -2
  49. package/src/lib/components/ui/toggle.tsx +1 -1
  50. package/src/lib/framework/component-registry/component-registry.tsx +2 -6
  51. package/src/lib/framework/extension-api/display-component-extensions.tsx +4 -3
  52. package/src/lib/framework/extension-api/logic/detail-forms.ts +0 -13
  53. package/src/lib/framework/extension-api/types/data-table.ts +4 -2
  54. package/src/lib/framework/extension-api/types/navigation.ts +2 -2
  55. package/src/lib/framework/form-engine/use-generated-form.tsx +7 -1
  56. package/src/lib/framework/layout-engine/page-layout.tsx +1 -1
  57. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +1 -1
  58. package/src/lib/framework/page/detail-page-route-loader.tsx +1 -1
  59. package/src/lib/framework/page/list-page.tsx +62 -38
  60. package/src/lib/framework/page/page-api.ts +1 -1
  61. package/src/lib/framework/page/use-detail-page.ts +4 -2
  62. package/src/lib/framework/page/use-extended-router.tsx +20 -16
  63. package/src/lib/framework/registry/registry-types.ts +2 -1
  64. package/src/lib/graphql/graphql-env.d.ts +8 -12
  65. package/src/lib/hooks/use-drag-and-drop.ts +86 -0
  66. package/src/lib/providers/channel-provider.tsx +11 -7
  67. package/LICENSE.md +0 -42
  68. package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +0 -129
  69. /package/src/{app/routes/_authenticated/_global-settings → lib}/utils/global-languages.ts +0 -0
@@ -1,4 +1,4 @@
1
- import { AccessorFnColumnDef } from '@tanstack/react-table';
1
+ import { AccessorFnColumnDef, ExpandedState } from '@tanstack/react-table';
2
2
  import { AccessorKeyColumnDef } from '@tanstack/table-core';
3
3
 
4
4
  /**
@@ -49,3 +49,243 @@ export function getStandardizedDefaultColumnOrder<T extends string | number | sy
49
49
  const rest = defaultColumnOrder.filter(c => !standardFirstColumns.has(c as string));
50
50
  return [...standardFirstColumns, ...rest] as T[];
51
51
  }
52
+
53
+ /**
54
+ * Hierarchical item type with parent-child relationships
55
+ */
56
+ export interface HierarchicalItem {
57
+ id: string;
58
+ parentId?: string | null;
59
+ breadcrumbs?: Array<{ id: string }>;
60
+ children?: Array<{ id: string }> | null;
61
+ }
62
+
63
+ /**
64
+ * Gets the parent ID of a hierarchical item
65
+ */
66
+ export function getItemParentId<T extends HierarchicalItem>(
67
+ item: T | null | undefined,
68
+ ): string | null | undefined {
69
+ return item?.parentId || item?.breadcrumbs?.[0]?.id;
70
+ }
71
+
72
+ /**
73
+ * Gets all siblings (items with the same parent) for a given parent ID
74
+ */
75
+ export function getItemSiblings<T extends HierarchicalItem>(
76
+ items: T[],
77
+ parentId: string | null | undefined,
78
+ ): T[] {
79
+ return items.filter(item => getItemParentId(item) === parentId);
80
+ }
81
+
82
+ /**
83
+ * Checks if moving an item to a new parent would create a circular reference
84
+ */
85
+ export function isCircularReference<T extends HierarchicalItem>(
86
+ item: T,
87
+ targetParentId: string,
88
+ items: T[],
89
+ ): boolean {
90
+ const targetParentItem = items.find(i => i.id === targetParentId);
91
+ return (
92
+ item.children?.some(child => {
93
+ if (child.id === targetParentId) return true;
94
+ const targetBreadcrumbIds = targetParentItem?.breadcrumbs?.map(b => b.id) || [];
95
+ return targetBreadcrumbIds.includes(item.id);
96
+ }) ?? false
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Result of calculating the target position for a drag and drop operation
102
+ */
103
+ export interface TargetPosition {
104
+ targetParentId: string;
105
+ adjustedIndex: number;
106
+ }
107
+
108
+ /**
109
+ * Context for drag and drop position calculation
110
+ */
111
+ interface DragContext<T extends HierarchicalItem> {
112
+ item: T;
113
+ targetItem: T | undefined;
114
+ previousItem: T | null;
115
+ isDraggingDown: boolean;
116
+ isTargetExpanded: boolean;
117
+ isPreviousExpanded: boolean;
118
+ sourceParentId: string;
119
+ items: T[];
120
+ }
121
+
122
+ /**
123
+ * Checks if dragging down directly onto an expanded item
124
+ */
125
+ function isDroppingIntoExpandedTarget<T extends HierarchicalItem>(context: DragContext<T>): boolean {
126
+ const { isDraggingDown, targetItem, item, isTargetExpanded } = context;
127
+ return isDraggingDown && targetItem?.id !== item.id && isTargetExpanded;
128
+ }
129
+
130
+ /**
131
+ * Checks if dragging down into an expanded item's children area
132
+ */
133
+ function isDroppingIntoExpandedPreviousChildren<T extends HierarchicalItem>(
134
+ context: DragContext<T>,
135
+ ): boolean {
136
+ const { isDraggingDown, targetItem, previousItem, item, isPreviousExpanded } = context;
137
+ return (
138
+ isDraggingDown &&
139
+ previousItem !== null &&
140
+ targetItem?.id !== item.id &&
141
+ isPreviousExpanded &&
142
+ targetItem?.parentId === previousItem.id
143
+ );
144
+ }
145
+
146
+ /**
147
+ * Checks if dragging up into an expanded item's children area
148
+ */
149
+ function isDroppingIntoExpandedPreviousWhenDraggingUp<T extends HierarchicalItem>(
150
+ context: DragContext<T>,
151
+ ): boolean {
152
+ const { isDraggingDown, previousItem, isPreviousExpanded } = context;
153
+ return !isDraggingDown && previousItem !== null && isPreviousExpanded;
154
+ }
155
+
156
+ /**
157
+ * Creates a position for dropping into an expanded item as first child
158
+ */
159
+ function createFirstChildPosition(parentId: string): TargetPosition {
160
+ return { targetParentId: parentId, adjustedIndex: 0 };
161
+ }
162
+
163
+ /**
164
+ * Calculates position for cross-parent drag operations
165
+ */
166
+ function calculateCrossParentPosition<T extends HierarchicalItem>(
167
+ targetItem: T,
168
+ sourceParentId: string,
169
+ items: T[],
170
+ ): TargetPosition | null {
171
+ const targetItemParentId = getItemParentId(targetItem);
172
+
173
+ if (!targetItemParentId || targetItemParentId === sourceParentId) {
174
+ return null;
175
+ }
176
+
177
+ const targetSiblings = getItemSiblings(items, targetItemParentId);
178
+ const adjustedIndex = targetSiblings.findIndex(i => i.id === targetItem.id);
179
+
180
+ return { targetParentId: targetItemParentId, adjustedIndex };
181
+ }
182
+
183
+ /**
184
+ * Calculates position when dropping at the end of the list
185
+ */
186
+ function calculateDropAtEndPosition<T extends HierarchicalItem>(
187
+ previousItem: T | null,
188
+ sourceParentId: string,
189
+ items: T[],
190
+ ): TargetPosition | null {
191
+ if (!previousItem) {
192
+ return null;
193
+ }
194
+
195
+ const previousItemParentId = getItemParentId(previousItem);
196
+
197
+ if (!previousItemParentId || previousItemParentId === sourceParentId) {
198
+ return null;
199
+ }
200
+
201
+ const targetSiblings = getItemSiblings(items, previousItemParentId);
202
+ return { targetParentId: previousItemParentId, adjustedIndex: targetSiblings.length };
203
+ }
204
+
205
+ /**
206
+ * Determines the target parent and index for a hierarchical drag and drop operation
207
+ */
208
+ export function calculateDragTargetPosition<T extends HierarchicalItem>(params: {
209
+ item: T;
210
+ oldIndex: number;
211
+ newIndex: number;
212
+ items: T[];
213
+ sourceParentId: string;
214
+ expanded: ExpandedState;
215
+ }): TargetPosition {
216
+ const { item, oldIndex, newIndex, items, sourceParentId, expanded } = params;
217
+
218
+ const targetItem = items[newIndex];
219
+ const previousItem = newIndex > 0 ? items[newIndex - 1] : null;
220
+
221
+ const context: DragContext<T> = {
222
+ item,
223
+ targetItem,
224
+ previousItem,
225
+ isDraggingDown: oldIndex < newIndex,
226
+ isTargetExpanded: targetItem ? !!expanded[targetItem.id as keyof ExpandedState] : false,
227
+ isPreviousExpanded: previousItem ? !!expanded[previousItem.id as keyof ExpandedState] : false,
228
+ sourceParentId,
229
+ items,
230
+ };
231
+
232
+ // Handle dropping into expanded items (becomes first child)
233
+ if (isDroppingIntoExpandedTarget(context)) {
234
+ return createFirstChildPosition(targetItem.id);
235
+ }
236
+
237
+ if (previousItem && isDroppingIntoExpandedPreviousChildren(context)) {
238
+ return createFirstChildPosition(previousItem.id);
239
+ }
240
+
241
+ if (previousItem && isDroppingIntoExpandedPreviousWhenDraggingUp(context)) {
242
+ return createFirstChildPosition(previousItem.id);
243
+ }
244
+
245
+ // Handle cross-parent drag operations
246
+ if (targetItem?.id !== item.id) {
247
+ const crossParentPosition = calculateCrossParentPosition(targetItem, sourceParentId, items);
248
+ if (crossParentPosition) {
249
+ return crossParentPosition;
250
+ }
251
+ }
252
+
253
+ // Handle dropping at the end of the list
254
+ if (!targetItem && previousItem) {
255
+ const dropAtEndPosition = calculateDropAtEndPosition(previousItem, sourceParentId, items);
256
+ if (dropAtEndPosition) {
257
+ return dropAtEndPosition;
258
+ }
259
+ }
260
+
261
+ // Default: stay in the same parent at the beginning
262
+ return { targetParentId: sourceParentId, adjustedIndex: 0 };
263
+ }
264
+
265
+ /**
266
+ * Calculates the adjusted sibling index when reordering within the same parent
267
+ */
268
+ export function calculateSiblingIndex<T extends HierarchicalItem>(params: {
269
+ item: T;
270
+ oldIndex: number;
271
+ newIndex: number;
272
+ items: T[];
273
+ parentId: string;
274
+ }): number {
275
+ const { item, oldIndex, newIndex, items, parentId } = params;
276
+
277
+ const siblings = getItemSiblings(items, parentId);
278
+ const oldSiblingIndex = siblings.findIndex(i => i.id === item.id);
279
+ const isDraggingDown = oldIndex < newIndex;
280
+
281
+ let newSiblingIndex = oldSiblingIndex;
282
+ const [start, end] = isDraggingDown ? [oldIndex + 1, newIndex] : [newIndex, oldIndex - 1];
283
+
284
+ for (let i = start; i <= end; i++) {
285
+ if (getItemParentId(items[i]) === parentId) {
286
+ newSiblingIndex += isDraggingDown ? 1 : -1;
287
+ }
288
+ }
289
+
290
+ return newSiblingIndex;
291
+ }
@@ -15,6 +15,17 @@ import { useChannel } from '@/vdb/hooks/use-channel.js';
15
15
  import { usePage } from '@/vdb/hooks/use-page.js';
16
16
  import { useSavedViews } from '@/vdb/hooks/use-saved-views.js';
17
17
  import { Trans, useLingui } from '@lingui/react/macro';
18
+ import {
19
+ closestCenter,
20
+ DndContext,
21
+ } from '@dnd-kit/core';
22
+ import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
23
+ import {
24
+ SortableContext,
25
+ useSortable,
26
+ verticalListSortingStrategy,
27
+ } from '@dnd-kit/sortable';
28
+ import { CSS } from '@dnd-kit/utilities';
18
29
  import {
19
30
  ColumnDef,
20
31
  ColumnFilter,
@@ -23,18 +34,66 @@ import {
23
34
  getCoreRowModel,
24
35
  getPaginationRowModel,
25
36
  PaginationState,
37
+ Row,
26
38
  SortingState,
27
39
  Table as TableType,
28
40
  useReactTable,
29
41
  VisibilityState,
30
42
  } from '@tanstack/react-table';
31
43
  import { RowSelectionState, TableOptions } from '@tanstack/table-core';
32
- import React, { Suspense, useEffect, useRef } from 'react';
44
+ import { GripVertical } from 'lucide-react';
45
+ import React, { Suspense, useEffect, useId, useMemo, useRef } from 'react';
33
46
  import { AddFilterMenu } from './add-filter-menu.js';
34
47
  import { DataTableBulkActions } from './data-table-bulk-actions.js';
35
48
  import { DataTableProvider } from './data-table-context.js';
36
49
  import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
37
50
  import { DataTableFilterBadgeEditable } from './data-table-filter-badge-editable.js';
51
+ import { useDragAndDrop } from '@/vdb/hooks/use-drag-and-drop.js';
52
+ import { toast } from 'sonner';
53
+
54
+ interface DraggableRowProps<TData> {
55
+ row: Row<TData>;
56
+ isDragDisabled: boolean;
57
+ }
58
+
59
+ function DraggableRow<TData>({ row, isDragDisabled }: Readonly<DraggableRowProps<TData>>) {
60
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
61
+ id: row.id,
62
+ disabled: isDragDisabled,
63
+ });
64
+
65
+ const style = {
66
+ transform: CSS.Transform.toString(transform),
67
+ transition,
68
+ opacity: isDragging ? 0.5 : 1,
69
+ };
70
+
71
+ return (
72
+ <TableRow
73
+ ref={setNodeRef}
74
+ style={style}
75
+ data-state={row.getIsSelected() && 'selected'}
76
+ className="animate-in fade-in duration-100"
77
+ >
78
+ {!isDragDisabled && (
79
+ <TableCell className="w-[40px] h-12">
80
+ <div
81
+ {...attributes}
82
+ {...listeners}
83
+ className="cursor-move text-muted-foreground hover:text-foreground transition-colors"
84
+ >
85
+ <GripVertical className="h-4 w-4" />
86
+ </div>
87
+ </TableCell>
88
+ )}
89
+ {row.getVisibleCells().filter(cell => cell.column.id !== '__drag_handle__').map(cell => (
90
+ <TableCell key={cell.id} className="h-12">
91
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
92
+ </TableCell>
93
+ ))}
94
+ </TableRow>
95
+ );
96
+ }
38
97
 
39
98
  export interface FacetedFilter {
40
99
  title: string;
@@ -77,6 +136,18 @@ interface DataTableProps<TData> {
77
136
  */
78
137
  setTableOptions?: (table: TableOptions<TData>) => TableOptions<TData>;
79
138
  onRefresh?: () => void;
139
+ /**
140
+ * @description
141
+ * Callback when items are reordered via drag and drop.
142
+ * When provided, enables drag-and-drop functionality.
143
+ * The fourth parameter provides all items for context-aware reordering.
144
+ */
145
+ onReorder?: (oldIndex: number, newIndex: number, item: TData, allItems?: TData[]) => void | Promise<void>;
146
+ /**
147
+ * @description
148
+ * When true, drag and drop will be disabled. This will only have an effect if the onReorder prop is also set
149
+ */
150
+ disableDragAndDrop?: boolean;
80
151
  }
81
152
 
82
153
  /**
@@ -111,6 +182,8 @@ export function DataTable<TData>({
111
182
  bulkActions,
112
183
  setTableOptions,
113
184
  onRefresh,
185
+ onReorder,
186
+ disableDragAndDrop = false,
114
187
  }: Readonly<DataTableProps<TData>>) {
115
188
  const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
116
189
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
@@ -131,6 +204,16 @@ export function DataTable<TData>({
131
204
  const prevSearchTermRef = useRef(searchTerm);
132
205
  const prevColumnFiltersRef = useRef(columnFilters);
133
206
 
207
+ const componentId = useId();
208
+ const { sensors, localData, handleDragEnd, itemIds } = useDragAndDrop({
209
+ data,
210
+ onReorder,
211
+ disabled: disableDragAndDrop,
212
+ onError: error => {
213
+ toast.error(t`Failed to reorder items`);
214
+ },
215
+ });
216
+
134
217
  useEffect(() => {
135
218
  // If the defaultColumnVisibility changes externally (e.g. the user reset the table settings),
136
219
  // we want to reset the column visibility to the default.
@@ -143,9 +226,25 @@ export function DataTable<TData>({
143
226
  // We intentionally do not include `columnVisibility` in the dependency array
144
227
  }, [defaultColumnVisibility]);
145
228
 
229
+ // Add drag handle column if drag and drop is enabled
230
+ const columnsWithOptionalDragHandle = useMemo(() => {
231
+ if (!disableDragAndDrop && onReorder) {
232
+ const dragHandleColumn: ColumnDef<TData, any> = {
233
+ id: '__drag_handle__',
234
+ header: '',
235
+ cell: () => null, // Rendered by DraggableRow
236
+ size: 40,
237
+ enableSorting: false,
238
+ enableHiding: false,
239
+ };
240
+ return [dragHandleColumn, ...columns];
241
+ }
242
+ return columns;
243
+ }, [columns, disableDragAndDrop, onReorder]);
244
+
146
245
  let tableOptions: TableOptions<TData> = {
147
- data,
148
- columns,
246
+ data: localData,
247
+ columns: columnsWithOptionalDragHandle,
149
248
  getRowId: row => (row as { id: string }).id,
150
249
  getCoreRowModel: getCoreRowModel(),
151
250
  getPaginationRowModel: getPaginationRowModel(),
@@ -220,6 +319,8 @@ export function DataTable<TData>({
220
319
 
221
320
  const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
222
321
 
322
+ const isDragDisabled = disableDragAndDrop || !onReorder;
323
+
223
324
  return (
224
325
  <DataTableProvider
225
326
  columnFilters={columnFilters}
@@ -254,6 +355,7 @@ export function DataTable<TData>({
254
355
  title={filter?.title}
255
356
  options={filter?.options}
256
357
  optionsFn={filter?.optionsFn}
358
+ icon={filter?.icon}
257
359
  />
258
360
  ))}
259
361
  </Suspense>
@@ -309,66 +411,94 @@ export function DataTable<TData>({
309
411
  ) : null}
310
412
 
311
413
  <div className="rounded-md border my-2 relative shadow-sm">
312
- <Table>
313
- <TableHeader className="bg-muted/50">
314
- {table.getHeaderGroups().map(headerGroup => (
315
- <TableRow key={headerGroup.id}>
316
- {headerGroup.headers.map(header => {
317
- return (
318
- <TableHead key={header.id}>
319
- {header.isPlaceholder
320
- ? null
321
- : flexRender(
322
- header.column.columnDef.header,
323
- header.getContext(),
324
- )}
325
- </TableHead>
326
- );
327
- })}
328
- </TableRow>
329
- ))}
330
- </TableHeader>
331
- <TableBody>
332
- {isLoading && !data?.length ? (
333
- Array.from({ length: Math.min(pagination.pageSize, 100) }).map((_, index) => (
334
- <TableRow
335
- key={`skeleton-${index}`}
336
- className="animate-in fade-in duration-100"
337
- >
338
- {Array.from({ length: visibleColumnCount }).map((_, cellIndex) => (
414
+ <DndContext
415
+ sensors={sensors}
416
+ collisionDetection={closestCenter}
417
+ onDragEnd={handleDragEnd}
418
+ modifiers={[restrictToVerticalAxis]}
419
+ >
420
+ <Table>
421
+ <TableHeader className="bg-muted/50">
422
+ {table.getHeaderGroups().map(headerGroup => (
423
+ <TableRow key={headerGroup.id}>
424
+ {headerGroup.headers.map(header => {
425
+ return (
426
+ <TableHead key={header.id}>
427
+ {header.isPlaceholder
428
+ ? null
429
+ : flexRender(
430
+ header.column.columnDef.header,
431
+ header.getContext(),
432
+ )}
433
+ </TableHead>
434
+ );
435
+ })}
436
+ </TableRow>
437
+ ))}
438
+ </TableHeader>
439
+ <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
440
+ <TableBody>
441
+ {isLoading && !localData?.length ? (
442
+ Array.from({ length: Math.min(pagination.pageSize, 100) }).map((_, index) => (
443
+ <TableRow
444
+ key={`skeleton-${index}`}
445
+ className="animate-in fade-in duration-100"
446
+ >
447
+ {!isDragDisabled && (
448
+ <TableCell className="w-[40px] h-12">
449
+ <Skeleton className="h-4 w-4" />
450
+ </TableCell>
451
+ )}
452
+ {Array.from({ length: visibleColumnCount }).map((_, cellIndex) => (
453
+ <TableCell
454
+ key={`skeleton-cell-${index}-${cellIndex}`}
455
+ className="h-12"
456
+ >
457
+ <Skeleton className="h-4 my-2 w-full" />
458
+ </TableCell>
459
+ ))}
460
+ </TableRow>
461
+ ))
462
+ ) : table.getRowModel().rows?.length ? (
463
+ (() => {
464
+ const isDraggableEnabled = onReorder && !isDragDisabled;
465
+ const rows = table.getRowModel().rows;
466
+
467
+ if (isDraggableEnabled) {
468
+ return rows.map(row => (
469
+ <DraggableRow key={`${row.id}-${componentId}`} row={row} isDragDisabled={isDragDisabled} />
470
+ ));
471
+ }
472
+
473
+ return rows.map(row => (
474
+ <TableRow
475
+ key={row.id}
476
+ data-state={row.getIsSelected() && 'selected'}
477
+ className="animate-in fade-in duration-100"
478
+ >
479
+ {row.getVisibleCells().map(cell => (
480
+ <TableCell key={cell.id} className="h-12">
481
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
482
+ </TableCell>
483
+ ))}
484
+ </TableRow>
485
+ ));
486
+ })()
487
+ ) : (
488
+ <TableRow className="animate-in fade-in duration-100">
339
489
  <TableCell
340
- key={`skeleton-cell-${index}-${cellIndex}`}
341
- className="h-12"
490
+ colSpan={columnsWithOptionalDragHandle.length + (isDragDisabled ? 0 : 1)}
491
+ className="h-24 text-center"
342
492
  >
343
- <Skeleton className="h-4 my-2 w-full" />
493
+ <Trans>No results</Trans>
344
494
  </TableCell>
345
- ))}
346
- </TableRow>
347
- ))
348
- ) : table.getRowModel().rows?.length ? (
349
- table.getRowModel().rows.map(row => (
350
- <TableRow
351
- key={row.id}
352
- data-state={row.getIsSelected() && 'selected'}
353
- className="animate-in fade-in duration-100"
354
- >
355
- {row.getVisibleCells().map(cell => (
356
- <TableCell key={cell.id} className="h-12">
357
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
358
- </TableCell>
359
- ))}
360
- </TableRow>
361
- ))
362
- ) : (
363
- <TableRow className="animate-in fade-in duration-100">
364
- <TableCell colSpan={columns.length} className="h-24 text-center">
365
- <Trans>No results</Trans>
366
- </TableCell>
367
- </TableRow>
368
- )}
369
- {children}
370
- </TableBody>
371
- </Table>
495
+ </TableRow>
496
+ )}
497
+ {children}
498
+ </TableBody>
499
+ </SortableContext>
500
+ </Table>
501
+ </DndContext>
372
502
  <DataTableBulkActions bulkActions={bulkActions ?? []} table={table} />
373
503
  </div>
374
504
  {onPageChange && totalItems != null && <DataTablePagination table={table} />}
@@ -18,6 +18,7 @@ import { useChannel } from '@/vdb/hooks/use-channel.js';
18
18
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
19
19
  import { usePermissions } from '@/vdb/hooks/use-permissions.js';
20
20
  import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
21
+ import { globalLanguageCodes } from '@/vdb/utils/global-languages.js';
21
22
  import { Trans } from '@lingui/react/macro';
22
23
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
23
24
  import { AlertCircle, Lock } from 'lucide-react';
@@ -68,36 +69,12 @@ const updateChannelDocument = graphql(`
68
69
  }
69
70
  `);
70
71
 
71
- // All possible language codes for global settings - includes more than what might be globally enabled
72
- const ALL_LANGUAGE_CODES = [
73
- 'en',
74
- 'es',
75
- 'fr',
76
- 'de',
77
- 'it',
78
- 'pt',
79
- 'nl',
80
- 'pl',
81
- 'ru',
82
- 'ja',
83
- 'zh',
84
- 'ko',
85
- 'ar',
86
- 'hi',
87
- 'sv',
88
- 'da',
89
- 'nb',
90
- 'nn',
91
- 'fi',
92
- ];
93
-
94
72
  interface ManageLanguagesDialogProps {
95
73
  open: boolean;
96
74
  onClose: () => void;
97
75
  }
98
76
 
99
77
  export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogProps) {
100
- const { formatLanguageName } = useLocalFormat();
101
78
  const { activeChannel } = useChannel();
102
79
  const { hasPermissions } = usePermissions();
103
80
  const queryClient = useQueryClient();
@@ -309,7 +286,7 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
309
286
  value={globalLanguages}
310
287
  onChange={handleGlobalLanguagesChange}
311
288
  multiple={true}
312
- availableLanguageCodes={ALL_LANGUAGE_CODES}
289
+ availableLanguageCodes={globalLanguageCodes}
313
290
  />
314
291
  </div>
315
292
  <p className="text-xs text-muted-foreground">