@vendure/dashboard 3.2.3 → 3.3.0

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 (123) hide show
  1. package/dist/plugin/utils/ast-utils.d.ts +10 -0
  2. package/dist/plugin/utils/ast-utils.js +96 -0
  3. package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
  4. package/dist/plugin/utils/ast-utils.spec.js +120 -0
  5. package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
  6. package/dist/plugin/utils/config-loader.js +325 -0
  7. package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
  8. package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +7 -1
  9. package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -3
  10. package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
  11. package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
  12. package/dist/plugin/vite-plugin-config-loader.js +18 -9
  13. package/dist/plugin/vite-plugin-config.js +4 -6
  14. package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
  15. package/dist/plugin/vite-plugin-gql-tada.js +2 -2
  16. package/dist/plugin/vite-plugin-ui-config.js +3 -2
  17. package/package.json +16 -11
  18. package/src/app/app-providers.tsx +9 -9
  19. package/src/app/main.tsx +1 -1
  20. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
  21. package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
  22. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
  23. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
  24. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
  25. package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
  26. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
  27. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
  28. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
  30. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
  31. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -2
  32. package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
  33. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
  34. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
  35. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
  36. package/src/app/routes/_authenticated/_products/products.tsx +1 -1
  37. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -0
  38. package/src/app/routes/_authenticated/_system/job-queue.tsx +7 -8
  39. package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +241 -0
  40. package/src/app/routes/_authenticated.tsx +12 -1
  41. package/src/app/styles.css +15 -0
  42. package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
  43. package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
  44. package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
  45. package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
  46. package/src/lib/components/data-table/data-table-types.ts +1 -0
  47. package/src/lib/components/data-table/data-table-view-options.tsx +73 -24
  48. package/src/lib/components/data-table/data-table.tsx +49 -44
  49. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
  50. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
  51. package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
  52. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
  53. package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
  54. package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
  55. package/src/lib/components/data-table/refresh-button.tsx +25 -0
  56. package/src/lib/components/layout/nav-user.tsx +20 -15
  57. package/src/lib/components/layout/prerelease-popup.tsx +1 -5
  58. package/src/lib/components/shared/alerts.tsx +19 -1
  59. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
  60. package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
  61. package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
  62. package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
  63. package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
  64. package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
  65. package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
  66. package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
  67. package/src/lib/components/shared/custom-fields-form.tsx +4 -3
  68. package/src/lib/components/shared/customer-selector.tsx +13 -14
  69. package/src/lib/components/shared/detail-page-button.tsx +2 -2
  70. package/src/lib/components/shared/entity-assets.tsx +3 -3
  71. package/src/lib/components/shared/error-page.tsx +2 -2
  72. package/src/lib/components/shared/navigation-confirmation.tsx +49 -0
  73. package/src/lib/components/shared/paginated-list-data-table.tsx +10 -1
  74. package/src/lib/components/shared/product-variant-selector.tsx +111 -0
  75. package/src/lib/components/shared/vendure-image.tsx +1 -1
  76. package/src/lib/components/ui/calendar.tsx +508 -63
  77. package/src/lib/framework/alert/alert-extensions.tsx +31 -0
  78. package/src/lib/framework/alert/alert-item.tsx +47 -0
  79. package/src/lib/framework/alert/alerts-indicator.tsx +23 -0
  80. package/src/lib/framework/alert/types.ts +13 -0
  81. package/src/lib/framework/dashboard-widget/base-widget.tsx +1 -0
  82. package/src/lib/framework/defaults.ts +34 -0
  83. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
  84. package/src/lib/framework/document-introspection/get-document-structure.ts +71 -13
  85. package/src/lib/framework/extension-api/define-dashboard-extension.ts +15 -5
  86. package/src/lib/framework/extension-api/extension-api-types.ts +81 -12
  87. package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
  88. package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
  89. package/src/lib/framework/layout-engine/page-layout.tsx +196 -35
  90. package/src/lib/framework/layout-engine/page-provider.tsx +10 -0
  91. package/src/lib/framework/page/detail-page.tsx +62 -9
  92. package/src/lib/framework/page/list-page.tsx +42 -4
  93. package/src/lib/framework/page/page-api.ts +1 -1
  94. package/src/lib/framework/page/use-detail-page.ts +82 -0
  95. package/src/lib/framework/registry/registry-types.ts +6 -2
  96. package/src/lib/graphql/fragments.tsx +8 -0
  97. package/src/lib/graphql/graphql-env.d.ts +25 -9
  98. package/src/lib/hooks/use-auth.tsx +13 -1
  99. package/src/lib/hooks/use-channel.ts +13 -0
  100. package/src/lib/hooks/use-local-format.ts +28 -1
  101. package/src/lib/hooks/use-page.tsx +2 -3
  102. package/src/lib/hooks/use-permissions.ts +13 -0
  103. package/src/lib/index.ts +7 -8
  104. package/src/lib/providers/auth.tsx +22 -9
  105. package/src/lib/providers/channel-provider.tsx +9 -1
  106. package/src/lib/providers/server-config.tsx +7 -1
  107. package/src/lib/providers/user-settings.tsx +24 -0
  108. package/vite/utils/ast-utils.spec.ts +128 -0
  109. package/vite/utils/ast-utils.ts +119 -0
  110. package/vite/utils/config-loader.ts +410 -0
  111. package/vite/{schema-generator.ts → utils/schema-generator.ts} +11 -6
  112. package/vite/{ui-config.ts → utils/ui-config.ts} +7 -3
  113. package/vite/vite-plugin-admin-api-schema.ts +2 -12
  114. package/vite/vite-plugin-config-loader.ts +25 -13
  115. package/vite/vite-plugin-config.ts +1 -0
  116. package/vite/vite-plugin-dashboard-metadata.ts +19 -15
  117. package/vite/vite-plugin-gql-tada.ts +2 -2
  118. package/vite/vite-plugin-ui-config.ts +3 -2
  119. package/dist/plugin/config-loader.js +0 -141
  120. package/src/lib/components/shared/asset-preview.tsx +0 -345
  121. package/src/lib/components/ui/avatar.tsx +0 -38
  122. package/vite/config-loader.ts +0 -181
  123. /package/dist/plugin/{ui-config.d.ts → utils/ui-config.d.ts} +0 -0
