@vendure/dashboard 3.5.0-minor-202510071456 → 3.5.0-minor-202510201346

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 (204) hide show
  1. package/dist/plugin/dashboard.plugin.js +1 -1
  2. package/dist/vite/utils/ast-utils.spec.js +3 -3
  3. package/dist/vite/vite-plugin-hmr.d.ts +8 -0
  4. package/dist/vite/vite-plugin-hmr.js +34 -0
  5. package/dist/vite/vite-plugin-theme.js +6 -6
  6. package/dist/vite/vite-plugin-transform-index.js +6 -1
  7. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +31 -4
  8. package/dist/vite/vite-plugin-vendure-dashboard.js +89 -34
  9. package/package.json +17 -5
  10. package/src/app/app-providers.tsx +4 -1
  11. package/src/app/common/map-faceted-filter-fields.ts +21 -0
  12. package/src/app/main.tsx +3 -1
  13. package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +2 -2
  14. package/src/app/routes/_authenticated/_administrators/administrators.tsx +13 -3
  15. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +6 -13
  16. package/src/app/routes/_authenticated/_administrators/components/role-permissions-display.tsx +1 -1
  17. package/src/app/routes/_authenticated/_assets/assets.tsx +17 -1
  18. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
  19. package/src/app/routes/_authenticated/_collections/collections.tsx +5 -0
  20. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +0 -1
  21. package/src/app/routes/_authenticated/_customers/customers.tsx +9 -5
  22. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +0 -6
  23. package/src/app/routes/_authenticated/_facets/components/facet-value-bulk-actions.tsx +16 -0
  24. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +43 -12
  25. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +14 -5
  26. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +117 -92
  27. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +1 -1
  28. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +2 -1
  29. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +26 -27
  30. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +5 -3
  31. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +6 -9
  32. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +17 -1
  33. package/src/app/routes/_authenticated/_orders/orders.tsx +2 -0
  34. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +48 -281
  35. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +59 -40
  36. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +73 -0
  37. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +312 -0
  38. package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +2 -2
  39. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +4 -0
  40. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +2 -0
  41. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +0 -6
  42. package/src/app/routes/_authenticated/_products/products.tsx +6 -2
  43. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +4 -8
  44. package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +0 -10
  45. package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +2 -2
  46. package/src/app/routes/_authenticated/_promotions/promotions.tsx +12 -0
  47. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -2
  48. package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +2 -2
  49. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +2 -2
  50. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +4 -0
  51. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +4 -10
  52. package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +2 -2
  53. package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +2 -2
  54. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -0
  55. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -0
  56. package/src/app/routes/_authenticated/_zones/zones.graphql.ts +2 -2
  57. package/src/app/routes/login.tsx +2 -2
  58. package/src/i18n/locales/ar.po +420 -289
  59. package/src/i18n/locales/cs.po +420 -289
  60. package/src/i18n/locales/de.po +420 -289
  61. package/src/i18n/locales/en.po +420 -289
  62. package/src/i18n/locales/es.po +420 -289
  63. package/src/i18n/locales/fa.po +420 -289
  64. package/src/i18n/locales/fr.po +468 -337
  65. package/src/i18n/locales/he.po +420 -289
  66. package/src/i18n/locales/hr.po +420 -289
  67. package/src/i18n/locales/it.po +420 -289
  68. package/src/i18n/locales/ja.po +420 -289
  69. package/src/i18n/locales/nb.po +420 -289
  70. package/src/i18n/locales/ne.po +420 -289
  71. package/src/i18n/locales/pl.po +420 -289
  72. package/src/i18n/locales/pt_BR.po +420 -289
  73. package/src/i18n/locales/pt_PT.po +420 -289
  74. package/src/i18n/locales/ru.po +420 -289
  75. package/src/i18n/locales/sv.po +420 -289
  76. package/src/i18n/locales/tr.po +420 -289
  77. package/src/i18n/locales/uk.po +420 -289
  78. package/src/i18n/locales/zh_Hans.po +420 -289
  79. package/src/i18n/locales/zh_Hant.po +420 -289
  80. package/src/lib/components/data-input/affixed-input.stories.tsx +93 -0
  81. package/src/lib/components/data-input/affixed-input.tsx +5 -2
  82. package/src/lib/components/data-input/boolean-input.stories.tsx +102 -0
  83. package/src/lib/components/data-input/checkbox-input.stories.tsx +61 -0
  84. package/src/lib/components/data-input/datetime-input.stories.tsx +62 -0
  85. package/src/lib/components/data-input/datetime-input.tsx +27 -13
  86. package/src/lib/components/data-input/default-relation-input.tsx +18 -12
  87. package/src/lib/components/data-input/money-input.stories.tsx +88 -0
  88. package/src/lib/components/data-input/number-input.stories.tsx +103 -0
  89. package/src/lib/components/data-input/number-input.tsx +10 -4
  90. package/src/lib/components/data-input/password-form-input.stories.tsx +65 -0
  91. package/src/lib/components/data-input/{password-input.tsx → password-form-input.tsx} +1 -1
  92. package/src/lib/components/data-input/rich-text-input.stories.tsx +92 -0
  93. package/src/lib/components/data-input/slug-input.stories.tsx +232 -0
  94. package/src/lib/components/data-input/slug-input.tsx +9 -10
  95. package/src/lib/components/data-input/text-input.stories.tsx +52 -0
  96. package/src/lib/components/data-input/textarea-input.stories.tsx +55 -0
  97. package/src/lib/components/data-table/add-filter-menu.tsx +6 -1
  98. package/src/lib/components/data-table/column-header-wrapper.tsx +106 -0
  99. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +11 -9
  100. package/src/lib/components/data-table/data-table-bulk-actions.tsx +4 -4
  101. package/src/lib/components/data-table/data-table-column-header.tsx +17 -14
  102. package/src/lib/components/data-table/data-table-faceted-filter.tsx +33 -11
  103. package/src/lib/components/data-table/data-table-filter-badge-editable.tsx +35 -0
  104. package/src/lib/components/data-table/data-table-filter-badge.tsx +23 -16
  105. package/src/lib/components/data-table/data-table-filter-dialog.tsx +28 -8
  106. package/src/lib/components/data-table/data-table-pagination.tsx +23 -7
  107. package/src/lib/components/data-table/data-table.stories.tsx +249 -0
  108. package/src/lib/components/data-table/data-table.tsx +37 -9
  109. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +79 -34
  110. package/src/lib/components/data-table/use-generated-columns.tsx +55 -27
  111. package/src/lib/components/layout/nav-user.tsx +19 -13
  112. package/src/lib/components/login/login-form.tsx +39 -123
  113. package/src/lib/components/shared/alerts.tsx +29 -17
  114. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +3 -3
  115. package/src/lib/components/shared/asset/asset-gallery.stories.tsx +76 -0
  116. package/src/lib/components/shared/asset/asset-gallery.tsx +147 -113
  117. package/src/lib/components/shared/asset/asset-picker-dialog.stories.tsx +58 -0
  118. package/src/lib/components/shared/customer-group-selector.tsx +5 -2
  119. package/src/lib/components/shared/detail-page-button.stories.tsx +52 -0
  120. package/src/lib/components/shared/facet-value-selector.stories.tsx +48 -0
  121. package/src/lib/components/shared/facet-value-selector.tsx +130 -34
  122. package/src/lib/components/shared/paginated-list-data-table.stories.tsx +212 -0
  123. package/src/lib/components/shared/paginated-list-data-table.tsx +12 -12
  124. package/src/lib/components/shared/permission-guard.stories.tsx +46 -0
  125. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -0
  126. package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +8 -4
  127. package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +1 -0
  128. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +40 -0
  129. package/src/lib/components/shared/vendure-image.stories.tsx +167 -0
  130. package/src/lib/components/shared/vendure-image.tsx +6 -7
  131. package/src/lib/components/ui/accordion.stories.tsx +33 -0
  132. package/src/lib/components/ui/alert-dialog.stories.tsx +48 -0
  133. package/src/lib/components/ui/alert.stories.tsx +35 -0
  134. package/src/lib/components/ui/aspect-ratio.stories.tsx +28 -0
  135. package/src/lib/components/ui/badge.stories.tsx +28 -0
  136. package/src/lib/components/ui/breadcrumb.stories.tsx +41 -0
  137. package/src/lib/components/ui/button.stories.tsx +38 -0
  138. package/src/lib/components/ui/calendar.stories.tsx +22 -0
  139. package/src/lib/components/ui/card.stories.tsx +28 -0
  140. package/src/lib/components/ui/carousel.stories.tsx +34 -0
  141. package/src/lib/components/ui/checkbox.stories.tsx +31 -0
  142. package/src/lib/components/ui/collapsible.stories.tsx +39 -0
  143. package/src/lib/components/ui/command.stories.tsx +44 -0
  144. package/src/lib/components/ui/context-menu.stories.tsx +38 -0
  145. package/src/lib/components/ui/dialog.stories.tsx +52 -0
  146. package/src/lib/components/ui/drawer.stories.tsx +50 -0
  147. package/src/lib/components/ui/dropdown-menu.stories.tsx +41 -0
  148. package/src/lib/components/ui/hover-card.stories.tsx +38 -0
  149. package/src/lib/components/ui/input-group.tsx +148 -0
  150. package/src/lib/components/ui/input-otp.stories.tsx +30 -0
  151. package/src/lib/components/ui/input.stories.tsx +38 -0
  152. package/src/lib/components/ui/label.stories.tsx +24 -0
  153. package/src/lib/components/ui/menubar.stories.tsx +53 -0
  154. package/src/lib/components/ui/navigation-menu.stories.tsx +54 -0
  155. package/src/lib/components/ui/pagination.stories.tsx +51 -0
  156. package/src/lib/components/ui/password-input.stories.tsx +32 -0
  157. package/src/lib/components/ui/password-input.tsx +29 -0
  158. package/src/lib/components/ui/popover.stories.tsx +33 -0
  159. package/src/lib/components/ui/progress.stories.tsx +27 -0
  160. package/src/lib/components/ui/radio-group.stories.tsx +34 -0
  161. package/src/lib/components/ui/resizable.stories.tsx +32 -0
  162. package/src/lib/components/ui/scroll-area.stories.tsx +31 -0
  163. package/src/lib/components/ui/select.stories.tsx +36 -0
  164. package/src/lib/components/ui/separator.stories.tsx +35 -0
  165. package/src/lib/components/ui/sheet.stories.tsx +50 -0
  166. package/src/lib/components/ui/sidebar-context.ts +16 -0
  167. package/src/lib/components/ui/sidebar.tsx +2 -13
  168. package/src/lib/components/ui/skeleton.stories.tsx +26 -0
  169. package/src/lib/components/ui/slider.stories.tsx +37 -0
  170. package/src/lib/components/ui/switch.stories.tsx +31 -0
  171. package/src/lib/components/ui/table.stories.tsx +52 -0
  172. package/src/lib/components/ui/tabs.stories.tsx +29 -0
  173. package/src/lib/components/ui/textarea.stories.tsx +32 -0
  174. package/src/lib/components/ui/toggle-group.stories.tsx +31 -0
  175. package/src/lib/components/ui/toggle.stories.tsx +39 -0
  176. package/src/lib/components/ui/tooltip.stories.tsx +30 -0
  177. package/src/lib/components/ui/tooltip.tsx +2 -2
  178. package/src/lib/framework/alert/alert-extensions.tsx +0 -11
  179. package/src/lib/framework/alert/alert-item.tsx +14 -19
  180. package/src/lib/framework/alert/alerts-indicator.tsx +14 -15
  181. package/src/lib/framework/alert/search-index-buffer-alert/search-index-buffer-alert.ts +41 -0
  182. package/src/lib/framework/component-registry/component-registry.tsx +3 -14
  183. package/src/lib/framework/dashboard-widget/base-widget.tsx +18 -9
  184. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +12 -11
  185. package/src/lib/framework/defaults.ts +9 -13
  186. package/src/lib/framework/extension-api/input-component-extensions.tsx +8 -3
  187. package/src/lib/framework/extension-api/logic/alerts.ts +3 -2
  188. package/src/lib/framework/extension-api/types/alerts.ts +12 -6
  189. package/src/lib/framework/extension-api/types/data-table.ts +5 -2
  190. package/src/lib/framework/extension-api/types/login.ts +0 -21
  191. package/src/lib/framework/layout-engine/custom-form-page.stories.tsx +344 -0
  192. package/src/lib/framework/layout-engine/page-layout.tsx +11 -9
  193. package/src/lib/framework/layout-engine/page.stories.tsx +275 -0
  194. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +32 -19
  195. package/src/lib/framework/page/detail-page.stories.tsx +151 -0
  196. package/src/lib/framework/page/list-page.stories.tsx +217 -0
  197. package/src/lib/framework/page/list-page.tsx +8 -1
  198. package/src/lib/graphql/api.ts +18 -1
  199. package/src/lib/graphql/graphql-env.d.ts +1 -1
  200. package/src/lib/hooks/use-alerts.ts +84 -0
  201. package/src/lib/hooks/use-floating-bulk-actions.ts +2 -3
  202. package/src/lib/index.ts +14 -1
  203. package/src/lib/providers/alerts-provider.tsx +60 -0
  204. package/src/lib/providers/theme-provider.tsx +6 -3
