@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.
Files changed (220) hide show
  1. package/dist/plugin/dashboard.plugin.js +1 -1
  2. package/dist/plugin/default-page.html +1 -1
  3. package/dist/vite/utils/ast-utils.spec.js +3 -3
  4. package/dist/vite/utils/tsconfig-utils.js +2 -1
  5. package/dist/vite/vite-plugin-hmr.d.ts +8 -0
  6. package/dist/vite/vite-plugin-hmr.js +34 -0
  7. package/dist/vite/vite-plugin-theme.js +6 -6
  8. package/dist/vite/vite-plugin-transform-index.js +6 -1
  9. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +31 -4
  10. package/dist/vite/vite-plugin-vendure-dashboard.js +89 -34
  11. package/package.json +18 -5
  12. package/src/app/app-providers.tsx +4 -1
  13. package/src/app/common/map-faceted-filter-fields.ts +21 -0
  14. package/src/app/main.tsx +3 -1
  15. package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +2 -2
  16. package/src/app/routes/_authenticated/_administrators/administrators.tsx +13 -3
  17. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +6 -13
  18. package/src/app/routes/_authenticated/_administrators/components/role-permissions-display.tsx +1 -1
  19. package/src/app/routes/_authenticated/_assets/assets.tsx +17 -1
  20. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
  21. package/src/app/routes/_authenticated/_collections/collections.tsx +5 -0
  22. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +0 -1
  23. package/src/app/routes/_authenticated/_customers/customers.tsx +9 -5
  24. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +0 -6
  25. package/src/app/routes/_authenticated/_facets/components/facet-value-bulk-actions.tsx +16 -0
  26. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +43 -12
  27. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +14 -5
  28. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +4 -8
  29. package/src/app/routes/_authenticated/_global-settings/utils/global-languages.ts +268 -0
  30. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +117 -92
  31. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +15 -15
  32. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +5 -5
  33. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +2 -1
  34. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +26 -27
  35. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +5 -3
  36. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +6 -9
  37. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +17 -1
  38. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +48 -281
  39. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +59 -40
  40. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +73 -0
  41. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +312 -0
  42. package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +2 -2
  43. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +4 -0
  44. package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +49 -0
  45. package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +56 -0
  46. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +12 -0
  47. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +178 -50
  48. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +0 -6
  49. package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +0 -11
  50. package/src/app/routes/_authenticated/_products/products.tsx +6 -2
  51. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +4 -8
  52. package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +0 -10
  53. package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +2 -2
  54. package/src/app/routes/_authenticated/_promotions/promotions.tsx +12 -0
  55. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +3 -10
  56. package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +2 -2
  57. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +2 -2
  58. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +4 -0
  59. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +4 -10
  60. package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +2 -2
  61. package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +2 -2
  62. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -0
  63. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -0
  64. package/src/app/routes/_authenticated/_zones/zones.graphql.ts +2 -2
  65. package/src/app/routes/login.tsx +2 -2
  66. package/src/i18n/locales/ar.po +420 -289
  67. package/src/i18n/locales/cs.po +420 -289
  68. package/src/i18n/locales/de.po +420 -289
  69. package/src/i18n/locales/en.po +420 -289
  70. package/src/i18n/locales/es.po +420 -289
  71. package/src/i18n/locales/fa.po +420 -289
  72. package/src/i18n/locales/fr.po +468 -337
  73. package/src/i18n/locales/he.po +420 -289
  74. package/src/i18n/locales/hr.po +420 -289
  75. package/src/i18n/locales/it.po +420 -289
  76. package/src/i18n/locales/ja.po +420 -289
  77. package/src/i18n/locales/nb.po +420 -289
  78. package/src/i18n/locales/ne.po +420 -289
  79. package/src/i18n/locales/pl.po +420 -289
  80. package/src/i18n/locales/pt_BR.po +420 -289
  81. package/src/i18n/locales/pt_PT.po +420 -289
  82. package/src/i18n/locales/ru.po +420 -289
  83. package/src/i18n/locales/sv.po +420 -289
  84. package/src/i18n/locales/tr.po +420 -289
  85. package/src/i18n/locales/uk.po +420 -289
  86. package/src/i18n/locales/zh_Hans.po +420 -289
  87. package/src/i18n/locales/zh_Hant.po +420 -289
  88. package/src/lib/components/data-input/affixed-input.stories.tsx +93 -0
  89. package/src/lib/components/data-input/affixed-input.tsx +5 -2
  90. package/src/lib/components/data-input/boolean-input.stories.tsx +102 -0
  91. package/src/lib/components/data-input/checkbox-input.stories.tsx +61 -0
  92. package/src/lib/components/data-input/customer-group-input.tsx +0 -1
  93. package/src/lib/components/data-input/datetime-input.stories.tsx +62 -0
  94. package/src/lib/components/data-input/datetime-input.tsx +27 -13
  95. package/src/lib/components/data-input/default-relation-input.tsx +18 -12
  96. package/src/lib/components/data-input/money-input.stories.tsx +88 -0
  97. package/src/lib/components/data-input/money-input.tsx +7 -11
  98. package/src/lib/components/data-input/number-input.stories.tsx +103 -0
  99. package/src/lib/components/data-input/number-input.tsx +16 -5
  100. package/src/lib/components/data-input/password-input.stories.tsx +65 -0
  101. package/src/lib/components/data-input/rich-text-input.stories.tsx +92 -0
  102. package/src/lib/components/data-input/slug-input.stories.tsx +232 -0
  103. package/src/lib/components/data-input/slug-input.tsx +9 -10
  104. package/src/lib/components/data-input/text-input.stories.tsx +52 -0
  105. package/src/lib/components/data-input/textarea-input.stories.tsx +55 -0
  106. package/src/lib/components/data-table/add-filter-menu.tsx +6 -1
  107. package/src/lib/components/data-table/column-header-wrapper.tsx +106 -0
  108. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +11 -9
  109. package/src/lib/components/data-table/data-table-bulk-actions.tsx +4 -4
  110. package/src/lib/components/data-table/data-table-column-header.tsx +17 -14
  111. package/src/lib/components/data-table/data-table-faceted-filter.tsx +33 -11
  112. package/src/lib/components/data-table/data-table-filter-badge-editable.tsx +35 -0
  113. package/src/lib/components/data-table/data-table-filter-badge.tsx +28 -14
  114. package/src/lib/components/data-table/data-table-filter-dialog.tsx +28 -8
  115. package/src/lib/components/data-table/data-table-pagination.tsx +23 -7
  116. package/src/lib/components/data-table/data-table.stories.tsx +249 -0
  117. package/src/lib/components/data-table/data-table.tsx +39 -11
  118. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +79 -34
  119. package/src/lib/components/data-table/use-generated-columns.tsx +55 -27
  120. package/src/lib/components/layout/generated-breadcrumbs.tsx +4 -12
  121. package/src/lib/components/layout/nav-user.tsx +19 -13
  122. package/src/lib/components/login/login-form.tsx +39 -123
  123. package/src/lib/components/shared/alerts.tsx +29 -17
  124. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +3 -3
  125. package/src/lib/components/shared/asset/asset-gallery.stories.tsx +76 -0
  126. package/src/lib/components/shared/asset/asset-gallery.tsx +147 -113
  127. package/src/lib/components/shared/asset/asset-picker-dialog.stories.tsx +58 -0
  128. package/src/lib/components/shared/configurable-operation-input.tsx +1 -1
  129. package/src/lib/components/shared/customer-group-selector.tsx +5 -2
  130. package/src/lib/components/shared/detail-page-button.stories.tsx +52 -0
  131. package/src/lib/components/shared/facet-value-selector.stories.tsx +48 -0
  132. package/src/lib/components/shared/facet-value-selector.tsx +130 -34
  133. package/src/lib/components/shared/paginated-list-data-table.stories.tsx +212 -0
  134. package/src/lib/components/shared/paginated-list-data-table.tsx +12 -12
  135. package/src/lib/components/shared/permission-guard.stories.tsx +46 -0
  136. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -0
  137. package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +8 -4
  138. package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +1 -0
  139. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +40 -0
  140. package/src/lib/components/shared/vendure-image.stories.tsx +167 -0
  141. package/src/lib/components/shared/vendure-image.tsx +6 -7
  142. package/src/lib/components/ui/accordion.stories.tsx +33 -0
  143. package/src/lib/components/ui/alert-dialog.stories.tsx +48 -0
  144. package/src/lib/components/ui/alert.stories.tsx +35 -0
  145. package/src/lib/components/ui/aspect-ratio.stories.tsx +28 -0
  146. package/src/lib/components/ui/badge.stories.tsx +28 -0
  147. package/src/lib/components/ui/breadcrumb.stories.tsx +41 -0
  148. package/src/lib/components/ui/button.stories.tsx +38 -0
  149. package/src/lib/components/ui/calendar.stories.tsx +22 -0
  150. package/src/lib/components/ui/card.stories.tsx +28 -0
  151. package/src/lib/components/ui/carousel.stories.tsx +34 -0
  152. package/src/lib/components/ui/checkbox.stories.tsx +31 -0
  153. package/src/lib/components/ui/collapsible.stories.tsx +39 -0
  154. package/src/lib/components/ui/command.stories.tsx +44 -0
  155. package/src/lib/components/ui/context-menu.stories.tsx +38 -0
  156. package/src/lib/components/ui/dialog.stories.tsx +52 -0
  157. package/src/lib/components/ui/drawer.stories.tsx +50 -0
  158. package/src/lib/components/ui/dropdown-menu.stories.tsx +41 -0
  159. package/src/lib/components/ui/hover-card.stories.tsx +38 -0
  160. package/src/lib/components/ui/input-group.tsx +148 -0
  161. package/src/lib/components/ui/input-otp.stories.tsx +30 -0
  162. package/src/lib/components/ui/input.stories.tsx +38 -0
  163. package/src/lib/components/ui/label.stories.tsx +24 -0
  164. package/src/lib/components/ui/menubar.stories.tsx +53 -0
  165. package/src/lib/components/ui/navigation-menu.stories.tsx +54 -0
  166. package/src/lib/components/ui/pagination.stories.tsx +51 -0
  167. package/src/lib/components/ui/password-input.stories.tsx +32 -0
  168. package/src/lib/components/ui/password-input.tsx +33 -0
  169. package/src/lib/components/ui/popover.stories.tsx +33 -0
  170. package/src/lib/components/ui/progress.stories.tsx +27 -0
  171. package/src/lib/components/ui/radio-group.stories.tsx +34 -0
  172. package/src/lib/components/ui/resizable.stories.tsx +32 -0
  173. package/src/lib/components/ui/scroll-area.stories.tsx +31 -0
  174. package/src/lib/components/ui/select.stories.tsx +36 -0
  175. package/src/lib/components/ui/separator.stories.tsx +35 -0
  176. package/src/lib/components/ui/sheet.stories.tsx +50 -0
  177. package/src/lib/components/ui/sidebar-context.ts +16 -0
  178. package/src/lib/components/ui/sidebar.tsx +2 -13
  179. package/src/lib/components/ui/skeleton.stories.tsx +26 -0
  180. package/src/lib/components/ui/slider.stories.tsx +37 -0
  181. package/src/lib/components/ui/switch.stories.tsx +31 -0
  182. package/src/lib/components/ui/table.stories.tsx +52 -0
  183. package/src/lib/components/ui/tabs.stories.tsx +29 -0
  184. package/src/lib/components/ui/textarea.stories.tsx +32 -0
  185. package/src/lib/components/ui/toggle-group.stories.tsx +31 -0
  186. package/src/lib/components/ui/toggle.stories.tsx +39 -0
  187. package/src/lib/components/ui/tooltip.stories.tsx +30 -0
  188. package/src/lib/components/ui/tooltip.tsx +2 -2
  189. package/src/lib/framework/alert/alert-extensions.tsx +0 -11
  190. package/src/lib/framework/alert/alert-item.tsx +14 -19
  191. package/src/lib/framework/alert/alerts-indicator.tsx +14 -15
  192. package/src/lib/framework/alert/search-index-buffer-alert/search-index-buffer-alert.ts +41 -0
  193. package/src/lib/framework/component-registry/component-registry.tsx +3 -14
  194. package/src/lib/framework/dashboard-widget/base-widget.tsx +18 -9
  195. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +0 -2
  196. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +12 -11
  197. package/src/lib/framework/defaults.ts +9 -13
  198. package/src/lib/framework/extension-api/input-component-extensions.tsx +6 -1
  199. package/src/lib/framework/extension-api/logic/alerts.ts +3 -2
  200. package/src/lib/framework/extension-api/types/alerts.ts +12 -6
  201. package/src/lib/framework/extension-api/types/data-table.ts +5 -2
  202. package/src/lib/framework/extension-api/types/layout.ts +41 -1
  203. package/src/lib/framework/extension-api/types/login.ts +0 -21
  204. package/src/lib/framework/form-engine/value-transformers.ts +8 -1
  205. package/src/lib/framework/layout-engine/custom-form-page.stories.tsx +344 -0
  206. package/src/lib/framework/layout-engine/page-layout.tsx +69 -57
  207. package/src/lib/framework/layout-engine/page.stories.tsx +275 -0
  208. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +32 -19
  209. package/src/lib/framework/page/detail-page.stories.tsx +151 -0
  210. package/src/lib/framework/page/detail-page.tsx +12 -15
  211. package/src/lib/framework/page/list-page.stories.tsx +217 -0
  212. package/src/lib/framework/page/list-page.tsx +8 -1
  213. package/src/lib/graphql/api.ts +18 -1
  214. package/src/lib/graphql/graphql-env.d.ts +1 -1
  215. package/src/lib/hooks/use-alerts.ts +84 -0
  216. package/src/lib/hooks/use-floating-bulk-actions.ts +2 -3
  217. package/src/lib/index.ts +12 -5
  218. package/src/lib/providers/alerts-provider.tsx +60 -0
  219. package/src/lib/providers/channel-provider.tsx +1 -0
  220. 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 (!value) {
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
- pageHeader,
123
- pageContent,
124
- form,
125
- submitHandler,
126
- ...props
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
- form,
150
- pageHeader,
151
- pageContent,
152
- submitHandler,
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
- const ExtensionBlock = (
240
- <PageBlock
241
- key={childBlock.key}
242
- column={extensionBlock.location.column}
243
- blockId={extensionBlock.id}
244
- title={extensionBlock.title}
245
- >
246
- {<extensionBlock.component context={page} />}
247
- </PageBlock>
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 (extensionBlock.location.position.order === 'replace') {
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="hidden md:grid md:grid-cols-5 lg:grid-cols-4 md:gap-4">
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="md:col-span-3 space-y-4">{mainBlocks}</div>
276
- <div className="md:col-span-2 lg:col-span-1 space-y-4">{sideBlocks}</div>
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="md:hidden space-y-4">{children}</div>
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
- children,
429
- dropdownMenuItems,
430
- }: Readonly<{
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
- item,
462
- page,
463
- }: Readonly<{ item: DashboardActionBarItem; page: PageContextValue }>) {
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
- items,
473
- page,
474
- }: Readonly<{ items: DashboardActionBarItem[]; page: PageContextValue }>) {
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
- children,
554
- title,
555
- description,
556
- className,
557
- blockId,
558
- column,
559
- }: Readonly<PageBlockProps>) {
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' : '')}>{children}</CardContent>
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
- children,
599
- className,
600
- blockId,
601
- }: Readonly<Pick<PageBlockProps, 'children' | 'className' | 'blockId'>>) {
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
- column,
629
- entityType,
630
- control,
631
- }: Readonly<{
640
+ column,
641
+ entityType,
642
+ control,
643
+ }: Readonly<{
632
644
  column: 'main' | 'side';
633
645
  entityType: string;
634
646
  control: Control<any, any>;