@@ -5,12 +5,16 @@ import { Form } from '@/components/ui/form.js';
5
5
  import { useCustomFieldConfig } from '@/hooks/use-custom-field-config.js';
6
6
  import { usePage } from '@/hooks/use-page.js';
7
7
  import { cn } from '@/lib/utils.js';
8
+ import { NavigationConfirmation } from '@/components/shared/navigation-confirmation.js';
8
9
  import { useMediaQuery } from '@uidotdev/usehooks';
9
- import React, { ComponentProps, createContext } from 'react';
10
+ import React, { ComponentProps } from 'react';
10
11
  import { Control, UseFormReturn } from 'react-hook-form';
12
+
11
13
  import { DashboardActionBarItem } from '../extension-api/extension-api-types.js';
14
+
12
15
  import { getDashboardActionBarItems, getDashboardPageBlocks } from './layout-extensions.js';
13
16
  import { LocationWrapper } from './location-wrapper.js';
17
+ import { PageContext, PageContextValue } from '@/framework/layout-engine/page-provider.js';
14
18
 
15
19
  export interface PageProps extends ComponentProps<'div'> {
16
20
  pageId?: string;
@@ -19,18 +23,34 @@ export interface PageProps extends ComponentProps<'div'> {
19
23
  submitHandler?: any;
20
24
  }
21
25
 
22
- export const PageProvider = createContext<PageContext | undefined>(undefined);
23
-
26
+ /**
27
+ * @description
28
+ * **Status: Developer Preview**
29
+ *
30
+ * This component should be used to wrap _all_ pages in the dashboard. It provides
31
+ * a consistent layout as well as a context for the slot-based PageBlock system.
32
+ *
33
+ * The typical hierarchy of a page is as follows:
34
+ * - `Page`
35
+ * - {@link PageTitle}
36
+ * - {@link PageActionBar}
37
+ * - {@link PageLayout}
38
+ *
39
+ * @docsCategory components
40
+ * @docsPage Page
41
+ * @docsWeight 0
42
+ * @since 3.3.0
43
+ */
24
44
  export function Page({ children, pageId, entity, form, submitHandler, ...props }: PageProps) {
25
45
  const childArray = React.Children.toArray(children);
26
46
 
27
47
  const pageTitle = childArray.find(child => React.isValidElement(child) && child.type === PageTitle);
28
48
  const pageActionBar = childArray.find(
29
- child => React.isValidElement(child) && child.type === PageActionBar,
49
+ child => isOfType(child, PageActionBar),
30
50
  );
31
51
 
32
52
  const pageContent = childArray.filter(
33
- child => React.isValidElement(child) && child.type !== PageTitle && child.type !== PageActionBar,
53
+ child => !isOfType(child, PageTitle) && !isOfType(child, PageActionBar),
34
54
  );
35
55
 
36
56
  const pageHeader = (
@@ -40,8 +60,50 @@ export function Page({ children, pageId, entity, form, submitHandler, ...props }
40
60
  </div>
41
61
  );
42
62
 
43
- const pageContentWithOptionalForm = form ? (
63
+ return (
64
+ <PageContext.Provider value={{ pageId, form, entity }}>
65
+ <PageContent
66
+ pageHeader={pageHeader}
67
+ pageContent={pageContent}
68
+ form={form}
69
+ submitHandler={submitHandler}
70
+ className={props.className}
71
+ {...props}
72
+ />
73
+ </PageContext.Provider>
74
+ );
75
+ }
76
+
77
+ function PageContent({ pageHeader, pageContent, form, submitHandler, ...props }: {
78
+ pageHeader: React.ReactNode;
79
+ pageContent: React.ReactNode;
80
+ form?: UseFormReturn<any>;
81
+ submitHandler?: any;
82
+ className?: string;
83
+ }) {
84
+ return (
85
+ <div className={cn('m-4', props.className)} {...props}>
86
+ <LocationWrapper>
87
+ <PageContentWithOptionalForm
88
+ pageHeader={pageHeader}
89
+ pageContent={pageContent}
90
+ form={form}
91
+ submitHandler={submitHandler}
92
+ />
93
+ </LocationWrapper>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export function PageContentWithOptionalForm({ form, pageHeader, pageContent, submitHandler }: {
99
+ form?: UseFormReturn<any>;
100
+ pageHeader: React.ReactNode
101
+ pageContent: React.ReactNode;
102
+ submitHandler?: any;
103
+ }) {
104
+ return form ? (
44
105
  <Form {...form}>
106
+ <NavigationConfirmation form={form} />
45
107
  <form onSubmit={submitHandler} className="space-y-4">
46
108
  {pageHeader}
47
109
  {pageContent}
@@ -53,18 +115,16 @@ export function Page({ children, pageId, entity, form, submitHandler, ...props }
53
115
  {pageContent}
54
116
  </div>
55
117
  );
56
-
57
- return (
58
- <PageProvider value={{ pageId, form, entity }}>
59
- <LocationWrapper>
60
- <div className={cn('m-4', props.className)} {...props}>
61
- {pageContentWithOptionalForm}
62
- </div>
63
- </LocationWrapper>
64
- </PageProvider>
65
- );
66
118
  }
67
119
 
120
+ /**
121
+ * @description
122
+ * **Status: Developer Preview**
123
+ *
124
+ * @docsCategory components
125
+ * @docsPage PageLayout
126
+ * @since 3.3.0
127
+ */
68
128
  export type PageLayoutProps = {
69
129
  children: React.ReactNode;
70
130
  className?: string;
@@ -83,6 +143,18 @@ function isPageBlock(child: unknown): child is React.ReactElement<PageBlockProps
83
143
  return hasColumn || hasBlockId;
84
144
  }
85
145
 
146
+ /**
147
+ * @description
148
+ * **Status: Developer Preview**
149
+ *
150
+ * This component governs the layout of the contents of a {@link Page} component.
151
+ * It should contain all the {@link PageBlock} components that are to be displayed on the page.
152
+ *
153
+ * @docsCategory components
154
+ * @docsPage PageLayout
155
+ * @docsWeight 0
156
+ * @since 3.3.0
157
+ */
86
158
  export function PageLayout({ children, className }: PageLayoutProps) {
87
159
  const page = usePage();
88
160
  const isDesktop = useMediaQuery('only screen and (min-width : 769px)');
@@ -108,7 +180,7 @@ export function PageLayout({ children, className }: PageLayoutProps) {
108
180
  if (childBlock) {
109
181
  const blockId =
110
182
  childBlock.props.blockId ??
111
- (childBlock.type === CustomFieldsPageBlock ? 'custom-fields' : undefined);
183
+ (isOfType(childBlock, CustomFieldsPageBlock) ? 'custom-fields' : undefined);
112
184
  const extensionBlock = extensionBlocks.find(block => block.location.position.blockId === blockId);
113
185
  if (extensionBlock) {
114
186
  const ExtensionBlock = (
@@ -134,7 +206,7 @@ export function PageLayout({ children, className }: PageLayoutProps) {
134
206
  }
135
207
 
136
208
  const fullWidthBlocks = finalChildArray.filter(
137
- child => isPageBlock(child) && child.type === FullWidthPageBlock,
209
+ child => isPageBlock(child) && isOfType(child, FullWidthPageBlock),
138
210
  );
139
211
  const mainBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'main');
140
212
  const sideBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'side');
@@ -160,24 +232,41 @@ export function DetailFormGrid({ children }: { children: React.ReactNode }) {
160
232
  return <div className="md:grid md:grid-cols-2 gap-4 items-start mb-4">{children}</div>;
161
233
  }
162
234
 
163
- export interface PageContext {
164
- pageId?: string;
165
- entity?: any;
166
- form?: UseFormReturn<any>;
167
- }
168
-
235
+ /**
236
+ * @description
237
+ * **Status: Developer Preview**
238
+ *
239
+ * A component for displaying the title of a page. This should be used inside the {@link Page} component.
240
+ *
241
+ * @docsCategory components
242
+ * @docsPage PageTitle
243
+ * @since 3.3.0
244
+ */
169
245
  export function PageTitle({ children }: { children: React.ReactNode }) {
170
246
  return <h1 className="text-2xl font-semibold">{children}</h1>;
171
247
  }
172
248
 
249
+ /**
250
+ * @description
251
+ * **Status: Developer Preview**
252
+ *
253
+ * A component for displaying the main actions for a page. This should be used inside the {@link Page} component.
254
+ * It should be used in conjunction with the {@link PageActionBarLeft} and {@link PageActionBarRight} components
255
+ * as direct children.
256
+ *
257
+ * @docsCategory components
258
+ * @docsPage PageActionBar
259
+ * @docsWeight 0
260
+ * @since 3.3.0
261
+ */
173
262
  export function PageActionBar({ children }: { children: React.ReactNode }) {
174
263
  let childArray = React.Children.toArray(children);
175
264
 
176
265
  const leftContent = childArray.filter(
177
- child => React.isValidElement(child) && child.type === PageActionBarLeft,
266
+ child => isOfType(child, PageActionBarLeft),
178
267
  );
179
268
  const rightContent = childArray.filter(
180
- child => React.isValidElement(child) && child.type === PageActionBarRight,
269
+ child => isOfType(child, PageActionBarRight),
181
270
  );
182
271
 
183
272
  return (
@@ -188,10 +277,26 @@ export function PageActionBar({ children }: { children: React.ReactNode }) {
188
277
  );
189
278
  }
190
279
 
280
+ /**
281
+ * @description
282
+ * **Status: Developer Preview**
283
+ *
284
+ * @docsCategory components
285
+ * @docsPage PageActionBar
286
+ * @since 3.3.0
287
+ */
191
288
  export function PageActionBarLeft({ children }: { children: React.ReactNode }) {
192
289
  return <div className="flex justify-start gap-2">{children}</div>;
193
290
  }
194
291
 
292
+ /**
293
+ * @description
294
+ * **Status: Developer Preview**
295
+ *
296
+ * @docsCategory components
297
+ * @docsPage PageActionBar
298
+ * @since 3.3.0
299
+ */
195
300
  export function PageActionBarRight({ children }: { children: React.ReactNode }) {
196
301
  const page = usePage();
197
302
  const actionBarItems = page.pageId ? getDashboardActionBarItems(page.pageId) : [];
@@ -205,7 +310,7 @@ export function PageActionBarRight({ children }: { children: React.ReactNode })
205
310
  );
206
311
  }
207
312
 
208
- function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page: PageContext }) {
313
+ function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page: PageContextValue }) {
209
314
  return (
210
315
  <PermissionGuard requires={item.requiresPermission ?? []}>
211
316
  <item.component context={page} />
@@ -213,6 +318,14 @@ function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page:
213
318
  );
214
319
  }
215
320
 
321
+ /**
322
+ * @description
323
+ * **Status: Developer Preview**
324
+ *
325
+ * @docsCategory components
326
+ * @docsPage PageBlock
327
+ * @since 3.3.0
328
+ */
216
329
  export type PageBlockProps = {
217
330
  children?: React.ReactNode;
218
331
  /** Which column this block should appear in */
@@ -223,6 +336,19 @@ export type PageBlockProps = {
223
336
  className?: string;
224
337
  };
225
338
 
339
+ /**
340
+ * @description
341
+ * **Status: Developer Preview**
342
+ *
343
+ * A component for displaying a block of content on a page. This should be used inside the {@link PageLayout} component.
344
+ * It should be provided with a `column` prop to determine which column it should appear in, and a `blockId` prop
345
+ * to identify the block.
346
+ *
347
+ * @docsCategory components
348
+ * @docsPage PageBlock
349
+ * @docsWeight 0
350
+ * @since 3.3.0
351
+ */
226
352
  export function PageBlock({ children, title, description, className, blockId }: PageBlockProps) {
227
353
  return (
228
354
  <LocationWrapper blockId={blockId}>
@@ -239,11 +365,22 @@ export function PageBlock({ children, title, description, className, blockId }:
239
365
  );
240
366
  }
241
367
 
368
+ /**
369
+ * @description
370
+ * **Status: Developer Preview**
371
+ *
372
+ * A component for displaying a block of content on a page that takes up the full width of the page.
373
+ * This should be used inside the {@link PageLayout} component.
374
+ *
375
+ * @docsCategory components
376
+ * @docsPage PageBlock
377
+ * @since 3.3.0
378
+ */
242
379
  export function FullWidthPageBlock({
243
- children,
244
- className,
245
- blockId,
246
- }: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
380
+ children,
381
+ className,
382
+ blockId,
383
+ }: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
247
384
  return (
248
385
  <LocationWrapper blockId={blockId}>
249
386
  <div className={cn('w-full', className)}>{children}</div>
@@ -251,11 +388,21 @@ export function FullWidthPageBlock({
251
388
  );
252
389
  }
253
390
 
391
+ /**
392
+ * @description
393
+ * **Status: Developer Preview**
394
+ *
395
+ * A component for displaying an auto-generated form for custom fields on a page.
396
+ *
397
+ * @docsCategory components
398
+ * @docsPage PageBlock
399
+ * @since 3.3.0
400
+ */
254
401
  export function CustomFieldsPageBlock({
255
- column,
256
- entityType,
257
- control,
258
- }: {
402
+ column,
403
+ entityType,
404
+ control,
405
+ }: {
259
406
  column: 'main' | 'side';
260
407
  entityType: string;
261
408
  control: Control<any, any>;
@@ -270,3 +417,17 @@ export function CustomFieldsPageBlock({
270
417
  </PageBlock>
271
418
  );
272
419
  }
420
+
421
+ /**
422
+ * @description
423
+ * This compares the type of a React component to a given type.
424
+ * It is safer than a simple `el === Component` check, as it also works in the context of
425
+ * the Vite build where the component is not the same reference.
426
+ */
427
+ export function isOfType(el: unknown, type: React.FunctionComponent<any>): boolean {
428
+ if (React.isValidElement(el)) {
429
+ const elTypeName = typeof el.type === 'string' ? el.type : (el.type as React.FunctionComponent).name;
430
+ return elTypeName === type.name;
431
+ }
432
+ return false;
433
+ }
@@ -0,0 +1,10 @@
1
+ import { createContext } from 'react';
2
+ import { UseFormReturn } from 'react-hook-form';
3
+
4
+ export interface PageContextValue {
5
+ pageId?: string;
6
+ entity?: any;
7
+ form?: UseFormReturn<any>;
8
+ }
9
+
10
+ export const PageContext = createContext<PageContextValue | undefined>(undefined);
@@ -3,19 +3,19 @@ import { Input } from '@/components/ui/input.js';
3
3
  import { useDetailPage } from '@/framework/page/use-detail-page.js';
4
4
  import { Trans } from '@/lib/trans.js';
5
5
  import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
6
- import { AnyRoute } from '@tanstack/react-router';
6
+ import { AnyRoute, useNavigate } from '@tanstack/react-router';
7
7
  import { ResultOf, VariablesOf } from 'gql.tada';
8
-
9
8
  import { DateTimeInput } from '@/components/data-input/datetime-input.js';
10
9
  import { Button } from '@/components/ui/button.js';
11
10
  import { Checkbox } from '@/components/ui/checkbox.js';
11
+ import { NEW_ENTITY_PATH } from '@/constants.js';
12
12
  import { toast } from 'sonner';
13
13
  import { getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
14
+
14
15
  import {
15
16
  DetailFormGrid,
16
17
  Page,
17
18
  PageActionBar,
18
- PageActionBarLeft,
19
19
  PageActionBarRight,
20
20
  PageBlock,
21
21
  PageLayout,
@@ -23,21 +23,70 @@ import {
23
23
  } from '../layout-engine/page-layout.js';
24
24
  import { DetailEntityPath } from './page-types.js';
25
25
 
26
+ /**
27
+ * @description
28
+ * **Status: Developer Preview**
29
+ *
30
+ * @docsCategory components
31
+ * @docsPage DetailPage
32
+ * @since 3.3.0
33
+ */
26
34
  export interface DetailPageProps<
27
35
  T extends TypedDocumentNode<any, any>,
28
36
  C extends TypedDocumentNode<any, any>,
29
37
  U extends TypedDocumentNode<any, any>,
30
38
  EntityField extends keyof ResultOf<T> = DetailEntityPath<T>,
31
39
  > {
40
+ /**
41
+ * @description
42
+ * A unique identifier for the page.
43
+ */
32
44
  pageId: string;
45
+ /**
46
+ * @description
47
+ * The Tanstack Router route used to navigate to this page.
48
+ */
33
49
  route: AnyRoute;
50
+ /**
51
+ * @description
52
+ * The title of the page.
53
+ */
34
54
  title: (entity: ResultOf<T>[EntityField]) => string;
55
+ /**
56
+ * @description
57
+ * The query document used to fetch the entity.
58
+ */
35
59
  queryDocument: T;
60
+ /**
61
+ * @description
62
+ * The mutation document used to create the entity.
63
+ */
36
64
  createDocument?: C;
65
+ /**
66
+ * @description
67
+ * The mutation document used to update the entity.
68
+ */
37
69
  updateDocument: U;
70
+ /**
71
+ * @description
72
+ * A function that sets the values for the update input type based on the entity.
73
+ */
38
74
  setValuesForUpdate: (entity: ResultOf<T>[EntityField]) => VariablesOf<U>['input'];
39
75
  }
40
76
 
77
+ /**
78
+ * @description
79
+ * **Status: Developer Preview**
80
+ *
81
+ * Auto-generates a detail page with a form based on the provided query and mutation documents.
82
+ *
83
+ * For more control over the layout, you would use the more low-level {@link Page} component.
84
+ *
85
+ * @docsCategory components
86
+ * @docsPage DetailPage
87
+ * @docsWeight 0
88
+ * @since 3.3.0
89
+ */
41
90
  export function DetailPage<
42
91
  T extends TypedDocumentNode<any, any>,
43
92
  C extends TypedDocumentNode<any, any>,
@@ -52,6 +101,8 @@ export function DetailPage<
52
101
  title,
53
102
  }: DetailPageProps<T, C, U>) {
54
103
  const params = route.useParams();
104
+ const creatingNewEntity = params.id === NEW_ENTITY_PATH;
105
+ const navigate = useNavigate();
55
106
 
56
107
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage<any, any, any>({
57
108
  queryDocument,
@@ -59,9 +110,13 @@ export function DetailPage<
59
110
  createDocument,
60
111
  params: { id: params.id },
61
112
  setValuesForUpdate,
62
- onSuccess: () => {
113
+ onSuccess: async (data) => {
63
114
  toast.success('Updated successfully');
64
115
  resetForm();
116
+ const id = (data as any).id;
117
+ if (creatingNewEntity && id) {
118
+ await navigate({ to: `../$id`, params: { id } });
119
+ }
65
120
  },
66
121
  onError: error => {
67
122
  toast.error('Failed to update', {
@@ -70,14 +125,12 @@ export function DetailPage<
70
125
  },
71
126
  });
72
127
 
73
- const updateFields = getOperationVariablesFields(updateDocument);
128
+ const updateFields = getOperationVariablesFields(updateDocument, 'input');
74
129
 
75
130
  return (
76
131
  <Page pageId={pageId} form={form} submitHandler={submitHandler}>
132
+ <PageTitle>{title(entity)}</PageTitle>
77
133
  <PageActionBar>
78
- <PageActionBarLeft>
79
- <PageTitle>{title(entity)}</PageTitle>
80
- </PageActionBarLeft>
81
134
  <PageActionBarRight>
82
135
  <Button
83
136
  type="submit"
@@ -114,7 +167,7 @@ export function DetailPage<
114
167
  case 'DateTime':
115
168
  return <DateTimeInput {...field} />;
116
169
  case 'Boolean':
117
- return <Checkbox {...field} />;
170
+ return <Checkbox value={field.value} onCheckedChange={field.onChange} />;
118
171
  case 'String':
119
172
  default:
120
173
  return <Input {...field} />;
@@ -11,7 +11,9 @@ import { TypedDocumentNode } from '@graphql-typed-document-node/core';
11
11
  import { AnyRoute, AnyRouter, useNavigate } from '@tanstack/react-router';
12
12
  import { ColumnFiltersState, SortingState, Table } from '@tanstack/react-table';
13
13
  import { TableOptions } from '@tanstack/table-core';
14
+ import { useUserSettings } from '@/hooks/use-user-settings.js';
14
15
  import { ResultOf } from 'gql.tada';
16
+
15
17
  import { addCustomFields } from '../document-introspection/add-custom-fields.js';
16
18
  import {
17
19
  FullWidthPageBlock,
@@ -29,6 +31,14 @@ type ListQueryFields<T extends TypedDocumentNode<any, any>> = {
29
31
  : never;
30
32
  }[keyof ResultOf<T>];
31
33
 
34
+ /**
35
+ * @description
36
+ * **Status: Developer Preview**
37
+ *
38
+ * @docsCategory components
39
+ * @docsPage ListPage
40
+ * @since 3.3.0
41
+ */
32
42
  export interface ListPageProps<
33
43
  T extends TypedDocumentNode<U, V>,
34
44
  U extends ListQueryShape,
@@ -54,6 +64,17 @@ export interface ListPageProps<
54
64
  setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
55
65
  }
56
66
 
67
+ /**
68
+ * @description
69
+ * **Status: Developer Preview**
70
+ *
71
+ * Auto-generates a list page with columns generated based on the provided query document fields.
72
+ *
73
+ * @docsCategory components
74
+ * @docsPage ListPage
75
+ * @docsWeight 0
76
+ * @since 3.3.0
77
+ */
57
78
  export function ListPage<
58
79
  T extends TypedDocumentNode<U, V>,
59
80
  U extends Record<string, any> = any,
@@ -81,12 +102,18 @@ export function ListPage<
81
102
  const route = typeof routeOrFn === 'function' ? routeOrFn() : routeOrFn;
82
103
  const routeSearch = route.useSearch();
83
104
  const navigate = useNavigate<AnyRouter>({ from: route.fullPath });
105
+ const { setTableSettings, settings } = useUserSettings();
106
+ const tableSettings = pageId ? settings.tableSettings?.[pageId] : undefined;
84
107
 
85
108
  const pagination = {
86
109
  page: routeSearch.page ? parseInt(routeSearch.page) : 1,
87
- itemsPerPage: routeSearch.perPage ? parseInt(routeSearch.perPage) : 10,
110
+ itemsPerPage: routeSearch.perPage ? parseInt(routeSearch.perPage) : tableSettings?.pageSize ?? 10,
88
111
  };
89
112
 
113
+ const columnVisibility = pageId ? tableSettings?.columnVisibility : defaultVisibility;
114
+ const columnOrder = pageId ? tableSettings?.columnOrder : defaultColumnOrder;
115
+ const columnFilters = pageId ? tableSettings?.columnFilters : routeSearch.filters;
116
+
90
117
  const sorting: SortingState = (routeSearch.sort ?? '')
91
118
  .split(',')
92
119
  .filter((s: string) => s.length)
@@ -138,21 +165,32 @@ export function ListPage<
138
165
  transformVariables={transformVariables}
139
166
  customizeColumns={customizeColumns as any}
140
167
  additionalColumns={additionalColumns as any}
141
- defaultColumnOrder={defaultColumnOrder}
142
- defaultVisibility={defaultVisibility}
168
+ defaultColumnOrder={columnOrder as any}
169
+ defaultVisibility={columnVisibility as any}
143
170
  onSearchTermChange={onSearchTermChange}
144
171
  page={pagination.page}
145
172
  itemsPerPage={pagination.itemsPerPage}
146
173
  sorting={sorting}
147
- columnFilters={routeSearch.filters}
174
+ columnFilters={columnFilters}
148
175
  onPageChange={(table, page, perPage) => {
149
176
  persistListStateToUrl(table, { page, perPage });
177
+ if (pageId) {
178
+ setTableSettings(pageId, 'pageSize', perPage);
179
+ }
150
180
  }}
151
181
  onSortChange={(table, sorting) => {
152
182
  persistListStateToUrl(table, { sort: sorting });
153
183
  }}
154
184
  onFilterChange={(table, filters) => {
155
185
  persistListStateToUrl(table, { filters });
186
+ if (pageId) {
187
+ setTableSettings(pageId, 'columnFilters', filters);
188
+ }
189
+ }}
190
+ onColumnVisibilityChange={(table, columnVisibility) => {
191
+ if (pageId) {
192
+ setTableSettings(pageId, 'columnVisibility', columnVisibility);
193
+ }
156
194
  }}
157
195
  facetedFilters={facetedFilters}
158
196
  rowActions={rowActions}
@@ -1,4 +1,4 @@
1
- import { DashboardRouteDefinition } from '@/framework/extension-api/extension-api-types.js';
1
+ import { DashboardRouteDefinition } from '../extension-api/extension-api-types.js';
2
2
 
3
3
  export const extensionRoutes = new Map<string, DashboardRouteDefinition>();
4
4