@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.
- package/dist/plugin/utils/ast-utils.d.ts +10 -0
- package/dist/plugin/utils/ast-utils.js +96 -0
- package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
- package/dist/plugin/utils/ast-utils.spec.js +120 -0
- package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
- package/dist/plugin/utils/config-loader.js +325 -0
- package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
- package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +7 -1
- package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -3
- package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
- package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
- package/dist/plugin/vite-plugin-config-loader.js +18 -9
- package/dist/plugin/vite-plugin-config.js +4 -6
- package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
- package/dist/plugin/vite-plugin-gql-tada.js +2 -2
- package/dist/plugin/vite-plugin-ui-config.js +3 -2
- package/package.json +16 -11
- package/src/app/app-providers.tsx +9 -9
- package/src/app/main.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
- package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
- package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
- package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
- package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -2
- package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
- package/src/app/routes/_authenticated/_products/products.tsx +1 -1
- 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/routes/_authenticated.tsx +12 -1
- package/src/app/styles.css +15 -0
- package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
- package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
- package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
- package/src/lib/components/data-table/data-table-types.ts +1 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +73 -24
- package/src/lib/components/data-table/data-table.tsx +49 -44
- package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
- package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
- package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
- package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
- package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
- package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
- package/src/lib/components/data-table/refresh-button.tsx +25 -0
- package/src/lib/components/layout/nav-user.tsx +20 -15
- package/src/lib/components/layout/prerelease-popup.tsx +1 -5
- package/src/lib/components/shared/alerts.tsx +19 -1
- package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
- package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
- package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
- package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
- package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
- package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
- package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
- package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
- package/src/lib/components/shared/custom-fields-form.tsx +4 -3
- package/src/lib/components/shared/customer-selector.tsx +13 -14
- package/src/lib/components/shared/detail-page-button.tsx +2 -2
- package/src/lib/components/shared/entity-assets.tsx +3 -3
- package/src/lib/components/shared/error-page.tsx +2 -2
- package/src/lib/components/shared/navigation-confirmation.tsx +49 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +10 -1
- package/src/lib/components/shared/product-variant-selector.tsx +111 -0
- package/src/lib/components/shared/vendure-image.tsx +1 -1
- package/src/lib/components/ui/calendar.tsx +508 -63
- 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.spec.ts +113 -3
- package/src/lib/framework/document-introspection/get-document-structure.ts +71 -13
- 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/form-engine/use-generated-form.tsx +8 -7
- package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
- package/src/lib/framework/layout-engine/page-layout.tsx +196 -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 +42 -4
- package/src/lib/framework/page/page-api.ts +1 -1
- package/src/lib/framework/page/use-detail-page.ts +82 -0
- package/src/lib/framework/registry/registry-types.ts +6 -2
- package/src/lib/graphql/fragments.tsx +8 -0
- 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 +7 -8
- package/src/lib/providers/auth.tsx +22 -9
- package/src/lib/providers/channel-provider.tsx +9 -1
- package/src/lib/providers/server-config.tsx +7 -1
- package/src/lib/providers/user-settings.tsx +24 -0
- package/vite/utils/ast-utils.spec.ts +128 -0
- package/vite/utils/ast-utils.ts +119 -0
- package/vite/utils/config-loader.ts +410 -0
- package/vite/{schema-generator.ts → utils/schema-generator.ts} +11 -6
- package/vite/{ui-config.ts → utils/ui-config.ts} +7 -3
- package/vite/vite-plugin-admin-api-schema.ts +2 -12
- package/vite/vite-plugin-config-loader.ts +25 -13
- package/vite/vite-plugin-config.ts +1 -0
- package/vite/vite-plugin-dashboard-metadata.ts +19 -15
- package/vite/vite-plugin-gql-tada.ts +2 -2
- package/vite/vite-plugin-ui-config.ts +3 -2
- package/dist/plugin/config-loader.js +0 -141
- package/src/lib/components/shared/asset-preview.tsx +0 -345
- package/src/lib/components/ui/avatar.tsx +0 -38
- package/vite/config-loader.ts +0 -181
- /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
|
|
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
|
-
|
|
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 =>
|
|
49
|
+
child => isOfType(child, PageActionBar),
|
|
30
50
|
);
|
|
31
51
|
|
|
32
52
|
const pageContent = childArray.filter(
|
|
33
|
-
child =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 =>
|
|
266
|
+
child => isOfType(child, PageActionBarLeft),
|
|
178
267
|
);
|
|
179
268
|
const rightContent = childArray.filter(
|
|
180
|
-
child =>
|
|
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:
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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 {
|
|
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={
|
|
142
|
-
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={
|
|
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}
|