@vendure/dashboard 3.2.4 → 3.3.1
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 +191 -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,47 @@ 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
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
</PageContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function PageContent({ pageHeader, pageContent, form, submitHandler, ...props }: {
|
|
77
|
+
pageHeader: React.ReactNode;
|
|
78
|
+
pageContent: React.ReactNode;
|
|
79
|
+
form?: UseFormReturn<any>;
|
|
80
|
+
submitHandler?: any;
|
|
81
|
+
className?: string;
|
|
82
|
+
}) {
|
|
83
|
+
return (
|
|
84
|
+
<div className={cn('m-4', props.className)} {...props}>
|
|
85
|
+
<LocationWrapper>
|
|
86
|
+
<PageContentWithOptionalForm
|
|
87
|
+
pageHeader={pageHeader}
|
|
88
|
+
pageContent={pageContent}
|
|
89
|
+
form={form}
|
|
90
|
+
submitHandler={submitHandler}
|
|
91
|
+
/>
|
|
92
|
+
</LocationWrapper>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function PageContentWithOptionalForm({ form, pageHeader, pageContent, submitHandler }: {
|
|
98
|
+
form?: UseFormReturn<any>;
|
|
99
|
+
pageHeader: React.ReactNode
|
|
100
|
+
pageContent: React.ReactNode;
|
|
101
|
+
submitHandler?: any;
|
|
102
|
+
}) {
|
|
103
|
+
return form ? (
|
|
47
104
|
<Form {...form}>
|
|
48
105
|
<NavigationConfirmation form={form} />
|
|
49
106
|
<form onSubmit={submitHandler} className="space-y-4">
|
|
@@ -57,18 +114,16 @@ export function Page({ children, pageId, entity, form, submitHandler, ...props }
|
|
|
57
114
|
{pageContent}
|
|
58
115
|
</div>
|
|
59
116
|
);
|
|
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
117
|
}
|
|
71
118
|
|
|
119
|
+
/**
|
|
120
|
+
* @description
|
|
121
|
+
* **Status: Developer Preview**
|
|
122
|
+
*
|
|
123
|
+
* @docsCategory components
|
|
124
|
+
* @docsPage PageLayout
|
|
125
|
+
* @since 3.3.0
|
|
126
|
+
*/
|
|
72
127
|
export type PageLayoutProps = {
|
|
73
128
|
children: React.ReactNode;
|
|
74
129
|
className?: string;
|
|
@@ -87,6 +142,18 @@ function isPageBlock(child: unknown): child is React.ReactElement<PageBlockProps
|
|
|
87
142
|
return hasColumn || hasBlockId;
|
|
88
143
|
}
|
|
89
144
|
|
|
145
|
+
/**
|
|
146
|
+
* @description
|
|
147
|
+
* **Status: Developer Preview**
|
|
148
|
+
*
|
|
149
|
+
* This component governs the layout of the contents of a {@link Page} component.
|
|
150
|
+
* It should contain all the {@link PageBlock} components that are to be displayed on the page.
|
|
151
|
+
*
|
|
152
|
+
* @docsCategory components
|
|
153
|
+
* @docsPage PageLayout
|
|
154
|
+
* @docsWeight 0
|
|
155
|
+
* @since 3.3.0
|
|
156
|
+
*/
|
|
90
157
|
export function PageLayout({ children, className }: PageLayoutProps) {
|
|
91
158
|
const page = usePage();
|
|
92
159
|
const isDesktop = useMediaQuery('only screen and (min-width : 769px)');
|
|
@@ -112,7 +179,7 @@ export function PageLayout({ children, className }: PageLayoutProps) {
|
|
|
112
179
|
if (childBlock) {
|
|
113
180
|
const blockId =
|
|
114
181
|
childBlock.props.blockId ??
|
|
115
|
-
(childBlock
|
|
182
|
+
(isOfType(childBlock, CustomFieldsPageBlock) ? 'custom-fields' : undefined);
|
|
116
183
|
const extensionBlock = extensionBlocks.find(block => block.location.position.blockId === blockId);
|
|
117
184
|
if (extensionBlock) {
|
|
118
185
|
const ExtensionBlock = (
|
|
@@ -138,7 +205,7 @@ export function PageLayout({ children, className }: PageLayoutProps) {
|
|
|
138
205
|
}
|
|
139
206
|
|
|
140
207
|
const fullWidthBlocks = finalChildArray.filter(
|
|
141
|
-
child => isPageBlock(child) && child
|
|
208
|
+
child => isPageBlock(child) && isOfType(child, FullWidthPageBlock),
|
|
142
209
|
);
|
|
143
210
|
const mainBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'main');
|
|
144
211
|
const sideBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'side');
|
|
@@ -164,24 +231,41 @@ export function DetailFormGrid({ children }: { children: React.ReactNode }) {
|
|
|
164
231
|
return <div className="md:grid md:grid-cols-2 gap-4 items-start mb-4">{children}</div>;
|
|
165
232
|
}
|
|
166
233
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
234
|
+
/**
|
|
235
|
+
* @description
|
|
236
|
+
* **Status: Developer Preview**
|
|
237
|
+
*
|
|
238
|
+
* A component for displaying the title of a page. This should be used inside the {@link Page} component.
|
|
239
|
+
*
|
|
240
|
+
* @docsCategory components
|
|
241
|
+
* @docsPage PageTitle
|
|
242
|
+
* @since 3.3.0
|
|
243
|
+
*/
|
|
173
244
|
export function PageTitle({ children }: { children: React.ReactNode }) {
|
|
174
245
|
return <h1 className="text-2xl font-semibold">{children}</h1>;
|
|
175
246
|
}
|
|
176
247
|
|
|
248
|
+
/**
|
|
249
|
+
* @description
|
|
250
|
+
* **Status: Developer Preview**
|
|
251
|
+
*
|
|
252
|
+
* A component for displaying the main actions for a page. This should be used inside the {@link Page} component.
|
|
253
|
+
* It should be used in conjunction with the {@link PageActionBarLeft} and {@link PageActionBarRight} components
|
|
254
|
+
* as direct children.
|
|
255
|
+
*
|
|
256
|
+
* @docsCategory components
|
|
257
|
+
* @docsPage PageActionBar
|
|
258
|
+
* @docsWeight 0
|
|
259
|
+
* @since 3.3.0
|
|
260
|
+
*/
|
|
177
261
|
export function PageActionBar({ children }: { children: React.ReactNode }) {
|
|
178
262
|
let childArray = React.Children.toArray(children);
|
|
179
263
|
|
|
180
264
|
const leftContent = childArray.filter(
|
|
181
|
-
child =>
|
|
265
|
+
child => isOfType(child, PageActionBarLeft),
|
|
182
266
|
);
|
|
183
267
|
const rightContent = childArray.filter(
|
|
184
|
-
child =>
|
|
268
|
+
child => isOfType(child, PageActionBarRight),
|
|
185
269
|
);
|
|
186
270
|
|
|
187
271
|
return (
|
|
@@ -192,10 +276,26 @@ export function PageActionBar({ children }: { children: React.ReactNode }) {
|
|
|
192
276
|
);
|
|
193
277
|
}
|
|
194
278
|
|
|
279
|
+
/**
|
|
280
|
+
* @description
|
|
281
|
+
* **Status: Developer Preview**
|
|
282
|
+
*
|
|
283
|
+
* @docsCategory components
|
|
284
|
+
* @docsPage PageActionBar
|
|
285
|
+
* @since 3.3.0
|
|
286
|
+
*/
|
|
195
287
|
export function PageActionBarLeft({ children }: { children: React.ReactNode }) {
|
|
196
288
|
return <div className="flex justify-start gap-2">{children}</div>;
|
|
197
289
|
}
|
|
198
290
|
|
|
291
|
+
/**
|
|
292
|
+
* @description
|
|
293
|
+
* **Status: Developer Preview**
|
|
294
|
+
*
|
|
295
|
+
* @docsCategory components
|
|
296
|
+
* @docsPage PageActionBar
|
|
297
|
+
* @since 3.3.0
|
|
298
|
+
*/
|
|
199
299
|
export function PageActionBarRight({ children }: { children: React.ReactNode }) {
|
|
200
300
|
const page = usePage();
|
|
201
301
|
const actionBarItems = page.pageId ? getDashboardActionBarItems(page.pageId) : [];
|
|
@@ -209,7 +309,7 @@ export function PageActionBarRight({ children }: { children: React.ReactNode })
|
|
|
209
309
|
);
|
|
210
310
|
}
|
|
211
311
|
|
|
212
|
-
function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page:
|
|
312
|
+
function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page: PageContextValue }) {
|
|
213
313
|
return (
|
|
214
314
|
<PermissionGuard requires={item.requiresPermission ?? []}>
|
|
215
315
|
<item.component context={page} />
|
|
@@ -217,6 +317,14 @@ function PageActionBarItem({ item, page }: { item: DashboardActionBarItem; page:
|
|
|
217
317
|
);
|
|
218
318
|
}
|
|
219
319
|
|
|
320
|
+
/**
|
|
321
|
+
* @description
|
|
322
|
+
* **Status: Developer Preview**
|
|
323
|
+
*
|
|
324
|
+
* @docsCategory components
|
|
325
|
+
* @docsPage PageBlock
|
|
326
|
+
* @since 3.3.0
|
|
327
|
+
*/
|
|
220
328
|
export type PageBlockProps = {
|
|
221
329
|
children?: React.ReactNode;
|
|
222
330
|
/** Which column this block should appear in */
|
|
@@ -227,6 +335,19 @@ export type PageBlockProps = {
|
|
|
227
335
|
className?: string;
|
|
228
336
|
};
|
|
229
337
|
|
|
338
|
+
/**
|
|
339
|
+
* @description
|
|
340
|
+
* **Status: Developer Preview**
|
|
341
|
+
*
|
|
342
|
+
* A component for displaying a block of content on a page. This should be used inside the {@link PageLayout} component.
|
|
343
|
+
* It should be provided with a `column` prop to determine which column it should appear in, and a `blockId` prop
|
|
344
|
+
* to identify the block.
|
|
345
|
+
*
|
|
346
|
+
* @docsCategory components
|
|
347
|
+
* @docsPage PageBlock
|
|
348
|
+
* @docsWeight 0
|
|
349
|
+
* @since 3.3.0
|
|
350
|
+
*/
|
|
230
351
|
export function PageBlock({ children, title, description, className, blockId }: PageBlockProps) {
|
|
231
352
|
return (
|
|
232
353
|
<LocationWrapper blockId={blockId}>
|
|
@@ -243,11 +364,22 @@ export function PageBlock({ children, title, description, className, blockId }:
|
|
|
243
364
|
);
|
|
244
365
|
}
|
|
245
366
|
|
|
367
|
+
/**
|
|
368
|
+
* @description
|
|
369
|
+
* **Status: Developer Preview**
|
|
370
|
+
*
|
|
371
|
+
* A component for displaying a block of content on a page that takes up the full width of the page.
|
|
372
|
+
* This should be used inside the {@link PageLayout} component.
|
|
373
|
+
*
|
|
374
|
+
* @docsCategory components
|
|
375
|
+
* @docsPage PageBlock
|
|
376
|
+
* @since 3.3.0
|
|
377
|
+
*/
|
|
246
378
|
export function FullWidthPageBlock({
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
|
|
379
|
+
children,
|
|
380
|
+
className,
|
|
381
|
+
blockId,
|
|
382
|
+
}: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
|
|
251
383
|
return (
|
|
252
384
|
<LocationWrapper blockId={blockId}>
|
|
253
385
|
<div className={cn('w-full', className)}>{children}</div>
|
|
@@ -255,11 +387,21 @@ export function FullWidthPageBlock({
|
|
|
255
387
|
);
|
|
256
388
|
}
|
|
257
389
|
|
|
390
|
+
/**
|
|
391
|
+
* @description
|
|
392
|
+
* **Status: Developer Preview**
|
|
393
|
+
*
|
|
394
|
+
* A component for displaying an auto-generated form for custom fields on a page.
|
|
395
|
+
*
|
|
396
|
+
* @docsCategory components
|
|
397
|
+
* @docsPage PageBlock
|
|
398
|
+
* @since 3.3.0
|
|
399
|
+
*/
|
|
258
400
|
export function CustomFieldsPageBlock({
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}: {
|
|
401
|
+
column,
|
|
402
|
+
entityType,
|
|
403
|
+
control,
|
|
404
|
+
}: {
|
|
263
405
|
column: 'main' | 'side';
|
|
264
406
|
entityType: string;
|
|
265
407
|
control: Control<any, any>;
|
|
@@ -274,3 +416,17 @@ export function CustomFieldsPageBlock({
|
|
|
274
416
|
</PageBlock>
|
|
275
417
|
);
|
|
276
418
|
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @description
|
|
422
|
+
* This compares the type of a React component to a given type.
|
|
423
|
+
* It is safer than a simple `el === Component` check, as it also works in the context of
|
|
424
|
+
* the Vite build where the component is not the same reference.
|
|
425
|
+
*/
|
|
426
|
+
export function isOfType(el: unknown, type: React.FunctionComponent<any>): boolean {
|
|
427
|
+
if (React.isValidElement(el)) {
|
|
428
|
+
const elTypeName = typeof el.type === 'string' ? el.type : (el.type as React.FunctionComponent).name;
|
|
429
|
+
return elTypeName === type.name;
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
@@ -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;
|