@@ -4,6 +4,12 @@ import { Input } from '@/vdb/components/ui/input.js';
4
4
  import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
5
5
  import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
6
6
 
7
+ export type NumberInputProps = DashboardFormComponentProps & {
8
+ min?: number;
9
+ max?: number;
10
+ step?: number;
11
+ };
12
+
7
13
  /**
8
14
  * @description
9
15
  * A component for displaying a numeric value.
@@ -11,12 +17,12 @@ import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
11
17
  * @docsCategory form-components
12
18
  * @docsPage NumberInput
13
19
  */
14
- export function NumberInput({ fieldDef, onChange, ...fieldProps }: Readonly<DashboardFormComponentProps>) {
20
+ export function NumberInput({ fieldDef, onChange, ...fieldProps }: Readonly<NumberInputProps>) {
15
21
  const readOnly = fieldProps.disabled || isReadonlyField(fieldDef);
16
22
  const isFloat = fieldDef ? fieldDef.type === 'float' : false;
17
- const min = fieldDef?.ui?.min;
18
- const max = fieldDef?.ui?.max;
19
- const step = fieldDef?.ui?.step || (isFloat ? 0.01 : 1);
23
+ const min = fieldProps.min ?? fieldDef?.ui?.min;
24
+ const max = fieldProps.max ?? fieldDef?.ui?.max;
25
+ const step = fieldProps.step ?? (fieldDef?.ui?.step || (isFloat ? 0.01 : 1));
20
26
  const prefix = fieldDef?.ui?.prefix;
21
27
  const suffix = fieldDef?.ui?.suffix;
22
28
  const shouldUseAffixedInput = prefix || suffix;
@@ -0,0 +1,65 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useForm } from 'react-hook-form';
3
+ import { withDescription } from '../../../.storybook/with-description.js';
4
+ import { PasswordFormInput } from './password-form-input.js';
5
+
6
+ const meta = {
7
+ title: 'Form Inputs/PasswordFormInput',
8
+ component: PasswordFormInput,
9
+ ...withDescription(import.meta.url, './password-form-input.js'),
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ tags: ['autodocs'],
14
+ argTypes: {
15
+ value: {
16
+ control: 'text',
17
+ description: 'The current password value',
18
+ },
19
+ disabled: {
20
+ control: 'boolean',
21
+ description: 'Whether the input is disabled',
22
+ },
23
+ },
24
+ } satisfies Meta<typeof PasswordFormInput>;
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ export const Playground: Story = {
30
+ args: {
31
+ value: 'secret123',
32
+ disabled: false,
33
+ },
34
+ render: args => {
35
+ const { register } = useForm();
36
+ const field = register('password');
37
+ return (
38
+ <div className="w-[300px]">
39
+ <PasswordFormInput {...field} {...args} />
40
+ </div>
41
+ );
42
+ },
43
+ };
44
+
45
+ export const ChangePassword: Story = {
46
+ render: () => {
47
+ const { register } = useForm();
48
+ return (
49
+ <div className="w-[300px] space-y-4">
50
+ <div className="space-y-2">
51
+ <label className="text-sm font-medium">Current Password</label>
52
+ <PasswordFormInput {...register('currentPassword')} />
53
+ </div>
54
+ <div className="space-y-2">
55
+ <label className="text-sm font-medium">New Password</label>
56
+ <PasswordFormInput {...register('newPassword')} />
57
+ </div>
58
+ <div className="space-y-2">
59
+ <label className="text-sm font-medium">Confirm New Password</label>
60
+ <PasswordFormInput {...register('confirmPassword')} />
61
+ </div>
62
+ </div>
63
+ );
64
+ },
65
+ };
@@ -9,7 +9,7 @@ import { Input } from '../ui/input.js';
9
9
  * @docsCategory form-components
10
10
  * @docsPage PasswordInput
11
11
  */
12
- export function PasswordInput(props: Readonly<DashboardFormComponentProps>) {
12
+ export function PasswordFormInput(props: Readonly<DashboardFormComponentProps>) {
13
13
  const readOnly = props.disabled || isReadonlyField(props.fieldDef);
14
14
  return (
15
15
  <Input
@@ -0,0 +1,92 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useForm } from 'react-hook-form';
3
+ import { withDescription } from '../../../.storybook/with-description.js';
4
+ import { RichTextInput } from './rich-text-input.js';
5
+
6
+ const meta = {
7
+ title: 'Form Inputs/RichTextInput',
8
+ component: RichTextInput,
9
+ ...withDescription(import.meta.url, './rich-text-input.js'),
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ tags: ['autodocs'],
14
+ argTypes: {
15
+ value: {
16
+ control: 'text',
17
+ description: 'The rich text HTML content',
18
+ },
19
+ },
20
+ } satisfies Meta<typeof RichTextInput>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Playground: Story = {
26
+ args: {
27
+ value: '<p>Edit this <strong>rich text</strong> content!</p>',
28
+ },
29
+ render: args => {
30
+ const { register } = useForm();
31
+ const field = register('content');
32
+ return (
33
+ <div className="w-[600px]">
34
+ <RichTextInput {...field} {...args} />
35
+ </div>
36
+ );
37
+ },
38
+ };
39
+
40
+ export const EmptyEditor: Story = {
41
+ render: () => {
42
+ const { register } = useForm();
43
+ const field = register('empty');
44
+ return (
45
+ <div className="w-[600px]">
46
+ <RichTextInput {...field} />
47
+ </div>
48
+ );
49
+ },
50
+ };
51
+
52
+ export const WithComplexContent: Story = {
53
+ render: () => {
54
+ const { register } = useForm();
55
+ const field = register('complex');
56
+ return (
57
+ <div className="w-[600px]">
58
+ <RichTextInput
59
+ {...field}
60
+ value={`
61
+ <h2>Product Description</h2>
62
+ <p>This is a <strong>high-quality</strong> product with the following features:</p>
63
+ <ul>
64
+ <li>Feature one with <em>emphasis</em></li>
65
+ <li>Feature two with a <a href="https://example.com">link</a></li>
66
+ <li>Feature three</li>
67
+ </ul>
68
+ <blockquote>
69
+ <p>Customer testimonial goes here</p>
70
+ </blockquote>
71
+ `}
72
+ />
73
+ </div>
74
+ );
75
+ },
76
+ };
77
+
78
+ export const ReadonlyMode: Story = {
79
+ render: () => {
80
+ const { register } = useForm();
81
+ const field = register('readonly');
82
+ return (
83
+ <div className="w-[600px]">
84
+ <RichTextInput
85
+ {...field}
86
+ value="<p>This content is <strong>readonly</strong> and cannot be edited.</p>"
87
+ fieldDef={{ readonly: true }}
88
+ />
89
+ </div>
90
+ );
91
+ },
92
+ };
@@ -0,0 +1,232 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { FormProvider, useForm } from 'react-hook-form';
3
+ import { withDescription } from '../../../.storybook/with-description.js';
4
+ import { SlugInput } from './slug-input.js';
5
+
6
+ const meta = {
7
+ title: 'Form Inputs/SlugInput',
8
+ component: SlugInput,
9
+ ...withDescription(import.meta.url, './slug-input.js'),
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ tags: ['autodocs'],
14
+ decorators: [
15
+ Story => (
16
+ <div className="w-[500px]">
17
+ <Story />
18
+ </div>
19
+ ),
20
+ ],
21
+ argTypes: {
22
+ entityName: {
23
+ control: 'text',
24
+ description: 'The name of the entity (e.g., "Product", "Collection")',
25
+ },
26
+ fieldName: {
27
+ control: 'text',
28
+ description: 'The name of the field to check for uniqueness (e.g., "slug", "code")',
29
+ },
30
+ watchFieldName: {
31
+ control: 'text',
32
+ description: 'The name of the field to watch for changes',
33
+ },
34
+ defaultReadonly: {
35
+ control: 'boolean',
36
+ description: 'Whether the input should start in readonly mode',
37
+ },
38
+ },
39
+ } satisfies Meta<typeof SlugInput>;
40
+
41
+ export default meta;
42
+ type Story = StoryObj<typeof meta>;
43
+
44
+ export const AutoGenerating: Story = {
45
+ render: () => {
46
+ const form = useForm({
47
+ defaultValues: {
48
+ name: 'Product Name',
49
+ slug: '',
50
+ },
51
+ });
52
+
53
+ return (
54
+ <FormProvider {...form}>
55
+ <div className="space-y-4">
56
+ <div>
57
+ <label className="text-sm font-medium mb-2 block">Name</label>
58
+ <input
59
+ {...form.register('name')}
60
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors"
61
+ placeholder="Enter product name"
62
+ />
63
+ </div>
64
+ <div>
65
+ <label className="text-sm font-medium mb-2 block">Slug (auto-generated)</label>
66
+ <SlugInput
67
+ {...form.register('slug')}
68
+ value={form.watch('slug')}
69
+ onChange={value => form.setValue('slug', value)}
70
+ entityName="Product"
71
+ fieldName="slug"
72
+ watchFieldName="name"
73
+ />
74
+ </div>
75
+ <div className="text-sm text-muted-foreground">
76
+ <div>Name value: {form.watch('name')}</div>
77
+ <div>Slug value: {form.watch('slug')}</div>
78
+ </div>
79
+ </div>
80
+ </FormProvider>
81
+ );
82
+ },
83
+ };
84
+
85
+ export const WithExistingValue: Story = {
86
+ render: () => {
87
+ const form = useForm({
88
+ defaultValues: {
89
+ name: 'Existing Product',
90
+ slug: 'existing-product-slug',
91
+ },
92
+ });
93
+
94
+ return (
95
+ <FormProvider {...form}>
96
+ <div className="space-y-4">
97
+ <div>
98
+ <label className="text-sm font-medium mb-2 block">Name</label>
99
+ <input
100
+ {...form.register('name')}
101
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors"
102
+ placeholder="Enter product name"
103
+ />
104
+ </div>
105
+ <div>
106
+ <label className="text-sm font-medium mb-2 block">Slug (with existing value)</label>
107
+ <SlugInput
108
+ {...form.register('slug')}
109
+ value={form.watch('slug')}
110
+ onChange={value => form.setValue('slug', value)}
111
+ entityName="Product"
112
+ fieldName="slug"
113
+ watchFieldName="name"
114
+ entityId="1"
115
+ />
116
+ </div>
117
+ <div className="text-sm text-muted-foreground">
118
+ Click the Edit button to manually edit the slug, or click Regenerate to update from
119
+ the name field.
120
+ </div>
121
+ </div>
122
+ </FormProvider>
123
+ );
124
+ },
125
+ };
126
+
127
+ export const ManualEditing: Story = {
128
+ render: () => {
129
+ const form = useForm({
130
+ defaultValues: {
131
+ name: 'Custom Product',
132
+ slug: '',
133
+ },
134
+ });
135
+
136
+ return (
137
+ <FormProvider {...form}>
138
+ <div className="space-y-4">
139
+ <div>
140
+ <label className="text-sm font-medium mb-2 block">Name</label>
141
+ <input
142
+ {...form.register('name')}
143
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors"
144
+ placeholder="Enter product name"
145
+ />
146
+ </div>
147
+ <div>
148
+ <label className="text-sm font-medium mb-2 block">
149
+ Slug (click Edit to customize)
150
+ </label>
151
+ <SlugInput
152
+ {...form.register('slug')}
153
+ value={form.watch('slug')}
154
+ onChange={value => form.setValue('slug', value)}
155
+ entityName="Product"
156
+ fieldName="slug"
157
+ watchFieldName="name"
158
+ />
159
+ </div>
160
+ <div className="text-sm text-muted-foreground">
161
+ Click the Edit button to switch to manual mode. Click the Lock button to switch back
162
+ to auto-generation.
163
+ </div>
164
+ </div>
165
+ </FormProvider>
166
+ );
167
+ },
168
+ };
169
+
170
+ export const StartInEditableMode: Story = {
171
+ render: () => {
172
+ const form = useForm({
173
+ defaultValues: {
174
+ code: '',
175
+ },
176
+ });
177
+
178
+ return (
179
+ <FormProvider {...form}>
180
+ <div className="space-y-4">
181
+ <div>
182
+ <label className="text-sm font-medium mb-2 block">Channel Code</label>
183
+ <SlugInput
184
+ {...form.register('code')}
185
+ value={form.watch('code')}
186
+ onChange={value => form.setValue('code', value)}
187
+ entityName="Channel"
188
+ fieldName="code"
189
+ watchFieldName="name"
190
+ defaultReadonly={false}
191
+ />
192
+ </div>
193
+ <div className="text-sm text-muted-foreground">
194
+ This slug input starts in editable mode (defaultReadonly=false).
195
+ </div>
196
+ </div>
197
+ </FormProvider>
198
+ );
199
+ },
200
+ };
201
+
202
+ export const Readonly: Story = {
203
+ render: () => {
204
+ const form = useForm({
205
+ defaultValues: {
206
+ slug: 'readonly-slug',
207
+ },
208
+ });
209
+
210
+ return (
211
+ <FormProvider {...form}>
212
+ <div className="space-y-4">
213
+ <div>
214
+ <label className="text-sm font-medium mb-2 block">Readonly Slug</label>
215
+ <SlugInput
216
+ {...form.register('slug')}
217
+ value={form.watch('slug')}
218
+ onChange={value => form.setValue('slug', value)}
219
+ entityName="Product"
220
+ fieldName="slug"
221
+ watchFieldName="name"
222
+ fieldDef={{ readonly: true }}
223
+ />
224
+ </div>
225
+ <div className="text-sm text-muted-foreground">
226
+ This slug input is readonly (fieldDef.readonly=true). No edit buttons are shown.
227
+ </div>
228
+ </div>
229
+ </FormProvider>
230
+ );
231
+ },
232
+ };
@@ -168,20 +168,19 @@ export function SlugInput({
168
168
 
169
169
  const watchFieldState = form.getFieldState(actualWatchFieldName);
170
170
  const debouncedWatchedValue = useDebounce(watchedValue, 500);
171
-
172
- const shouldAutoGenerate = isReadonly && !value && watchFieldState.isDirty;
173
-
171
+ const shouldAutoGenerate = isReadonly && !entityId && watchFieldState.isDirty;
172
+ const queryKey = ['slugForEntity', entityName, fieldName, debouncedWatchedValue, entityId];
173
+ const enabled = !!debouncedWatchedValue && shouldAutoGenerate;
174
174
  const {
175
175
  data: generatedSlug,
176
176
  isLoading,
177
177
  refetch,
178
178
  } = useQuery({
179
- queryKey: ['slugForEntity', entityName, fieldName, debouncedWatchedValue, entityId],
179
+ queryKey,
180
180
  queryFn: async () => {
181
181
  if (!debouncedWatchedValue) {
182
182
  return '';
183
183
  }
184
-
185
184
  const result = await api.query(slugForEntityDocument, {
186
185
  input: {
187
186
  entityName,
@@ -193,14 +192,14 @@ export function SlugInput({
193
192
 
194
193
  return result.slugForEntity;
195
194
  },
196
- enabled: !!debouncedWatchedValue && shouldAutoGenerate,
195
+ enabled,
197
196
  });
198
197
 
199
198
  useEffect(() => {
200
- if (isReadonly && generatedSlug && generatedSlug !== value) {
199
+ if (shouldAutoGenerate && generatedSlug && generatedSlug !== value) {
201
200
  onChange?.(generatedSlug);
202
201
  }
203
- }, [generatedSlug, isReadonly, value, onChange]);
202
+ }, [generatedSlug, shouldAutoGenerate, value, onChange]);
204
203
 
205
204
  const toggleReadonly = () => {
206
205
  if (!isFormReadonly) {
@@ -221,8 +220,8 @@ export function SlugInput({
221
220
  onChange?.(newValue);
222
221
  };
223
222
 
224
- const displayValue = isReadonly && generatedSlug ? generatedSlug : value || '';
225
- const showLoading = isLoading && isReadonly;
223
+ const displayValue = shouldAutoGenerate && generatedSlug ? generatedSlug : value || '';
224
+ const showLoading = isLoading && shouldAutoGenerate;
226
225
 
227
226
  return (
228
227
  <div className="relative flex items-center gap-2">
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useForm } from 'react-hook-form';
3
+ import { withDescription } from '../../../.storybook/with-description.js';
4
+ import { TextInput } from './text-input.js';
5
+
6
+ const meta = {
7
+ title: 'Form Inputs/TextInput',
8
+ component: TextInput,
9
+ ...withDescription(import.meta.url, './text-input.js'),
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ tags: ['autodocs'],
14
+ argTypes: {
15
+ value: {
16
+ control: 'text',
17
+ description: 'The current value',
18
+ },
19
+ placeholder: {
20
+ control: 'text',
21
+ description: 'Placeholder text',
22
+ },
23
+ disabled: {
24
+ control: 'boolean',
25
+ description: 'Whether the input is disabled',
26
+ },
27
+ },
28
+ } satisfies Meta<typeof TextInput>;
29
+
30
+ export default meta;
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ export const Playground: Story = {
34
+ args: {
35
+ value: 'Edit me!',
36
+ placeholder: 'Enter text',
37
+ disabled: false,
38
+ },
39
+ render: args => {
40
+ const { register } = useForm();
41
+ const field = register('text');
42
+ return <TextInput {...field} {...args} />;
43
+ },
44
+ };
45
+
46
+ export const LongText: Story = {
47
+ render: () => {
48
+ const { register } = useForm();
49
+ const field = register('longText');
50
+ return <TextInput {...field} />;
51
+ },
52
+ };
@@ -0,0 +1,55 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useForm } from 'react-hook-form';
3
+ import { withDescription } from '../../../.storybook/with-description.js';
4
+ import { TextareaInput } from './textarea-input.js';
5
+
6
+ const meta = {
7
+ title: 'Form Inputs/TextareaInput',
8
+ component: TextareaInput,
9
+ ...withDescription(import.meta.url, './textarea-input.js'),
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ tags: ['autodocs'],
14
+ argTypes: {
15
+ value: {
16
+ control: 'text',
17
+ description: 'The current value',
18
+ },
19
+ disabled: {
20
+ control: 'boolean',
21
+ description: 'Whether the textarea is disabled',
22
+ },
23
+ },
24
+ } satisfies Meta<typeof TextareaInput>;
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ export const Playground: Story = {
30
+ args: {
31
+ value: 'Edit this text!\nMultiple lines supported.',
32
+ disabled: false,
33
+ },
34
+ render: args => {
35
+ const { register } = useForm();
36
+ const field = register('playground');
37
+ return (
38
+ <div className="w-[500px]">
39
+ <TextareaInput {...field} {...args} />
40
+ </div>
41
+ );
42
+ },
43
+ };
44
+
45
+ export const LongText: Story = {
46
+ render: () => {
47
+ const { register } = useForm();
48
+ const field = register('longText');
49
+ return (
50
+ <div className="w-[500px]">
51
+ <TextareaInput {...field} />
52
+ </div>
53
+ );
54
+ },
55
+ };
@@ -53,7 +53,12 @@ export function AddFilterMenu({ columns }: Readonly<AddFilterMenuProps>) {
53
53
  ))}
54
54
  </DropdownMenuContent>
55
55
  </DropdownMenu>
56
- {selectedColumn && <DataTableFilterDialog column={selectedColumn as any} />}
56
+ {selectedColumn && (
57
+ <DataTableFilterDialog
58
+ column={selectedColumn as any}
59
+ onEnter={() => setIsDialogOpen(false)}
60
+ />
61
+ )}
57
62
  </Dialog>
58
63
  );
59
64
  }