@wilshop/dashboard 3.5.6 → 3.5.7

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 (133) hide show
  1. package/dist/plugin/dashboard.plugin.d.ts +1 -1
  2. package/dist/plugin/dashboard.plugin.js +1 -1
  3. package/dist/vite/utils/compiler.js +50 -24
  4. package/dist/vite/utils/path-transformer.d.ts +20 -0
  5. package/dist/vite/utils/path-transformer.js +116 -0
  6. package/dist/vite/utils/plugin-discovery.js +3 -2
  7. package/dist/vite/utils/ui-config.js +15 -1
  8. package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
  9. package/dist/vite/vite-plugin-lingui-babel.js +90 -8
  10. package/dist/vite/vite-plugin-translations.js +2 -2
  11. package/dist/vite/vite-plugin-ui-config.d.ts +31 -0
  12. package/package.json +10 -6
  13. package/src/app/common/delete-bulk-action.tsx +1 -1
  14. package/src/app/common/duplicate-bulk-action.tsx +1 -1
  15. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -3
  16. package/src/app/routes/_authenticated/_collections/collections.tsx +169 -48
  17. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +36 -5
  18. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +1 -1
  19. package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +7 -1
  20. package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +31 -29
  21. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
  22. package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
  23. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
  24. package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
  25. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +8 -5
  26. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +79 -54
  27. package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +43 -3
  28. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +19 -3
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +1 -0
  30. package/src/app/routes/_authenticated/_orders/components/refund-order-dialog.tsx +372 -0
  31. package/src/app/routes/_authenticated/_orders/hooks/use-refund-order.ts +345 -0
  32. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +41 -0
  33. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
  34. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +51 -0
  35. package/src/app/routes/_authenticated/_orders/utils/refund-utils.ts +100 -0
  36. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +1 -1
  37. package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +4 -1
  38. package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +7 -1
  39. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +18 -2
  40. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +1 -1
  41. package/src/app/routes/_authenticated/_product-variants/components/variant-price-detail.tsx +6 -2
  42. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
  43. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
  44. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +1 -1
  45. package/src/app/routes/_authenticated/_profile/profile.graphql.ts +7 -0
  46. package/src/app/routes/_authenticated/_profile/profile.tsx +25 -1
  47. package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +7 -1
  48. package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +7 -1
  49. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +18 -2
  50. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +7 -1
  51. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +4 -1
  52. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +14 -2
  53. package/src/i18n/common-strings.ts +7 -0
  54. package/src/i18n/locales/ar.po +669 -399
  55. package/src/i18n/locales/bg.po +1889 -46
  56. package/src/i18n/locales/cs.po +676 -406
  57. package/src/i18n/locales/de.po +676 -406
  58. package/src/i18n/locales/en.po +669 -399
  59. package/src/i18n/locales/es.po +676 -406
  60. package/src/i18n/locales/fa.po +676 -406
  61. package/src/i18n/locales/fr.po +676 -406
  62. package/src/i18n/locales/he.po +676 -406
  63. package/src/i18n/locales/hr.po +676 -406
  64. package/src/i18n/locales/it.po +676 -406
  65. package/src/i18n/locales/ja.po +676 -406
  66. package/src/i18n/locales/nb.po +676 -406
  67. package/src/i18n/locales/ne.po +676 -406
  68. package/src/i18n/locales/pl.po +676 -406
  69. package/src/i18n/locales/pt_BR.po +676 -406
  70. package/src/i18n/locales/pt_PT.po +676 -406
  71. package/src/i18n/locales/ru.po +676 -406
  72. package/src/i18n/locales/sv.po +676 -406
  73. package/src/i18n/locales/tr.po +676 -406
  74. package/src/i18n/locales/uk.po +676 -406
  75. package/src/i18n/locales/zh_Hans.po +676 -406
  76. package/src/i18n/locales/zh_Hant.po +676 -406
  77. package/src/lib/components/data-input/facet-value-input.tsx +2 -2
  78. package/src/lib/components/data-input/index.ts +1 -0
  79. package/src/lib/components/data-input/select-with-options.tsx +23 -7
  80. package/src/lib/components/data-input/struct-form-input.tsx +53 -21
  81. package/src/lib/components/data-input/text-input.tsx +1 -1
  82. package/src/lib/components/data-table/data-table-bulk-actions.tsx +2 -1
  83. package/src/lib/components/data-table/data-table-context.tsx +2 -10
  84. package/src/lib/components/data-table/data-table-utils.ts +34 -12
  85. package/src/lib/components/data-table/data-table.tsx +68 -30
  86. package/src/lib/components/data-table/global-views-bar.tsx +1 -1
  87. package/src/lib/components/data-table/my-views-button.tsx +1 -1
  88. package/src/lib/components/data-table/save-view-button.tsx +1 -1
  89. package/src/lib/components/data-table/use-generated-columns.tsx +9 -2
  90. package/src/lib/components/data-table/views-sheet.tsx +1 -1
  91. package/src/lib/components/layout/channel-switcher.tsx +16 -17
  92. package/src/lib/components/layout/manage-languages-dialog.tsx +1 -1
  93. package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +1 -1
  94. package/src/lib/components/shared/configurable-operation-input.tsx +23 -0
  95. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +45 -0
  96. package/src/lib/components/shared/configurable-operation-selector.tsx +5 -0
  97. package/src/lib/components/shared/paginated-list-context.ts +10 -0
  98. package/src/lib/components/shared/paginated-list-data-table.tsx +6 -32
  99. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +1 -1
  100. package/src/lib/components/ui/alert.tsx +2 -0
  101. package/src/lib/constants.ts +7 -319
  102. package/src/lib/framework/dashboard-widget/base-widget.tsx +3 -12
  103. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +1 -1
  104. package/src/lib/framework/dashboard-widget/metrics-widget/chart.tsx +1 -1
  105. package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +1 -1
  106. package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +1 -1
  107. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +2 -20
  108. package/src/lib/framework/extension-api/input-component-extensions.tsx +4 -0
  109. package/src/lib/framework/form-engine/custom-form-component.tsx +13 -3
  110. package/src/lib/framework/form-engine/form-engine-types.ts +3 -5
  111. package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
  112. package/src/lib/framework/form-engine/use-generated-form.tsx +6 -2
  113. package/src/lib/framework/form-engine/utils.spec.ts +129 -2
  114. package/src/lib/framework/form-engine/utils.ts +36 -9
  115. package/src/lib/framework/form-engine/value-transformers.ts +6 -0
  116. package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
  117. package/src/lib/framework/page/detail-page.tsx +22 -37
  118. package/src/lib/framework/page/list-page.stories.tsx +41 -2
  119. package/src/lib/framework/page/list-page.tsx +8 -0
  120. package/src/lib/graphql/graphql-env.d.ts +33 -16
  121. package/src/lib/graphql/schema-enums.ts +13 -0
  122. package/src/lib/hooks/use-alerts-context.ts +10 -0
  123. package/src/lib/hooks/use-alerts.ts +1 -1
  124. package/src/lib/hooks/use-data-table-context.ts +11 -0
  125. package/src/lib/hooks/use-dynamic-translations.ts +7 -0
  126. package/src/lib/hooks/use-job-queue-polling.ts +160 -0
  127. package/src/lib/hooks/use-paginated-list.ts +28 -0
  128. package/src/lib/hooks/use-widget-dimensions.ts +12 -0
  129. package/src/lib/hooks/use-widget-filters.ts +21 -0
  130. package/src/lib/index.ts +12 -0
  131. package/src/lib/providers/alerts-provider.tsx +3 -11
  132. package/src/lib/virtual.d.ts +5 -0
  133. package/src/lib/utils/global-languages.ts +0 -268
