@vendure/dashboard 3.2.4 → 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.
- package/dist/plugin/utils/ast-utils.spec.js +2 -2
- package/dist/plugin/utils/schema-generator.js +1 -1
- package/dist/plugin/utils/ui-config.js +2 -3
- package/dist/plugin/vite-plugin-config.js +4 -6
- package/package.json +13 -9
- package/src/app/app-providers.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +0 -1
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -0
- package/src/app/routes/_authenticated/_system/job-queue.tsx +7 -8
- package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +241 -0
- package/src/app/styles.css +15 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +1 -1
- package/src/lib/components/data-table/data-table.tsx +32 -26
- package/src/lib/components/data-table/refresh-button.tsx +25 -0
- package/src/lib/components/layout/nav-user.tsx +16 -11
- package/src/lib/components/layout/prerelease-popup.tsx +1 -5
- package/src/lib/components/shared/alerts.tsx +19 -1
- package/src/lib/components/shared/error-page.tsx +2 -2
- package/src/lib/components/shared/navigation-confirmation.tsx +20 -10
- package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
- package/src/lib/framework/alert/alert-extensions.tsx +31 -0
- package/src/lib/framework/alert/alert-item.tsx +47 -0
- package/src/lib/framework/alert/alerts-indicator.tsx +23 -0
- package/src/lib/framework/alert/types.ts +13 -0
- package/src/lib/framework/dashboard-widget/base-widget.tsx +1 -0
- package/src/lib/framework/defaults.ts +34 -0
- package/src/lib/framework/document-introspection/get-document-structure.ts +1 -2
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +15 -5
- package/src/lib/framework/extension-api/extension-api-types.ts +81 -12
- package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
- package/src/lib/framework/layout-engine/page-layout.tsx +192 -35
- package/src/lib/framework/layout-engine/page-provider.tsx +10 -0
- package/src/lib/framework/page/detail-page.tsx +62 -9
- package/src/lib/framework/page/list-page.tsx +19 -0
- package/src/lib/framework/page/page-api.ts +1 -1
- package/src/lib/framework/page/use-detail-page.ts +81 -0
- package/src/lib/framework/registry/registry-types.ts +6 -2
- package/src/lib/graphql/graphql-env.d.ts +25 -9
- package/src/lib/hooks/use-auth.tsx +13 -1
- package/src/lib/hooks/use-channel.ts +13 -0
- package/src/lib/hooks/use-local-format.ts +28 -1
- package/src/lib/hooks/use-page.tsx +2 -3
- package/src/lib/hooks/use-permissions.ts +13 -0
- package/src/lib/index.ts +3 -4
- package/src/lib/providers/auth.tsx +11 -1
- package/src/lib/providers/channel-provider.tsx +8 -1
- package/vite/utils/ast-utils.spec.ts +2 -2
- package/vite/utils/schema-generator.ts +4 -5
- package/vite/utils/ui-config.ts +7 -3
- package/vite/vite-plugin-admin-api-schema.ts +0 -10
- package/vite/vite-plugin-config.ts +1 -0
- package/src/lib/components/ui/avatar.tsx +0 -38
|
@@ -7,13 +7,14 @@ import { usePage } from '@/hooks/use-page.js';
|
|
|
7
7
|
import { cn } from '@/lib/utils.js';
|
|
8
8
|
import { NavigationConfirmation } from '@/components/shared/navigation-confirmation.js';
|
|
9
9
|
import { useMediaQuery } from '@uidotdev/usehooks';
|
|
10
|
-
import React, { ComponentProps
|
|
10
|
+
import React, { ComponentProps } from 'react';
|
|
11
11
|
import { Control, UseFormReturn } from 'react-hook-form';
|
|
12
12
|
|
|
13
13
|
import { DashboardActionBarItem } from '../extension-api/extension-api-types.js';
|
|
14
14
|
|
|
15
15
|
import { getDashboardActionBarItems, getDashboardPageBlocks } from './layout-extensions.js';
|
|
16
16
|
import { LocationWrapper } from './location-wrapper.js';
|
|
17
|
+
import { PageContext, PageContextValue } from '@/framework/layout-engine/page-provider.js';
|
|
17
18
|
|
|
18
19
|
export interface PageProps extends ComponentProps<'div'> {
|
|
19
20
|
pageId?: string;
|
|
@@ -22,18 +23,34 @@ export interface PageProps extends ComponentProps<'div'> {
|
|
|
22
23
|
submitHandler?: any;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
*/
|
|
27
44
|
export function Page({ children, pageId, entity, form, submitHandler, ...props }: PageProps) {
|
|
28
45
|
const childArray = React.Children.toArray(children);
|
|
29
46
|
|
|
30
47
|
const pageTitle = childArray.find(child => React.isValidElement(child) && child.type === PageTitle);
|
|
31
48
|
const pageActionBar = childArray.find(
|
|
32
|
-
child =>
|
|
49
|
+
child => isOfType(child, PageActionBar),
|
|
33
50
|
);
|
|
34
51
|
|
|
35
52
|
const pageContent = childArray.filter(
|
|
36
|
-
child =>
|
|
53
|
+
child => !isOfType(child, PageTitle) && !isOfType(child, PageActionBar),
|
|
37
54
|
);
|
|
38
55
|
|
|
39
56
|
const pageHeader = (
|
|
@@ -43,7 +60,48 @@ export function Page({ children, pageId, entity, form, submitHandler, ...props }
|
|
|
43
60
|
</div>
|
|
44
61
|
);
|
|
45
62
|
|
|
46
|
-
|
|
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 ? (
|
|
47
105
|
<Form {...form}>
|
|
48
106
|
<NavigationConfirmation form={form} />
|
|
49
107
|
<form onSubmit={submitHandler} className="space-y-4">
|
|
@@ -57,18 +115,16 @@ export function Page({ children, pageId, entity, form, submitHandler, ...props }
|
|
|
57
115
|
{pageContent}
|
|
58
116
|
</div>
|
|
59
117
|
);
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<PageProvider value={{ pageId, form, entity }}>
|
|
63
|
-
<LocationWrapper>
|
|
64
|
-
<div className={cn('m-4', props.className)} {...props}>
|
|
65
|
-
{pageContentWithOptionalForm}
|
|
66
|
-
</div>
|
|
67
|
-
</LocationWrapper>
|
|
68
|
-
</PageProvider>
|
|
69
|
-
);
|
|
70
118
|
}
|
|
71
119
|
|
|
120
|
+
/**
|
|
121
|
+
* @description
|
|
122
|
+
* **Status: Developer Preview**
|
|
123
|
+
*
|
|
124
|
+
* @docsCategory components
|
|
125
|
+
* @docsPage PageLayout
|
|
126
|
+
* @since 3.3.0
|
|
127
|
+
*/
|
|
72
128
|
export type PageLayoutProps = {
|
|
73
129
|
children: React.ReactNode;
|
|
74
130
|
className?: string;
|
|
@@ -87,6 +143,18 @@ function isPageBlock(child: unknown): child is React.ReactElement<PageBlockProps
|
|
|
87
143
|
return hasColumn || hasBlockId;
|
|
88
144
|
}
|
|
89
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
|
+
*/
|
|
90
158
|
export function PageLayout({ children, className }: PageLayoutProps) {
|
|
91
159
|
const page = usePage();
|
|
92
160
|
const isDesktop = useMediaQuery('only screen and (min-width : 769px)');
|
|
@@ -112,7 +180,7 @@ export function PageLayout({ children, className }: PageLayoutProps) {
|
|
|
112
180
|
if (childBlock) {
|
|
113
181
|
const blockId =
|
|
114
182
|
childBlock.props.blockId ??
|
|
115
|
-
(childBlock
|
|
183
|
+
(isOfType(childBlock, CustomFieldsPageBlock) ? 'custom-fields' : undefined);
|
|
116
184
|
const extensionBlock = extensionBlocks.find(block => block.location.position.blockId === blockId);
|
|
117
185
|
if (extensionBlock) {
|
|
118
186
|
const ExtensionBlock = (
|
|
@@ -138,7 +206,7 @@ export function PageLayout({ children, className }: PageLayoutProps) {
|
|
|
138
206
|
}
|
|
139
207
|
|
|
140
208
|
const fullWidthBlocks = finalChildArray.filter(
|
|
141
|
-
child => isPageBlock(child) && child
|
|
209
|
+
child => isPageBlock(child) && isOfType(child, FullWidthPageBlock),
|
|
142
210
|
);
|
|
143
211
|
const mainBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'main');
|
|
144
212
|
const sideBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'side');
|
|
@@ -164,24 +232,41 @@ export function DetailFormGrid({ children }: { children: React.ReactNode }) {
|
|
|
164
232
|
return <div className="md:grid md:grid-cols-2 gap-4 items-start mb-4">{children}</div>;
|
|
165
233
|
}
|
|
166
234
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
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
|
+
*/
|
|
173
245
|
export function PageTitle({ children }: { children: React.ReactNode }) {
|
|
174
246
|
return <h1 className="text-2xl font-semibold">{children}</h1>;
|
|
175
247
|
}
|
|
176
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
|
+
*/
|
|
177
262
|
export function PageActionBar({ children }: { children: React.ReactNode }) {
|
|
178
263
|
let childArray = React.Children.toArray(children);
|
|
179
264
|
|
|
180
265
|
const leftContent = childArray.filter(
|
|
181
|
-
child =>
|
|
266
|
+
child => isOfType(child, PageActionBarLeft),
|
|
182
267
|
);
|
|
183
268
|
const rightContent = childArray.filter(
|
|
184
|
-
child =>
|
|
269
|
+
child => isOfType(child, PageActionBarRight),
|
|
185
270
|
);
|
|
186
271
|
|
|
187
272
|
return (
|
|
@@ -192,10 +277,26 @@ export function PageActionBar({ children }: { children: React.ReactNode }) {
|
|
|
192
277
|
);
|
|
193
278
|
}
|
|
194
279
|
|
|
280
|
+
/**
|
|
281
|
+
* @description
|
|
282
|
+
* **Status: Developer Preview**
|
|
283
|
+
*
|
|
284
|
+
* @docsCategory components
|
|
285
|
+
* @docsPage PageActionBar
|
|
286
|
+
* @since 3.3.0
|
|
287
|
+
*/
|
|
195
288
|
export function PageActionBarLeft({ children }: { children: React.ReactNode }) {
|
|
196
289
|
return <div className="flex justify-start gap-2">{children}</div>;
|
|
197
290
|
}
|
|
198
291
|
|
|
292
|
+
/**
|
|
293
|
+
* @description
|
|
294
|
+
* **Status: Developer Preview**
|
|
295
|
+
*
|
|
296
|
+
* @docsCategory components
|
|
297
|
+
* @docsPage PageActionBar
|
|
298
|
+
* @since 3.3.0
|
|
299
|
+
*/
|
|
199
300
|
export function PageActionBarRight({ children }: { children: React.ReactNode }) {
|
|
200
301
|
const page = usePage();
|
|
201
302
|
const actionBarItems = page.pageId ? getDashboardActionBarItems(page.pageId) : [];
|
|
@@ -209,7 +310,7 @@ export function PageActionBarRight({ children }: { children: React.ReactNode })
|
|
|
209
310
|
);
|
|
210
311
|
}
|
|
211
312
|
|
|
212
|
-
function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page:
|
|
313
|
+
function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page: PageContextValue }) {
|
|
213
314
|
return (
|
|
214
315
|
<PermissionGuard requires={item.requiresPermission ?? []}>
|
|
215
316
|
<item.component context={page} />
|
|
@@ -217,6 +318,14 @@ function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page:
|
|
|
217
318
|
);
|
|
218
319
|
}
|
|
219
320
|
|
|
321
|
+
/**
|
|
322
|
+
* @description
|
|
323
|
+
* **Status: Developer Preview**
|
|
324
|
+
*
|
|
325
|
+
* @docsCategory components
|
|
326
|
+
* @docsPage PageBlock
|
|
327
|
+
* @since 3.3.0
|
|
328
|
+
*/
|
|
220
329
|
export type PageBlockProps = {
|
|
221
330
|
children?: React.ReactNode;
|
|
222
331
|
/** Which column this block should appear in */
|
|
@@ -227,6 +336,19 @@ export type PageBlockProps = {
|
|
|
227
336
|
className?: string;
|
|
228
337
|
};
|
|
229
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
|
+
*/
|
|
230
352
|
export function PageBlock({ children, title, description, className, blockId }: PageBlockProps) {
|
|
231
353
|
return (
|
|
232
354
|
<LocationWrapper blockId={blockId}>
|
|
@@ -243,11 +365,22 @@ export function PageBlock({ children, title, description, className, blockId }:
|
|
|
243
365
|
);
|
|
244
366
|
}
|
|
245
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
|
+
*/
|
|
246
379
|
export function FullWidthPageBlock({
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
|
|
380
|
+
children,
|
|
381
|
+
className,
|
|
382
|
+
blockId,
|
|
383
|
+
}: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
|
|
251
384
|
return (
|
|
252
385
|
<LocationWrapper blockId={blockId}>
|
|
253
386
|
<div className={cn('w-full', className)}>{children}</div>
|
|
@@ -255,11 +388,21 @@ export function FullWidthPageBlock({
|
|
|
255
388
|
);
|
|
256
389
|
}
|
|
257
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
|
+
*/
|
|
258
401
|
export function CustomFieldsPageBlock({
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}: {
|
|
402
|
+
column,
|
|
403
|
+
entityType,
|
|
404
|
+
control,
|
|
405
|
+
}: {
|
|
263
406
|
column: 'main' | 'side';
|
|
264
407
|
entityType: string;
|
|
265
408
|
control: Control<any, any>;
|
|
@@ -274,3 +417,17 @@ export function CustomFieldsPageBlock({
|
|
|
274
417
|
</PageBlock>
|
|
275
418
|
);
|
|
276
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 {
|
|
170
|
+
return <Checkbox value={field.value} onCheckedChange={field.onChange} />;
|
|
118
171
|
case 'String':
|
|
119
172
|
default:
|
|
120
173
|
return <Input {...field} />;
|
|
@@ -31,6 +31,14 @@ type ListQueryFields<T extends TypedDocumentNode<any, any>> = {
|
|
|
31
31
|
: never;
|
|
32
32
|
}[keyof ResultOf<T>];
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @description
|
|
36
|
+
* **Status: Developer Preview**
|
|
37
|
+
*
|
|
38
|
+
* @docsCategory components
|
|
39
|
+
* @docsPage ListPage
|
|
40
|
+
* @since 3.3.0
|
|
41
|
+
*/
|
|
34
42
|
export interface ListPageProps<
|
|
35
43
|
T extends TypedDocumentNode<U, V>,
|
|
36
44
|
U extends ListQueryShape,
|
|
@@ -56,6 +64,17 @@ export interface ListPageProps<
|
|
|
56
64
|
setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
|
|
57
65
|
}
|
|
58
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
|
+
*/
|
|
59
78
|
export function ListPage<
|
|
60
79
|
T extends TypedDocumentNode<U, V>,
|
|
61
80
|
U extends Record<string, any> = any,
|
|
@@ -26,6 +26,14 @@ type RemoveNullFields<T> = {
|
|
|
26
26
|
[K in keyof T]: RemoveNull<T[K]>;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @description
|
|
31
|
+
* **Status: Developer Preview**
|
|
32
|
+
*
|
|
33
|
+
* @docsCategory hooks
|
|
34
|
+
* @docsPage useDetailPage
|
|
35
|
+
* @since 3.3.0
|
|
36
|
+
*/
|
|
29
37
|
export interface DetailPageOptions<
|
|
30
38
|
T extends TypedDocumentNode<any, any>,
|
|
31
39
|
C extends TypedDocumentNode<any, any>,
|
|
@@ -114,6 +122,14 @@ export type DetailPageEntity<
|
|
|
114
122
|
translations: DetailPageTranslations<T, EntityField>;
|
|
115
123
|
};
|
|
116
124
|
|
|
125
|
+
/**
|
|
126
|
+
* @description
|
|
127
|
+
* **Status: Developer Preview**
|
|
128
|
+
*
|
|
129
|
+
* @docsCategory hooks
|
|
130
|
+
* @docsPage useDetailPage
|
|
131
|
+
* @since 3.3.0
|
|
132
|
+
*/
|
|
117
133
|
export interface UseDetailPageResult<
|
|
118
134
|
T extends TypedDocumentNode<any, any>,
|
|
119
135
|
C extends TypedDocumentNode<any, any>,
|
|
@@ -130,8 +146,72 @@ export interface UseDetailPageResult<
|
|
|
130
146
|
|
|
131
147
|
/**
|
|
132
148
|
* @description
|
|
149
|
+
* **Status: Developer Preview**
|
|
150
|
+
*
|
|
133
151
|
* This hook is used to create an entity detail page which can read
|
|
134
152
|
* and update an entity.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
|
|
157
|
+
* queryDocument: paymentMethodDetailDocument,
|
|
158
|
+
* createDocument: createPaymentMethodDocument,
|
|
159
|
+
* updateDocument: updatePaymentMethodDocument,
|
|
160
|
+
* setValuesForUpdate: entity => {
|
|
161
|
+
* return {
|
|
162
|
+
* id: entity.id,
|
|
163
|
+
* enabled: entity.enabled,
|
|
164
|
+
* name: entity.name,
|
|
165
|
+
* code: entity.code,
|
|
166
|
+
* description: entity.description,
|
|
167
|
+
* checker: entity.checker?.code
|
|
168
|
+
* ? {
|
|
169
|
+
* code: entity.checker?.code,
|
|
170
|
+
* arguments: entity.checker?.args,
|
|
171
|
+
* }
|
|
172
|
+
* : null,
|
|
173
|
+
* handler: entity.handler?.code
|
|
174
|
+
* ? {
|
|
175
|
+
* code: entity.handler?.code,
|
|
176
|
+
* arguments: entity.handler?.args,
|
|
177
|
+
* }
|
|
178
|
+
* : null,
|
|
179
|
+
* translations: entity.translations.map(translation => ({
|
|
180
|
+
* id: translation.id,
|
|
181
|
+
* languageCode: translation.languageCode,
|
|
182
|
+
* name: translation.name,
|
|
183
|
+
* description: translation.description,
|
|
184
|
+
* })),
|
|
185
|
+
* customFields: entity.customFields,
|
|
186
|
+
* };
|
|
187
|
+
* },
|
|
188
|
+
* transformCreateInput: input => {
|
|
189
|
+
* return {
|
|
190
|
+
* ...input,
|
|
191
|
+
* checker: input.checker?.code ? input.checker : undefined,
|
|
192
|
+
* handler: input.handler,
|
|
193
|
+
* };
|
|
194
|
+
* },
|
|
195
|
+
* params: { id: params.id },
|
|
196
|
+
* onSuccess: async data => {
|
|
197
|
+
* toast.success(i18n.t('Successfully updated payment method'));
|
|
198
|
+
* resetForm();
|
|
199
|
+
* if (creatingNewEntity) {
|
|
200
|
+
* await navigate({ to: `../$id`, params: { id: data.id } });
|
|
201
|
+
* }
|
|
202
|
+
* },
|
|
203
|
+
* onError: err => {
|
|
204
|
+
* toast.error(i18n.t('Failed to update payment method'), {
|
|
205
|
+
* description: err instanceof Error ? err.message : 'Unknown error',
|
|
206
|
+
* });
|
|
207
|
+
* },
|
|
208
|
+
* });
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @docsCategory hooks
|
|
212
|
+
* @docsPage useDetailPage
|
|
213
|
+
* @docsWeight 0
|
|
214
|
+
* @since 3.3.0
|
|
135
215
|
*/
|
|
136
216
|
export function useDetailPage<
|
|
137
217
|
T extends TypedDocumentNode<any, any>,
|
|
@@ -178,6 +258,7 @@ export function useDetailPage<
|
|
|
178
258
|
onSuccess?.((data as any)[createMutationName]);
|
|
179
259
|
}
|
|
180
260
|
},
|
|
261
|
+
onError,
|
|
181
262
|
});
|
|
182
263
|
|
|
183
264
|
const updateMutation = useMutation({
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { DashboardAlertDefinition } from '../alert/types.js';
|
|
1
2
|
import { DashboardWidgetDefinition } from '../dashboard-widget/types.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import {
|
|
4
|
+
DashboardActionBarItem,
|
|
5
|
+
DashboardPageBlockDefinition,
|
|
6
|
+
} from '../extension-api/extension-api-types.js';
|
|
4
7
|
import { NavMenuConfig } from '../nav-menu/nav-menu-extensions.js';
|
|
5
8
|
|
|
6
9
|
export interface GlobalRegistryContents {
|
|
@@ -10,6 +13,7 @@ export interface GlobalRegistryContents {
|
|
|
10
13
|
dashboardActionBarItemRegistry: Map<string, DashboardActionBarItem[]>;
|
|
11
14
|
dashboardPageBlockRegistry: Map<string, DashboardPageBlockDefinition[]>;
|
|
12
15
|
dashboardWidgetRegistry: Map<string, DashboardWidgetDefinition>;
|
|
16
|
+
dashboardAlertRegistry: Map<string, DashboardAlertDefinition>;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export type GlobalRegistryKey = keyof GlobalRegistryContents;
|