@vendure/dashboard 3.5.0-minor-202510031341 → 3.5.0-minor-202510161257
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/dashboard.plugin.js +1 -1
- package/dist/plugin/default-page.html +1 -1
- package/dist/vite/utils/ast-utils.spec.js +3 -3
- package/dist/vite/utils/tsconfig-utils.js +2 -1
- package/dist/vite/vite-plugin-hmr.d.ts +8 -0
- package/dist/vite/vite-plugin-hmr.js +34 -0
- package/dist/vite/vite-plugin-theme.js +6 -6
- package/dist/vite/vite-plugin-transform-index.js +6 -1
- package/dist/vite/vite-plugin-vendure-dashboard.d.ts +31 -4
- package/dist/vite/vite-plugin-vendure-dashboard.js +89 -34
- package/package.json +18 -5
- package/src/app/app-providers.tsx +4 -1
- package/src/app/common/map-faceted-filter-fields.ts +21 -0
- package/src/app/main.tsx +3 -1
- package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_administrators/administrators.tsx +13 -3
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +6 -13
- package/src/app/routes/_authenticated/_administrators/components/role-permissions-display.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets.tsx +17 -1
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +5 -0
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +0 -1
- package/src/app/routes/_authenticated/_customers/customers.tsx +9 -5
- package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +0 -6
- package/src/app/routes/_authenticated/_facets/components/facet-value-bulk-actions.tsx +16 -0
- package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +43 -12
- package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +14 -5
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +4 -8
- package/src/app/routes/_authenticated/_global-settings/utils/global-languages.ts +268 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +117 -92
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +15 -15
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +5 -5
- package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +26 -27
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +5 -3
- package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +6 -9
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +17 -1
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +48 -281
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +59 -40
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +73 -0
- package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +312 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +4 -0
- package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +49 -0
- package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +56 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +12 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +178 -50
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +0 -6
- package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +0 -11
- package/src/app/routes/_authenticated/_products/products.tsx +6 -2
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +4 -8
- package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +0 -10
- package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +12 -0
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +3 -10
- package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +4 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +4 -10
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -0
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -0
- package/src/app/routes/_authenticated/_zones/zones.graphql.ts +2 -2
- package/src/app/routes/login.tsx +2 -2
- package/src/i18n/locales/ar.po +420 -289
- package/src/i18n/locales/cs.po +420 -289
- package/src/i18n/locales/de.po +420 -289
- package/src/i18n/locales/en.po +420 -289
- package/src/i18n/locales/es.po +420 -289
- package/src/i18n/locales/fa.po +420 -289
- package/src/i18n/locales/fr.po +468 -337
- package/src/i18n/locales/he.po +420 -289
- package/src/i18n/locales/hr.po +420 -289
- package/src/i18n/locales/it.po +420 -289
- package/src/i18n/locales/ja.po +420 -289
- package/src/i18n/locales/nb.po +420 -289
- package/src/i18n/locales/ne.po +420 -289
- package/src/i18n/locales/pl.po +420 -289
- package/src/i18n/locales/pt_BR.po +420 -289
- package/src/i18n/locales/pt_PT.po +420 -289
- package/src/i18n/locales/ru.po +420 -289
- package/src/i18n/locales/sv.po +420 -289
- package/src/i18n/locales/tr.po +420 -289
- package/src/i18n/locales/uk.po +420 -289
- package/src/i18n/locales/zh_Hans.po +420 -289
- package/src/i18n/locales/zh_Hant.po +420 -289
- package/src/lib/components/data-input/affixed-input.stories.tsx +93 -0
- package/src/lib/components/data-input/affixed-input.tsx +5 -2
- package/src/lib/components/data-input/boolean-input.stories.tsx +102 -0
- package/src/lib/components/data-input/checkbox-input.stories.tsx +61 -0
- package/src/lib/components/data-input/customer-group-input.tsx +0 -1
- package/src/lib/components/data-input/datetime-input.stories.tsx +62 -0
- package/src/lib/components/data-input/datetime-input.tsx +27 -13
- package/src/lib/components/data-input/default-relation-input.tsx +18 -12
- package/src/lib/components/data-input/money-input.stories.tsx +88 -0
- package/src/lib/components/data-input/money-input.tsx +7 -11
- package/src/lib/components/data-input/number-input.stories.tsx +103 -0
- package/src/lib/components/data-input/number-input.tsx +16 -5
- package/src/lib/components/data-input/password-input.stories.tsx +65 -0
- package/src/lib/components/data-input/rich-text-input.stories.tsx +92 -0
- package/src/lib/components/data-input/slug-input.stories.tsx +232 -0
- package/src/lib/components/data-input/slug-input.tsx +9 -10
- package/src/lib/components/data-input/text-input.stories.tsx +52 -0
- package/src/lib/components/data-input/textarea-input.stories.tsx +55 -0
- package/src/lib/components/data-table/add-filter-menu.tsx +6 -1
- package/src/lib/components/data-table/column-header-wrapper.tsx +106 -0
- package/src/lib/components/data-table/data-table-bulk-action-item.tsx +11 -9
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +4 -4
- package/src/lib/components/data-table/data-table-column-header.tsx +17 -14
- package/src/lib/components/data-table/data-table-faceted-filter.tsx +33 -11
- package/src/lib/components/data-table/data-table-filter-badge-editable.tsx +35 -0
- package/src/lib/components/data-table/data-table-filter-badge.tsx +28 -14
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +28 -8
- package/src/lib/components/data-table/data-table-pagination.tsx +23 -7
- package/src/lib/components/data-table/data-table.stories.tsx +249 -0
- package/src/lib/components/data-table/data-table.tsx +39 -11
- package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +79 -34
- package/src/lib/components/data-table/use-generated-columns.tsx +55 -27
- package/src/lib/components/layout/generated-breadcrumbs.tsx +4 -12
- package/src/lib/components/layout/nav-user.tsx +19 -13
- package/src/lib/components/login/login-form.tsx +39 -123
- package/src/lib/components/shared/alerts.tsx +29 -17
- package/src/lib/components/shared/asset/asset-bulk-actions.tsx +3 -3
- package/src/lib/components/shared/asset/asset-gallery.stories.tsx +76 -0
- package/src/lib/components/shared/asset/asset-gallery.tsx +147 -113
- package/src/lib/components/shared/asset/asset-picker-dialog.stories.tsx +58 -0
- package/src/lib/components/shared/configurable-operation-input.tsx +1 -1
- package/src/lib/components/shared/customer-group-selector.tsx +5 -2
- package/src/lib/components/shared/detail-page-button.stories.tsx +52 -0
- package/src/lib/components/shared/facet-value-selector.stories.tsx +48 -0
- package/src/lib/components/shared/facet-value-selector.tsx +130 -34
- package/src/lib/components/shared/paginated-list-data-table.stories.tsx +212 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +12 -12
- package/src/lib/components/shared/permission-guard.stories.tsx +46 -0
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -0
- package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +8 -4
- package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +1 -0
- package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +40 -0
- package/src/lib/components/shared/vendure-image.stories.tsx +167 -0
- package/src/lib/components/shared/vendure-image.tsx +6 -7
- package/src/lib/components/ui/accordion.stories.tsx +33 -0
- package/src/lib/components/ui/alert-dialog.stories.tsx +48 -0
- package/src/lib/components/ui/alert.stories.tsx +35 -0
- package/src/lib/components/ui/aspect-ratio.stories.tsx +28 -0
- package/src/lib/components/ui/badge.stories.tsx +28 -0
- package/src/lib/components/ui/breadcrumb.stories.tsx +41 -0
- package/src/lib/components/ui/button.stories.tsx +38 -0
- package/src/lib/components/ui/calendar.stories.tsx +22 -0
- package/src/lib/components/ui/card.stories.tsx +28 -0
- package/src/lib/components/ui/carousel.stories.tsx +34 -0
- package/src/lib/components/ui/checkbox.stories.tsx +31 -0
- package/src/lib/components/ui/collapsible.stories.tsx +39 -0
- package/src/lib/components/ui/command.stories.tsx +44 -0
- package/src/lib/components/ui/context-menu.stories.tsx +38 -0
- package/src/lib/components/ui/dialog.stories.tsx +52 -0
- package/src/lib/components/ui/drawer.stories.tsx +50 -0
- package/src/lib/components/ui/dropdown-menu.stories.tsx +41 -0
- package/src/lib/components/ui/hover-card.stories.tsx +38 -0
- package/src/lib/components/ui/input-group.tsx +148 -0
- package/src/lib/components/ui/input-otp.stories.tsx +30 -0
- package/src/lib/components/ui/input.stories.tsx +38 -0
- package/src/lib/components/ui/label.stories.tsx +24 -0
- package/src/lib/components/ui/menubar.stories.tsx +53 -0
- package/src/lib/components/ui/navigation-menu.stories.tsx +54 -0
- package/src/lib/components/ui/pagination.stories.tsx +51 -0
- package/src/lib/components/ui/password-input.stories.tsx +32 -0
- package/src/lib/components/ui/password-input.tsx +33 -0
- package/src/lib/components/ui/popover.stories.tsx +33 -0
- package/src/lib/components/ui/progress.stories.tsx +27 -0
- package/src/lib/components/ui/radio-group.stories.tsx +34 -0
- package/src/lib/components/ui/resizable.stories.tsx +32 -0
- package/src/lib/components/ui/scroll-area.stories.tsx +31 -0
- package/src/lib/components/ui/select.stories.tsx +36 -0
- package/src/lib/components/ui/separator.stories.tsx +35 -0
- package/src/lib/components/ui/sheet.stories.tsx +50 -0
- package/src/lib/components/ui/sidebar-context.ts +16 -0
- package/src/lib/components/ui/sidebar.tsx +2 -13
- package/src/lib/components/ui/skeleton.stories.tsx +26 -0
- package/src/lib/components/ui/slider.stories.tsx +37 -0
- package/src/lib/components/ui/switch.stories.tsx +31 -0
- package/src/lib/components/ui/table.stories.tsx +52 -0
- package/src/lib/components/ui/tabs.stories.tsx +29 -0
- package/src/lib/components/ui/textarea.stories.tsx +32 -0
- package/src/lib/components/ui/toggle-group.stories.tsx +31 -0
- package/src/lib/components/ui/toggle.stories.tsx +39 -0
- package/src/lib/components/ui/tooltip.stories.tsx +30 -0
- package/src/lib/components/ui/tooltip.tsx +2 -2
- package/src/lib/framework/alert/alert-extensions.tsx +0 -11
- package/src/lib/framework/alert/alert-item.tsx +14 -19
- package/src/lib/framework/alert/alerts-indicator.tsx +14 -15
- package/src/lib/framework/alert/search-index-buffer-alert/search-index-buffer-alert.ts +41 -0
- package/src/lib/framework/component-registry/component-registry.tsx +3 -14
- package/src/lib/framework/dashboard-widget/base-widget.tsx +18 -9
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +0 -2
- package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +12 -11
- package/src/lib/framework/defaults.ts +9 -13
- package/src/lib/framework/extension-api/input-component-extensions.tsx +6 -1
- package/src/lib/framework/extension-api/logic/alerts.ts +3 -2
- package/src/lib/framework/extension-api/types/alerts.ts +12 -6
- package/src/lib/framework/extension-api/types/data-table.ts +5 -2
- package/src/lib/framework/extension-api/types/layout.ts +41 -1
- package/src/lib/framework/extension-api/types/login.ts +0 -21
- package/src/lib/framework/form-engine/value-transformers.ts +8 -1
- package/src/lib/framework/layout-engine/custom-form-page.stories.tsx +344 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +69 -57
- package/src/lib/framework/layout-engine/page.stories.tsx +275 -0
- package/src/lib/framework/nav-menu/nav-menu-extensions.ts +32 -19
- package/src/lib/framework/page/detail-page.stories.tsx +151 -0
- package/src/lib/framework/page/detail-page.tsx +12 -15
- package/src/lib/framework/page/list-page.stories.tsx +217 -0
- package/src/lib/framework/page/list-page.tsx +8 -1
- package/src/lib/graphql/api.ts +18 -1
- package/src/lib/graphql/graphql-env.d.ts +1 -1
- package/src/lib/hooks/use-alerts.ts +84 -0
- package/src/lib/hooks/use-floating-bulk-actions.ts +2 -3
- package/src/lib/index.ts +12 -5
- package/src/lib/providers/alerts-provider.tsx +60 -0
- package/src/lib/providers/channel-provider.tsx +1 -0
- package/src/lib/providers/theme-provider.tsx +6 -3
|
@@ -29,9 +29,16 @@ export const nativeValueTransformer: ValueTransformer = {
|
|
|
29
29
|
*/
|
|
30
30
|
export const jsonStringValueTransformer: ValueTransformer = {
|
|
31
31
|
parse: (value: string, fieldDef: ConfigurableFieldDef) => {
|
|
32
|
-
if (
|
|
32
|
+
if (value === undefined) {
|
|
33
33
|
return getDefaultValue(fieldDef);
|
|
34
34
|
}
|
|
35
|
+
// This case arises often when the administrator is actively editing
|
|
36
|
+
// values and clears out the input. At that point, we don't want to suddenly
|
|
37
|
+
// switch to the default value otherwise it results in poor UX, e.g. pressing
|
|
38
|
+
// backspace to delete a number would result in `0` suddenly appearing as the value.
|
|
39
|
+
if (value === '') {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
35
42
|
|
|
36
43
|
try {
|
|
37
44
|
// For JSON string mode, parse the string to get the native value
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
2
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
|
+
import { Input } from '@/vdb/components/ui/input.js';
|
|
4
|
+
import { Textarea } from '@/vdb/components/ui/textarea.js';
|
|
5
|
+
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
6
|
+
import { Trans } from '@lingui/react/macro';
|
|
7
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
8
|
+
import { RouterContextProvider } from '@tanstack/react-router';
|
|
9
|
+
import { useForm } from 'react-hook-form';
|
|
10
|
+
import { createDemoRoute } from '../../../../.storybook/providers.js';
|
|
11
|
+
import {
|
|
12
|
+
DetailFormGrid,
|
|
13
|
+
Page,
|
|
14
|
+
PageActionBar,
|
|
15
|
+
PageActionBarRight,
|
|
16
|
+
PageBlock,
|
|
17
|
+
PageLayout,
|
|
18
|
+
PageTitle,
|
|
19
|
+
} from './page-layout.js';
|
|
20
|
+
|
|
21
|
+
// Sample GraphQL query for a product detail
|
|
22
|
+
const productFragment = graphql(`
|
|
23
|
+
fragment ProductDetailForForm on Product {
|
|
24
|
+
id
|
|
25
|
+
createdAt
|
|
26
|
+
updatedAt
|
|
27
|
+
name
|
|
28
|
+
slug
|
|
29
|
+
description
|
|
30
|
+
enabled
|
|
31
|
+
}
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
const productQuery = graphql(
|
|
35
|
+
`
|
|
36
|
+
query ProductForCustomForm($id: ID!) {
|
|
37
|
+
product(id: $id) {
|
|
38
|
+
...ProductDetailForForm
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`,
|
|
42
|
+
[productFragment],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
interface ProductFormData {
|
|
46
|
+
name: string;
|
|
47
|
+
slug: string;
|
|
48
|
+
description: string;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const meta = {
|
|
53
|
+
title: 'Layout/Custom Form Page',
|
|
54
|
+
parameters: {
|
|
55
|
+
layout: 'fullscreen',
|
|
56
|
+
},
|
|
57
|
+
tags: ['autodocs'],
|
|
58
|
+
} satisfies Meta;
|
|
59
|
+
|
|
60
|
+
export default meta;
|
|
61
|
+
type Story = StoryObj<typeof meta>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* This example shows how to create a custom form page using FormFieldWrapper
|
|
65
|
+
* with a product entity. This pattern is useful when you want full control over
|
|
66
|
+
* the form layout instead of using the automated DetailPage component.
|
|
67
|
+
*/
|
|
68
|
+
export const ProductCustomForm: Story = {
|
|
69
|
+
render: () => {
|
|
70
|
+
const { route, router } = createDemoRoute();
|
|
71
|
+
const form = useForm<ProductFormData>({
|
|
72
|
+
defaultValues: {
|
|
73
|
+
name: 'Wireless Headphones',
|
|
74
|
+
slug: 'wireless-headphones',
|
|
75
|
+
description: 'High-quality wireless headphones with active noise cancellation.',
|
|
76
|
+
enabled: true,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const onSubmit = (data: ProductFormData) => {
|
|
81
|
+
console.log('Form submitted:', data);
|
|
82
|
+
// In a real app, you would call your update mutation here
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<RouterContextProvider router={router}>
|
|
87
|
+
<Page
|
|
88
|
+
pageId="product-custom-detail"
|
|
89
|
+
form={form}
|
|
90
|
+
submitHandler={form.handleSubmit(onSubmit)}
|
|
91
|
+
entity={{
|
|
92
|
+
id: '1',
|
|
93
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
94
|
+
updatedAt: '2024-01-15T00:00:00.000Z',
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<PageTitle>
|
|
98
|
+
<Trans>Product: Wireless Headphones</Trans>
|
|
99
|
+
</PageTitle>
|
|
100
|
+
<PageActionBar>
|
|
101
|
+
<PageActionBarRight>
|
|
102
|
+
<Button type="submit" disabled={!form.formState.isDirty}>
|
|
103
|
+
<Trans>Save Changes</Trans>
|
|
104
|
+
</Button>
|
|
105
|
+
</PageActionBarRight>
|
|
106
|
+
</PageActionBar>
|
|
107
|
+
<PageLayout>
|
|
108
|
+
<PageBlock
|
|
109
|
+
column="main"
|
|
110
|
+
blockId="product-details"
|
|
111
|
+
title={<Trans>Product Details</Trans>}
|
|
112
|
+
description={<Trans>Basic information about the product</Trans>}
|
|
113
|
+
>
|
|
114
|
+
<DetailFormGrid>
|
|
115
|
+
<FormFieldWrapper
|
|
116
|
+
control={form.control}
|
|
117
|
+
name="name"
|
|
118
|
+
label={<Trans>Product Name</Trans>}
|
|
119
|
+
description={<Trans>The display name of the product</Trans>}
|
|
120
|
+
render={({ field }) => <Input {...field} />}
|
|
121
|
+
/>
|
|
122
|
+
<FormFieldWrapper
|
|
123
|
+
control={form.control}
|
|
124
|
+
name="slug"
|
|
125
|
+
label={<Trans>Slug</Trans>}
|
|
126
|
+
description={<Trans>URL-friendly identifier</Trans>}
|
|
127
|
+
render={({ field }) => <Input {...field} />}
|
|
128
|
+
/>
|
|
129
|
+
</DetailFormGrid>
|
|
130
|
+
<FormFieldWrapper
|
|
131
|
+
control={form.control}
|
|
132
|
+
name="description"
|
|
133
|
+
label={<Trans>Description</Trans>}
|
|
134
|
+
render={({ field }) => <Textarea {...field} rows={4} />}
|
|
135
|
+
/>
|
|
136
|
+
</PageBlock>
|
|
137
|
+
<PageBlock column="side" blockId="product-status" title={<Trans>Status</Trans>}>
|
|
138
|
+
<FormFieldWrapper
|
|
139
|
+
control={form.control}
|
|
140
|
+
name="enabled"
|
|
141
|
+
label={<Trans>Enabled</Trans>}
|
|
142
|
+
description={<Trans>Whether this product is active</Trans>}
|
|
143
|
+
render={({ field }) => (
|
|
144
|
+
<div className="flex items-center">
|
|
145
|
+
<input
|
|
146
|
+
type="checkbox"
|
|
147
|
+
checked={field.value}
|
|
148
|
+
onChange={field.onChange}
|
|
149
|
+
className="mr-2"
|
|
150
|
+
/>
|
|
151
|
+
<span className="text-sm">
|
|
152
|
+
{field.value ? (
|
|
153
|
+
<Trans>Product is enabled</Trans>
|
|
154
|
+
) : (
|
|
155
|
+
<Trans>Product is disabled</Trans>
|
|
156
|
+
)}
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
/>
|
|
161
|
+
</PageBlock>
|
|
162
|
+
</PageLayout>
|
|
163
|
+
</Page>
|
|
164
|
+
</RouterContextProvider>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* This example shows a more complex form with multiple blocks and varied form fields.
|
|
171
|
+
*/
|
|
172
|
+
export const ComplexCustomForm: Story = {
|
|
173
|
+
render: () => {
|
|
174
|
+
const { route, router } = createDemoRoute();
|
|
175
|
+
const form = useForm({
|
|
176
|
+
defaultValues: {
|
|
177
|
+
// Basic Info
|
|
178
|
+
name: 'Premium Laptop',
|
|
179
|
+
slug: 'premium-laptop',
|
|
180
|
+
sku: 'LAPTOP-001',
|
|
181
|
+
// Pricing
|
|
182
|
+
price: 1299.99,
|
|
183
|
+
salePrice: null,
|
|
184
|
+
costPrice: 899.0,
|
|
185
|
+
// Inventory
|
|
186
|
+
stockOnHand: 50,
|
|
187
|
+
trackInventory: true,
|
|
188
|
+
// Details
|
|
189
|
+
description: 'High-performance laptop for professionals',
|
|
190
|
+
shortDescription: 'Professional laptop',
|
|
191
|
+
// SEO
|
|
192
|
+
metaTitle: '',
|
|
193
|
+
metaDescription: '',
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const onSubmit = (data: any) => {
|
|
198
|
+
console.log('Form submitted:', data);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<RouterContextProvider router={router}>
|
|
203
|
+
<Page
|
|
204
|
+
pageId="product-complex-detail"
|
|
205
|
+
form={form}
|
|
206
|
+
submitHandler={form.handleSubmit(onSubmit)}
|
|
207
|
+
entity={{ id: '2', createdAt: '2024-01-01', updatedAt: '2024-01-15' }}
|
|
208
|
+
>
|
|
209
|
+
<PageTitle>
|
|
210
|
+
<Trans>Product: Premium Laptop</Trans>
|
|
211
|
+
</PageTitle>
|
|
212
|
+
<PageActionBar>
|
|
213
|
+
<PageActionBarRight>
|
|
214
|
+
<Button variant="outline" type="button">
|
|
215
|
+
<Trans>Cancel</Trans>
|
|
216
|
+
</Button>
|
|
217
|
+
<Button type="submit" disabled={!form.formState.isDirty}>
|
|
218
|
+
<Trans>Save Changes</Trans>
|
|
219
|
+
</Button>
|
|
220
|
+
</PageActionBarRight>
|
|
221
|
+
</PageActionBar>
|
|
222
|
+
<PageLayout>
|
|
223
|
+
<PageBlock
|
|
224
|
+
column="main"
|
|
225
|
+
blockId="basic-info"
|
|
226
|
+
title={<Trans>Basic Information</Trans>}
|
|
227
|
+
>
|
|
228
|
+
<DetailFormGrid>
|
|
229
|
+
<FormFieldWrapper
|
|
230
|
+
control={form.control}
|
|
231
|
+
name="name"
|
|
232
|
+
label={<Trans>Product Name</Trans>}
|
|
233
|
+
render={({ field }) => <Input {...field} />}
|
|
234
|
+
/>
|
|
235
|
+
<FormFieldWrapper
|
|
236
|
+
control={form.control}
|
|
237
|
+
name="sku"
|
|
238
|
+
label={<Trans>SKU</Trans>}
|
|
239
|
+
render={({ field }) => <Input {...field} />}
|
|
240
|
+
/>
|
|
241
|
+
<FormFieldWrapper
|
|
242
|
+
control={form.control}
|
|
243
|
+
name="slug"
|
|
244
|
+
label={<Trans>Slug</Trans>}
|
|
245
|
+
render={({ field }) => <Input {...field} />}
|
|
246
|
+
/>
|
|
247
|
+
</DetailFormGrid>
|
|
248
|
+
</PageBlock>
|
|
249
|
+
|
|
250
|
+
<PageBlock column="main" blockId="description" title={<Trans>Description</Trans>}>
|
|
251
|
+
<FormFieldWrapper
|
|
252
|
+
control={form.control}
|
|
253
|
+
name="shortDescription"
|
|
254
|
+
label={<Trans>Short Description</Trans>}
|
|
255
|
+
render={({ field }) => <Input {...field} />}
|
|
256
|
+
/>
|
|
257
|
+
<FormFieldWrapper
|
|
258
|
+
control={form.control}
|
|
259
|
+
name="description"
|
|
260
|
+
label={<Trans>Full Description</Trans>}
|
|
261
|
+
render={({ field }) => <Textarea {...field} rows={6} />}
|
|
262
|
+
/>
|
|
263
|
+
</PageBlock>
|
|
264
|
+
|
|
265
|
+
<PageBlock column="main" blockId="pricing" title={<Trans>Pricing</Trans>}>
|
|
266
|
+
<DetailFormGrid>
|
|
267
|
+
<FormFieldWrapper
|
|
268
|
+
control={form.control}
|
|
269
|
+
name="price"
|
|
270
|
+
label={<Trans>Price</Trans>}
|
|
271
|
+
render={({ field }) => <Input {...field} type="number" step="0.01" />}
|
|
272
|
+
/>
|
|
273
|
+
<FormFieldWrapper
|
|
274
|
+
control={form.control}
|
|
275
|
+
name="salePrice"
|
|
276
|
+
label={<Trans>Sale Price</Trans>}
|
|
277
|
+
description={<Trans>Optional discounted price</Trans>}
|
|
278
|
+
render={({ field }) => (
|
|
279
|
+
<Input
|
|
280
|
+
{...field}
|
|
281
|
+
type="number"
|
|
282
|
+
step="0.01"
|
|
283
|
+
value={field.value ?? ''}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
/>
|
|
287
|
+
<FormFieldWrapper
|
|
288
|
+
control={form.control}
|
|
289
|
+
name="costPrice"
|
|
290
|
+
label={<Trans>Cost Price</Trans>}
|
|
291
|
+
description={<Trans>Your cost for this product</Trans>}
|
|
292
|
+
render={({ field }) => <Input {...field} type="number" step="0.01" />}
|
|
293
|
+
/>
|
|
294
|
+
</DetailFormGrid>
|
|
295
|
+
</PageBlock>
|
|
296
|
+
|
|
297
|
+
<PageBlock column="side" blockId="inventory" title={<Trans>Inventory</Trans>}>
|
|
298
|
+
<div className="space-y-4">
|
|
299
|
+
<FormFieldWrapper
|
|
300
|
+
control={form.control}
|
|
301
|
+
name="trackInventory"
|
|
302
|
+
label={<Trans>Track Inventory</Trans>}
|
|
303
|
+
render={({ field }) => (
|
|
304
|
+
<div className="flex items-center">
|
|
305
|
+
<input
|
|
306
|
+
type="checkbox"
|
|
307
|
+
checked={field.value}
|
|
308
|
+
onChange={field.onChange}
|
|
309
|
+
className="mr-2"
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
/>
|
|
314
|
+
<FormFieldWrapper
|
|
315
|
+
control={form.control}
|
|
316
|
+
name="stockOnHand"
|
|
317
|
+
label={<Trans>Stock on Hand</Trans>}
|
|
318
|
+
render={({ field }) => <Input {...field} type="number" />}
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
</PageBlock>
|
|
322
|
+
|
|
323
|
+
<PageBlock column="side" blockId="seo" title={<Trans>SEO</Trans>}>
|
|
324
|
+
<div className="space-y-4">
|
|
325
|
+
<FormFieldWrapper
|
|
326
|
+
control={form.control}
|
|
327
|
+
name="metaTitle"
|
|
328
|
+
label={<Trans>Meta Title</Trans>}
|
|
329
|
+
render={({ field }) => <Input {...field} />}
|
|
330
|
+
/>
|
|
331
|
+
<FormFieldWrapper
|
|
332
|
+
control={form.control}
|
|
333
|
+
name="metaDescription"
|
|
334
|
+
label={<Trans>Meta Description</Trans>}
|
|
335
|
+
render={({ field }) => <Textarea {...field} rows={3} />}
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
338
|
+
</PageBlock>
|
|
339
|
+
</PageLayout>
|
|
340
|
+
</Page>
|
|
341
|
+
</RouterContextProvider>
|
|
342
|
+
);
|
|
343
|
+
},
|
|
344
|
+
};
|
|
@@ -119,12 +119,12 @@ export function Page({ children, pageId, entity, form, submitHandler, ...props }
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function PageContent({
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
pageHeader,
|
|
123
|
+
pageContent,
|
|
124
|
+
form,
|
|
125
|
+
submitHandler,
|
|
126
|
+
...props
|
|
127
|
+
}: {
|
|
128
128
|
pageHeader: React.ReactNode;
|
|
129
129
|
pageContent: React.ReactNode;
|
|
130
130
|
form?: UseFormReturn<any>;
|
|
@@ -146,11 +146,11 @@ function PageContent({
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
export function PageContentWithOptionalForm({
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
form,
|
|
150
|
+
pageHeader,
|
|
151
|
+
pageContent,
|
|
152
|
+
submitHandler,
|
|
153
|
+
}: {
|
|
154
154
|
form?: UseFormReturn<any>;
|
|
155
155
|
pageHeader: React.ReactNode;
|
|
156
156
|
pageContent: React.ReactNode;
|
|
@@ -235,22 +235,32 @@ export function PageLayout({ children, className }: Readonly<PageLayoutProps>) {
|
|
|
235
235
|
childBlock.props.blockId ??
|
|
236
236
|
(isOfType(childBlock, CustomFieldsPageBlock) ? 'custom-fields' : undefined);
|
|
237
237
|
const extensionBlock = extensionBlocks.find(block => block.location.position.blockId === blockId);
|
|
238
|
+
|
|
238
239
|
if (extensionBlock) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
240
|
+
let extensionBlockShouldRender = true;
|
|
241
|
+
if (typeof extensionBlock?.shouldRender === 'function') {
|
|
242
|
+
extensionBlockShouldRender = extensionBlock.shouldRender(page);
|
|
243
|
+
}
|
|
244
|
+
const ExtensionBlock =
|
|
245
|
+
extensionBlock.component && extensionBlockShouldRender ? (
|
|
246
|
+
<PageBlock
|
|
247
|
+
key={childBlock.key}
|
|
248
|
+
column={extensionBlock.location.column}
|
|
249
|
+
blockId={extensionBlock.id}
|
|
250
|
+
title={extensionBlock.title}
|
|
251
|
+
>
|
|
252
|
+
{<extensionBlock.component context={page} />}
|
|
253
|
+
</PageBlock>
|
|
254
|
+
) : undefined;
|
|
249
255
|
if (extensionBlock.location.position.order === 'before') {
|
|
250
|
-
finalChildArray.push(ExtensionBlock, childBlock);
|
|
256
|
+
finalChildArray.push(...[ExtensionBlock, childBlock].filter(x => !!x));
|
|
251
257
|
} else if (extensionBlock.location.position.order === 'after') {
|
|
252
|
-
finalChildArray.push(childBlock, ExtensionBlock);
|
|
253
|
-
} else if (
|
|
258
|
+
finalChildArray.push(...[childBlock, ExtensionBlock].filter(x => !!x));
|
|
259
|
+
} else if (
|
|
260
|
+
extensionBlock.location.position.order === 'replace' &&
|
|
261
|
+
extensionBlockShouldRender &&
|
|
262
|
+
ExtensionBlock
|
|
263
|
+
) {
|
|
254
264
|
finalChildArray.push(ExtensionBlock);
|
|
255
265
|
}
|
|
256
266
|
} else {
|
|
@@ -266,17 +276,17 @@ export function PageLayout({ children, className }: Readonly<PageLayoutProps>) {
|
|
|
266
276
|
const sideBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'side');
|
|
267
277
|
|
|
268
278
|
return (
|
|
269
|
-
<div className={cn('w-full space-y-4', className)}>
|
|
279
|
+
<div className={cn('w-full space-y-4', className, '@container/layout')}>
|
|
270
280
|
{isDesktop ? (
|
|
271
|
-
<div className="
|
|
281
|
+
<div className="grid grid-cols-1 gap-4 @3xl/layout:grid-cols-4">
|
|
272
282
|
{fullWidthBlocks.length > 0 && (
|
|
273
|
-
<div className="md:col-span-5 space-y-4">{fullWidthBlocks}</div>
|
|
283
|
+
<div className="@md/layout:col-span-5 space-y-4">{fullWidthBlocks}</div>
|
|
274
284
|
)}
|
|
275
|
-
<div className="
|
|
276
|
-
<div className="
|
|
285
|
+
<div className="@3xl/layout:col-span-3 space-y-4">{mainBlocks}</div>
|
|
286
|
+
<div className="@3xl/layout:col-span-1 space-y-4">{sideBlocks}</div>
|
|
277
287
|
</div>
|
|
278
288
|
) : (
|
|
279
|
-
<div className="
|
|
289
|
+
<div className="space-y-4">{children}</div>
|
|
280
290
|
)}
|
|
281
291
|
</div>
|
|
282
292
|
);
|
|
@@ -425,9 +435,9 @@ function EntityInfoDropdown({ entity }: Readonly<{ entity: any }>) {
|
|
|
425
435
|
* @since 3.3.0
|
|
426
436
|
*/
|
|
427
437
|
export function PageActionBarRight({
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
438
|
+
children,
|
|
439
|
+
dropdownMenuItems,
|
|
440
|
+
}: Readonly<{
|
|
431
441
|
children: React.ReactNode;
|
|
432
442
|
dropdownMenuItems?: InlineDropdownItem[];
|
|
433
443
|
}>) {
|
|
@@ -458,9 +468,9 @@ export function PageActionBarRight({
|
|
|
458
468
|
}
|
|
459
469
|
|
|
460
470
|
function PageActionBarItem({
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
471
|
+
item,
|
|
472
|
+
page,
|
|
473
|
+
}: Readonly<{ item: DashboardActionBarItem; page: PageContextValue }>) {
|
|
464
474
|
return (
|
|
465
475
|
<PermissionGuard requires={item.requiresPermission ?? []}>
|
|
466
476
|
<item.component context={page} />
|
|
@@ -469,9 +479,9 @@ function PageActionBarItem({
|
|
|
469
479
|
}
|
|
470
480
|
|
|
471
481
|
function PageActionBarDropdown({
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
482
|
+
items,
|
|
483
|
+
page,
|
|
484
|
+
}: Readonly<{ items: DashboardActionBarItem[]; page: PageContextValue }>) {
|
|
475
485
|
return (
|
|
476
486
|
<DropdownMenu>
|
|
477
487
|
<DropdownMenuTrigger asChild>
|
|
@@ -550,13 +560,13 @@ export type PageBlockProps = {
|
|
|
550
560
|
* @since 3.3.0
|
|
551
561
|
*/
|
|
552
562
|
export function PageBlock({
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
563
|
+
children,
|
|
564
|
+
title,
|
|
565
|
+
description,
|
|
566
|
+
className,
|
|
567
|
+
blockId,
|
|
568
|
+
column,
|
|
569
|
+
}: Readonly<PageBlockProps>) {
|
|
560
570
|
const contextValue = useMemo(
|
|
561
571
|
() => ({
|
|
562
572
|
blockId,
|
|
@@ -569,14 +579,16 @@ export function PageBlock({
|
|
|
569
579
|
return (
|
|
570
580
|
<PageBlockContext.Provider value={contextValue}>
|
|
571
581
|
<LocationWrapper>
|
|
572
|
-
<Card className={cn('@container w-full', className)}>
|
|
582
|
+
<Card className={cn('@container w-full', className, 'animate-in fade-in duration-300')}>
|
|
573
583
|
{title || description ? (
|
|
574
584
|
<CardHeader>
|
|
575
585
|
{title && <CardTitle>{title}</CardTitle>}
|
|
576
586
|
{description && <CardDescription>{description}</CardDescription>}
|
|
577
587
|
</CardHeader>
|
|
578
588
|
) : null}
|
|
579
|
-
<CardContent className={cn(!title ? 'pt-6' : '')}>
|
|
589
|
+
<CardContent className={cn(!title ? 'pt-6' : '', 'overflow-auto')}>
|
|
590
|
+
{children}
|
|
591
|
+
</CardContent>
|
|
580
592
|
</Card>
|
|
581
593
|
</LocationWrapper>
|
|
582
594
|
</PageBlockContext.Provider>
|
|
@@ -595,15 +607,15 @@ export function PageBlock({
|
|
|
595
607
|
* @since 3.3.0
|
|
596
608
|
*/
|
|
597
609
|
export function FullWidthPageBlock({
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
610
|
+
children,
|
|
611
|
+
className,
|
|
612
|
+
blockId,
|
|
613
|
+
}: Readonly<Pick<PageBlockProps, 'children' | 'className' | 'blockId'>>) {
|
|
602
614
|
const contextValue = useMemo(() => ({ blockId, column: 'main' as const }), [blockId]);
|
|
603
615
|
return (
|
|
604
616
|
<PageBlockContext.Provider value={contextValue}>
|
|
605
617
|
<LocationWrapper>
|
|
606
|
-
<div className={cn('w-full', className)}>{children}</div>
|
|
618
|
+
<div className={cn('w-full', className, 'animate-in fade-in duration-300')}>{children}</div>
|
|
607
619
|
</LocationWrapper>
|
|
608
620
|
</PageBlockContext.Provider>
|
|
609
621
|
);
|
|
@@ -625,10 +637,10 @@ export function FullWidthPageBlock({
|
|
|
625
637
|
* @since 3.3.0
|
|
626
638
|
*/
|
|
627
639
|
export function CustomFieldsPageBlock({
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
640
|
+
column,
|
|
641
|
+
entityType,
|
|
642
|
+
control,
|
|
643
|
+
}: Readonly<{
|
|
632
644
|
column: 'main' | 'side';
|
|
633
645
|
entityType: string;
|
|
634
646
|
control: Control<any, any>;
|