@@ -41,49 +41,124 @@ export const Route = createFileRoute('/_authenticated/_collections/collections')
41
41
 
42
42
  type Collection = ResultOf<typeof collectionListDocument>['collections']['items'][number];
43
43
 
44
+ const CHILDREN_PAGE_SIZE = 20;
45
+
46
+ type LoadMoreRow = {
47
+ _isLoadMore: true;
48
+ _parentId: string;
49
+ _totalItems: number;
50
+ _loadedItems: number;
51
+ id: string;
52
+ breadcrumbs: { id: string; name: string; slug: string }[];
53
+ };
54
+
55
+ type CollectionOrLoadMore = Collection | LoadMoreRow;
56
+
57
+ function isLoadMoreRow(row: CollectionOrLoadMore): row is LoadMoreRow {
58
+ return '_isLoadMore' in row && row._isLoadMore === true;
59
+ }
60
+
44
61
  function CollectionListPage() {
45
62
  const { t } = useLingui();
46
63
  const queryClient = useQueryClient();
47
64
  const [expanded, setExpanded] = useState<ExpandedState>({});
48
65
  const [searchTerm, setSearchTerm] = useState<string>('');
66
+ const [accumulatedChildren, setAccumulatedChildren] = useState<
67
+ Record<string, { items: Collection[]; totalItems: number }>
68
+ >({});
69
+ const [nextPageToFetch, setNextPageToFetch] = useState<Record<string, number>>({});
49
70
 
50
- const childrenQueries = useQueries({
51
- queries: Object.entries(expanded).map(([collectionId, isExpanded]) => {
52
- return {
53
- queryKey: ['childCollections', collectionId],
54
- queryFn: () =>
55
- api.query(collectionListDocument, {
56
- options: {
57
- filter: {
58
- parentId: { eq: collectionId },
71
+ useQueries({
72
+ queries: expanded === true ? [] : Object.entries(expanded)
73
+ .filter(([collectionId]) => !accumulatedChildren[collectionId])
74
+ .map(([collectionId]) => {
75
+ return {
76
+ queryKey: ['childCollections', collectionId, 'page', 0],
77
+ queryFn: async () => {
78
+ const result = await api.query(collectionListDocument, {
79
+ options: {
80
+ filter: {
81
+ parentId: { eq: collectionId },
82
+ },
83
+ take: CHILDREN_PAGE_SIZE,
84
+ skip: 0,
85
+ },
86
+ });
87
+ setAccumulatedChildren(prev => ({
88
+ ...prev,
89
+ [collectionId]: {
90
+ items: result.collections.items,
91
+ totalItems: result.collections.totalItems,
59
92
  },
60
- },
61
- }),
62
- staleTime: 1000 * 60 * 5,
63
- } satisfies FetchQueryOptions;
64
- }),
93
+ }));
94
+ return result;
95
+ },
96
+ staleTime: 1000 * 60 * 5,
97
+ } satisfies FetchQueryOptions;
98
+ }),
65
99
  });
66
100
 
67
- const childCollectionsByParentId = childrenQueries.reduce(
68
- (acc, query, index) => {
69
- const collectionId = Object.keys(expanded)[index];
70
- if (query.data) {
71
- acc[collectionId] = query.data.collections.items;
72
- }
73
- return acc;
74
- },
75
- {} as Record<string, any[]>,
76
- );
101
+ useQueries({
102
+ queries: Object.entries(nextPageToFetch)
103
+ .filter(([_, page]) => page > 0)
104
+ .map(([collectionId, page]) => {
105
+ return {
106
+ queryKey: ['childCollections', collectionId, 'page', page],
107
+ queryFn: async () => {
108
+ const result = await api.query(collectionListDocument, {
109
+ options: {
110
+ filter: {
111
+ parentId: { eq: collectionId },
112
+ },
113
+ take: CHILDREN_PAGE_SIZE,
114
+ skip: page * CHILDREN_PAGE_SIZE,
115
+ },
116
+ });
117
+ setAccumulatedChildren(prev => {
118
+ const existing = prev[collectionId];
119
+ if (!existing) return prev;
120
+ return {
121
+ ...prev,
122
+ [collectionId]: {
123
+ items: [...existing.items, ...result.collections.items],
124
+ totalItems: result.collections.totalItems,
125
+ },
126
+ };
127
+ });
128
+ setNextPageToFetch(prev => {
129
+ const { [collectionId]: _, ...rest } = prev;
130
+ return rest;
131
+ });
132
+ return result;
133
+ },
134
+ staleTime: 1000 * 60 * 5,
135
+ } satisfies FetchQueryOptions;
136
+ }),
137
+ });
77
138
 
78
- const addSubCollections = (data: Collection[]) => {
79
- const allRows = [] as Collection[];
139
+ const addSubCollections = (data: Collection[]): CollectionOrLoadMore[] => {
140
+ const allRows: CollectionOrLoadMore[] = [];
80
141
  const addSubRows = (row: Collection) => {
81
- const subRows = childCollectionsByParentId[row.id] || [];
82
- if (subRows.length) {
83
- for (const subRow of subRows) {
142
+ const isExpanded = expanded === true || (typeof expanded === 'object' && expanded[row.id]);
143
+ if (!isExpanded) {
144
+ return;
145
+ }
146
+ const childData = accumulatedChildren[row.id];
147
+ if (childData?.items.length) {
148
+ for (const subRow of childData.items) {
84
149
  allRows.push(subRow);
85
150
  addSubRows(subRow);
86
151
  }
152
+ if (childData.totalItems > childData.items.length) {
153
+ allRows.push({
154
+ _isLoadMore: true,
155
+ _parentId: row.id,
156
+ _totalItems: childData.totalItems,
157
+ _loadedItems: childData.items.length,
158
+ id: `load-more-${row.id}`,
159
+ breadcrumbs: [...(row.breadcrumbs || []), { id: row.id, name: row.name, slug: row.slug }],
160
+ });
161
+ }
87
162
  }
88
163
  };
89
164
  data.forEach(row => {
@@ -93,37 +168,56 @@ function CollectionListPage() {
93
168
  return allRows;
94
169
  };
95
170
 
171
+ const handleLoadMoreChildren = (parentId: string) => {
172
+ const currentItems = accumulatedChildren[parentId]?.items.length ?? 0;
173
+ const nextPage = Math.floor(currentItems / CHILDREN_PAGE_SIZE);
174
+ setNextPageToFetch(prev => ({
175
+ ...prev,
176
+ [parentId]: nextPage,
177
+ }));
178
+ };
179
+
96
180
  const handleReorder = async (oldIndex: number, newIndex: number, item: Collection, allItems?: Collection[]) => {
181
+ if (isLoadMoreRow(item as CollectionOrLoadMore)) {
182
+ return;
183
+ }
97
184
  try {
98
- const items = allItems || [];
185
+ const rawItems = (allItems || []) as CollectionOrLoadMore[];
186
+
187
+ // Filter out LoadMoreRows - they shouldn't affect position calculations
188
+ const items = rawItems.filter((i): i is Collection => !isLoadMoreRow(i));
189
+
190
+ // Recalculate indices in the filtered array
191
+ const adjustedOldIndex = items.findIndex(i => i.id === item.id);
192
+ const targetItem = rawItems[newIndex];
193
+ const adjustedNewIndex = isLoadMoreRow(targetItem)
194
+ ? items.findIndex(i => i.id === targetItem._parentId)
195
+ : items.findIndex(i => i.id === (targetItem as Collection).id);
196
+
99
197
  const sourceParentId = getItemParentId(item);
100
198
 
101
199
  if (!sourceParentId) {
102
200
  throw new Error('Unable to determine parent collection ID');
103
201
  }
104
202
 
105
- // Calculate target position (parent and index)
106
203
  const { targetParentId, adjustedIndex: initialIndex } = calculateDragTargetPosition({
107
204
  item,
108
- oldIndex,
109
- newIndex,
205
+ oldIndex: adjustedOldIndex,
206
+ newIndex: adjustedNewIndex,
110
207
  items,
111
208
  sourceParentId,
112
209
  expanded,
113
210
  });
114
211
 
115
- // Validate no circular references when moving to different parent
116
212
  if (targetParentId !== sourceParentId && isCircularReference(item, targetParentId, items)) {
117
213
  toast.error(t`Cannot move a collection into its own descendant`);
118
214
  throw new Error('Circular reference detected');
119
215
  }
120
216
 
121
- // Calculate final index (adjust for same-parent moves)
122
217
  const adjustedIndex = targetParentId === sourceParentId
123
- ? calculateSiblingIndex({ item, oldIndex, newIndex, items, parentId: sourceParentId })
218
+ ? calculateSiblingIndex({ item, oldIndex: adjustedOldIndex, newIndex: adjustedNewIndex, items, parentId: sourceParentId })
124
219
  : initialIndex;
125
220
 
126
- // Perform the move
127
221
  await api.mutate(moveCollectionDocument, {
128
222
  input: {
129
223
  collectionId: item.id,
@@ -132,7 +226,15 @@ function CollectionListPage() {
132
226
  },
133
227
  });
134
228
 
135
- // Invalidate queries and show success message
229
+ setAccumulatedChildren(prev => {
230
+ const newState = { ...prev };
231
+ delete newState[sourceParentId];
232
+ if (targetParentId !== sourceParentId) {
233
+ delete newState[targetParentId];
234
+ }
235
+ return newState;
236
+ });
237
+
136
238
  const queriesToInvalidate = [
137
239
  queryClient.invalidateQueries({ queryKey: ['childCollections', sourceParentId] }),
138
240
  queryClient.invalidateQueries({ queryKey: ['PaginatedListDataTable'] }),
@@ -178,11 +280,12 @@ function CollectionListPage() {
178
280
  dependencies: ['children', 'breadcrumbs'],
179
281
  },
180
282
  cell: ({ row }) => {
283
+ const original = row.original as Collection;
181
284
  const isExpanded = row.getIsExpanded();
182
- const hasChildren = !!row.original.children?.length;
285
+ const hasChildren = !!original.children?.length;
183
286
  return (
184
287
  <div
185
- style={{ marginLeft: (row.original.breadcrumbs?.length - 2) * 20 + 'px' }}
288
+ style={{ marginLeft: (original.breadcrumbs?.length - 2) * 20 + 'px' }}
186
289
  className="flex gap-2 items-center"
187
290
  >
188
291
  <Button
@@ -194,7 +297,7 @@ function CollectionListPage() {
194
297
  >
195
298
  {isExpanded ? <FolderOpen /> : <Folder />}
196
299
  </Button>
197
- <DetailPageButton id={row.original.id} label={row.original.name} />
300
+ <DetailPageButton id={original.id} label={original.name} />
198
301
  </div>
199
302
  );
200
303
  },
@@ -218,7 +321,7 @@ function CollectionListPage() {
218
321
  );
219
322
  },
220
323
  },
221
- productVariants: {
324
+ productVariantCount: {
222
325
  header: () => <Trans>Contents</Trans>,
223
326
  cell: ({ row }) => {
224
327
  return (
@@ -226,7 +329,7 @@ function CollectionListPage() {
226
329
  collectionId={row.original.id}
227
330
  collectionName={row.original.name}
228
331
  >
229
- <Trans>{row.original.productVariants?.totalItems} variants</Trans>
332
+ <Trans>{row.original.productVariantCount} variants</Trans>
230
333
  </CollectionContentsSheet>
231
334
  );
232
335
  },
@@ -257,7 +360,7 @@ function CollectionListPage() {
257
360
  'name',
258
361
  'slug',
259
362
  'breadcrumbs',
260
- 'productVariants',
363
+ 'productVariantCount',
261
364
  ]}
262
365
  transformData={data => {
263
366
  return addSubCollections(data);
@@ -270,12 +373,30 @@ function CollectionListPage() {
270
373
  options.onExpandedChange = setExpanded;
271
374
  options.getExpandedRowModel = getExpandedRowModel();
272
375
  options.getRowCanExpand = () => true;
273
- options.getRowId = row => {
274
- return row.id;
275
- };
376
+ options.getRowId = row => row.id;
377
+ options.enableRowSelection = row => !isLoadMoreRow(row.original);
276
378
  options.meta = {
277
379
  ...options.meta,
278
380
  resetExpanded: () => setExpanded({}),
381
+ isUtilityRow: (row: { original: CollectionOrLoadMore }) => isLoadMoreRow(row.original),
382
+ renderUtilityRow: (row: { original: CollectionOrLoadMore }) => {
383
+ const original = row.original as LoadMoreRow;
384
+ const remaining = original._totalItems - original._loadedItems;
385
+ return (
386
+ <div
387
+ style={{ paddingLeft: (original.breadcrumbs?.length - 1) * 20 + 'px' }}
388
+ className="flex justify-center py-2"
389
+ >
390
+ <Button
391
+ size="sm"
392
+ variant="outline"
393
+ onClick={() => handleLoadMoreChildren(original._parentId)}
394
+ >
395
+ <Trans>Load {Math.min(remaining, CHILDREN_PAGE_SIZE)} more ({remaining} remaining)</Trans>
396
+ </Button>
397
+ </div>
398
+ );
399
+ },
279
400
  };
280
401
  return options;
281
402
  }}
@@ -319,7 +440,7 @@ function CollectionListPage() {
319
440
  },
320
441
  ]}
321
442
  onReorder={handleReorder}
322
- disableDragAndDrop={!!searchTerm} // Disable dragging while searching
443
+ disableDragAndDrop={!!searchTerm}
323
444
  >
324
445
  <PageActionBarRight>
325
446
  <PermissionGuard requires={['CreateCollection', 'CreateCatalog']}>
@@ -22,8 +22,11 @@ import {
22
22
  } from '@/vdb/framework/layout-engine/page-layout.js';
23
23
  import { detailPageRouteLoader } from '@/vdb/framework/page/detail-page-route-loader.js';
24
24
  import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
25
+ import { useJobQueuePolling } from '@/vdb/hooks/use-job-queue-polling.js';
25
26
  import { Trans, useLingui } from '@lingui/react/macro';
27
+ import { useQueryClient } from '@tanstack/react-query';
26
28
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
29
+ import { useState } from 'react';
27
30
  import { toast } from 'sonner';
28
31
  import {
29
32
  collectionDetailDocument,
@@ -54,6 +57,12 @@ function CollectionDetailPage() {
54
57
  const navigate = useNavigate();
55
58
  const creatingNewEntity = params.id === NEW_ENTITY_PATH;
56
59
  const { t } = useLingui();
60
+ const queryClient = useQueryClient();
61
+
62
+ const { isPolling: pendingFilterApplication, startPolling } = useJobQueuePolling(
63
+ 'apply-collection-filters',
64
+ () => queryClient.invalidateQueries({ queryKey: ['PaginatedListDataTable'] }),
65
+ );
57
66
 
58
67
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
59
68
  pageId,
@@ -79,6 +88,7 @@ function CollectionDetailPage() {
79
88
  name: translation.name,
80
89
  slug: translation.slug,
81
90
  description: translation.description,
91
+ customFields: (translation as any).customFields,
82
92
  })),
83
93
  filters: entity.filters.map(f => ({
84
94
  code: f.code,
@@ -90,12 +100,20 @@ function CollectionDetailPage() {
90
100
  },
91
101
  params: { id: params.id },
92
102
  onSuccess: async data => {
103
+ const filtersWereDirty =
104
+ form.getFieldState('inheritFilters').isDirty || form.getFieldState('filters').isDirty;
93
105
  toast(
94
106
  creatingNewEntity ? t`Successfully created collection` : t`Successfully updated collection`,
95
107
  );
96
108
  resetForm();
109
+ if (filtersWereDirty) {
110
+ startPolling();
111
+ }
97
112
  if (creatingNewEntity) {
98
- await navigate({ to: `../$id`, params: { id: data.id } });
113
+ await navigate({
114
+ to: `../$id`,
115
+ params: { id: data.id },
116
+ });
99
117
  }
100
118
  },
101
119
  onError: err => {
@@ -106,11 +124,15 @@ function CollectionDetailPage() {
106
124
  });
107
125
 
108
126
  const shouldPreviewContents =
109
- form.getFieldState('inheritFilters').isDirty || form.getFieldState('filters').isDirty;
127
+ form.getFieldState('inheritFilters').isDirty ||
128
+ form.getFieldState('filters').isDirty ||
129
+ pendingFilterApplication;
110
130
 
111
131
  const currentFiltersValue = form.watch('filters');
112
132
  const currentInheritFiltersValue = form.watch('inheritFilters');
113
133
 
134
+ const [filtersArgsValid, setFiltersArgsValid] = useState(true);
135
+
114
136
  return (
115
137
  <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
116
138
  <PageTitle>{creatingNewEntity ? <Trans>New collection</Trans> : (entity?.name ?? '')}</PageTitle>
@@ -119,7 +141,12 @@ function CollectionDetailPage() {
119
141
  <PermissionGuard requires={['UpdateCollection', 'UpdateCatalog']}>
120
142
  <Button
121
143
  type="submit"
122
- disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
144
+ disabled={
145
+ !form.formState.isDirty ||
146
+ !form.formState.isValid ||
147
+ isPending ||
148
+ !filtersArgsValid
149
+ }
123
150
  >
124
151
  {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
125
152
  </Button>
@@ -188,7 +215,11 @@ function CollectionDetailPage() {
188
215
  control={form.control}
189
216
  name="filters"
190
217
  render={({ field }) => (
191
- <CollectionFiltersSelector value={field.value ?? []} onChange={field.onChange} />
218
+ <CollectionFiltersSelector
219
+ value={field.value ?? []}
220
+ onChange={field.onChange}
221
+ onValidityChange={setFiltersArgsValid}
222
+ />
192
223
  )}
193
224
  />
194
225
  </PageBlock>
@@ -220,7 +251,7 @@ function CollectionDetailPage() {
220
251
  </FormItem>
221
252
  </PageBlock>
222
253
  <PageBlock column="main" blockId="contents" title={<Trans>Contents</Trans>}>
223
- {shouldPreviewContents || creatingNewEntity ? (
254
+ {pendingFilterApplication || shouldPreviewContents || creatingNewEntity ? (
224
255
  <CollectionContentsPreviewTable
225
256
  parentId={entity?.parent?.id}
226
257
  filters={currentFiltersValue ?? []}
@@ -4,7 +4,7 @@ import { useState } from 'react';
4
4
 
5
5
  import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
6
6
  import { AssignToChannelBulkAction } from '@/vdb/components/shared/assign-to-channel-bulk-action.js';
7
- import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
7
+ import { usePaginatedList } from '@/vdb/hooks/use-paginated-list.js';
8
8
  import { RemoveFromChannelBulkAction } from '@/vdb/components/shared/remove-from-channel-bulk-action.js';
9
9
  import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
10
10
  import { api } from '@/vdb/graphql/api.js';
@@ -5,9 +5,14 @@ import { getCollectionFiltersQueryOptions } from '../collections.graphql.js';
5
5
  export interface CollectionFiltersSelectorProps {
6
6
  value: ConfigurableOperationInputType[];
7
7
  onChange: (filters: ConfigurableOperationInputType[]) => void;
8
+ onValidityChange?: (isValid: boolean) => void;
8
9
  }
9
10
 
10
- export function CollectionFiltersSelector({ value, onChange }: Readonly<CollectionFiltersSelectorProps>) {
11
+ export function CollectionFiltersSelector({
12
+ value,
13
+ onChange,
14
+ onValidityChange,
15
+ }: Readonly<CollectionFiltersSelectorProps>) {
11
16
  return (
12
17
  <div className="mt-4">
13
18
  <ConfigurableOperationMultiSelector
@@ -18,6 +23,7 @@ export function CollectionFiltersSelector({ value, onChange }: Readonly<Collecti
18
23
  dataPath="collectionFilters"
19
24
  buttonText="Add collection filter"
20
25
  showEnhancedDropdown={false}
26
+ onValidityChange={onValidityChange}
21
27
  />
22
28
  </div>
23
29
  );
@@ -141,36 +141,38 @@ export function CustomerPasswordResetVerifiedComponent(props: Readonly<HistoryEn
141
141
  );
142
142
  }
143
143
 
144
- export function CustomerEmailUpdateComponent({ entry }: Readonly<HistoryEntryProps>) {
145
- const { oldEmailAddress, newEmailAddress } = entry.data || {};
144
+ export function CustomerEmailUpdateComponent(props: Readonly<HistoryEntryProps>) {
145
+ const { oldEmailAddress, newEmailAddress } = props.entry.data || {};
146
146
 
147
147
  return (
148
- <div className="space-y-2">
149
- {(oldEmailAddress || newEmailAddress) && (
150
- <details className="text-xs">
151
- <summary className="cursor-pointer text-muted-foreground hover:text-foreground">
152
- <Trans>View details</Trans>
153
- </summary>
154
- <div className="mt-2 space-y-1">
155
- {oldEmailAddress && (
156
- <div>
157
- <span className="font-medium">
158
- <Trans>Old email:</Trans>
159
- </span>{' '}
160
- {oldEmailAddress}
161
- </div>
162
- )}
163
- {newEmailAddress && (
164
- <div>
165
- <span className="font-medium">
166
- <Trans>New email:</Trans>
167
- </span>{' '}
168
- {newEmailAddress}
169
- </div>
170
- )}
171
- </div>
172
- </details>
173
- )}
174
- </div>
148
+ <HistoryEntry {...props}>
149
+ <div className="space-y-2">
150
+ {(oldEmailAddress || newEmailAddress) && (
151
+ <details className="text-xs">
152
+ <summary className="cursor-pointer text-muted-foreground hover:text-foreground">
153
+ <Trans>View details</Trans>
154
+ </summary>
155
+ <div className="mt-2 space-y-1">
156
+ {oldEmailAddress && (
157
+ <div>
158
+ <span className="font-medium">
159
+ <Trans>Old email:</Trans>
160
+ </span>{' '}
161
+ {oldEmailAddress}
162
+ </div>
163
+ )}
164
+ {newEmailAddress && (
165
+ <div>
166
+ <span className="font-medium">
167
+ <Trans>New email:</Trans>
168
+ </span>{' '}
169
+ {newEmailAddress}
170
+ </div>
171
+ )}
172
+ </div>
173
+ </details>
174
+ )}
175
+ </div>
176
+ </HistoryEntry>
175
177
  );
176
178
  }
@@ -10,6 +10,7 @@ export const customerListDocument = graphql(`
10
10
  firstName
11
11
  lastName
12
12
  emailAddress
13
+ phoneNumber
13
14
  groups {
14
15
  id
15
16
  name
@@ -30,6 +30,9 @@ function CustomerListPage() {
30
30
  emailAddress: {
31
31
  contains: searchTerm,
32
32
  },
33
+ phoneNumber: {
34
+ contains: searchTerm,
35
+ },
33
36
  };
34
37
  }}
35
38
  transformVariables={variables => {
@@ -23,7 +23,7 @@ import { Trans, useLingui } from '@lingui/react/macro';
23
23
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
24
24
  import { toast } from 'sonner';
25
25
  import { globalSettingsDocument, updateGlobalSettingsDocument } from './global-settings.graphql.js';
26
- import { globalLanguageCodes } from '@/vdb/utils/global-languages.js';
26
+ import { schemaLanguageCodes as globalLanguageCodes } from '@/vdb/graphql/schema-enums.js';
27
27
 
28
28
  const pageId = 'global-settings';
29
29
 
@@ -0,0 +1,48 @@
1
+ import { Alert, AlertDescription, AlertTitle } from '@/vdb/components/ui/alert.js';
2
+ import { Trans, useLingui } from '@lingui/react/macro';
3
+ import { AlertTriangle, CheckCircle } from 'lucide-react';
4
+
5
+ export type DraftOrderStatusProps = Readonly<{
6
+ hasCustomer: boolean;
7
+ hasLines: boolean;
8
+ hasShippingMethod: boolean;
9
+ isDraftState: boolean;
10
+ }>;
11
+
12
+ export function DraftOrderStatus({
13
+ hasCustomer,
14
+ hasLines,
15
+ hasShippingMethod,
16
+ isDraftState,
17
+ }: DraftOrderStatusProps) {
18
+ const { t } = useLingui();
19
+ const isCompleteDraftDisabled = !hasCustomer || !hasLines || !hasShippingMethod || !isDraftState;
20
+
21
+ let completeDraftDisabledReason: string | null = null;
22
+ if (!hasCustomer) {
23
+ completeDraftDisabledReason = t`Select a customer to continue`;
24
+ } else if (!hasLines) {
25
+ completeDraftDisabledReason = t`Add at least one item to the order`;
26
+ } else if (!hasShippingMethod) {
27
+ completeDraftDisabledReason = t`Set a shipping address and select a shipping method`;
28
+ } else if (!isDraftState) {
29
+ completeDraftDisabledReason = t`Only draft orders can be completed`;
30
+ }
31
+
32
+ const Icon = isCompleteDraftDisabled ? AlertTriangle : CheckCircle;
33
+ const title = isCompleteDraftDisabled ? (
34
+ <Trans>Order draft isn't ready to be completed</Trans>
35
+ ) : (
36
+ <Trans>Order draft is ready to be completed</Trans>
37
+ );
38
+
39
+ return (
40
+ <Alert variant={isCompleteDraftDisabled ? 'destructive' : 'default'}>
41
+ <Icon className={isCompleteDraftDisabled ? '' : 'stroke-success'} />
42
+ <AlertTitle className={isCompleteDraftDisabled ? '' : 'text-success'}>{title}</AlertTitle>
43
+ {completeDraftDisabledReason ? (
44
+ <AlertDescription>{completeDraftDisabledReason}</AlertDescription>
45
+ ) : null}
46
+ </Alert>
47
+ );
48
+ }