@btst/stack 2.8.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +3 -2
  2. package/dist/components/markdown/index.d.cts +15 -2
  3. package/dist/components/markdown/index.d.mts +15 -2
  4. package/dist/components/markdown/index.d.ts +15 -2
  5. package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.cjs +30 -1
  6. package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.mjs +30 -1
  7. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.cjs +49 -9
  8. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.mjs +50 -10
  9. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.cjs +77 -9
  10. package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.mjs +77 -9
  11. package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -5
  12. package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.mjs +24 -5
  13. package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.cjs +47 -13
  14. package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.mjs +47 -13
  15. package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.cjs +1 -1
  16. package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.mjs +1 -1
  17. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +6 -2
  18. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +6 -2
  19. package/dist/packages/stack/src/plugins/media/api/adapters/local.cjs +55 -0
  20. package/dist/packages/stack/src/plugins/media/api/adapters/local.mjs +37 -0
  21. package/dist/packages/stack/src/plugins/media/api/getters.cjs +83 -0
  22. package/dist/packages/stack/src/plugins/media/api/getters.mjs +78 -0
  23. package/dist/packages/stack/src/plugins/media/api/mutations.cjs +88 -0
  24. package/dist/packages/stack/src/plugins/media/api/mutations.mjs +82 -0
  25. package/dist/packages/stack/src/plugins/media/api/plugin.cjs +525 -0
  26. package/dist/packages/stack/src/plugins/media/api/plugin.mjs +523 -0
  27. package/dist/packages/stack/src/plugins/media/api/query-key-defs.cjs +19 -0
  28. package/dist/packages/stack/src/plugins/media/api/query-key-defs.mjs +16 -0
  29. package/dist/packages/stack/src/plugins/media/api/serializers.cjs +17 -0
  30. package/dist/packages/stack/src/plugins/media/api/serializers.mjs +14 -0
  31. package/dist/packages/stack/src/plugins/media/api/storage-adapter.cjs +15 -0
  32. package/dist/packages/stack/src/plugins/media/api/storage-adapter.mjs +11 -0
  33. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.cjs +129 -0
  34. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.mjs +127 -0
  35. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.cjs +58 -0
  36. package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.mjs +56 -0
  37. package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.cjs +94 -0
  38. package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.mjs +92 -0
  39. package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.cjs +171 -0
  40. package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.mjs +168 -0
  41. package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.cjs +308 -0
  42. package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.mjs +305 -0
  43. package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.cjs +104 -0
  44. package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.mjs +102 -0
  45. package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.cjs +70 -0
  46. package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.mjs +68 -0
  47. package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.cjs +21 -0
  48. package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.mjs +17 -0
  49. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.cjs +35 -0
  50. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.cjs +125 -0
  51. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.mjs +123 -0
  52. package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.mjs +33 -0
  53. package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +222 -0
  54. package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +214 -0
  55. package/dist/packages/stack/src/plugins/media/client/plugin.cjs +94 -0
  56. package/dist/packages/stack/src/plugins/media/client/plugin.mjs +92 -0
  57. package/dist/packages/stack/src/plugins/media/client/upload.cjs +121 -0
  58. package/dist/packages/stack/src/plugins/media/client/upload.mjs +119 -0
  59. package/dist/packages/stack/src/plugins/media/client/utils/image-compression.cjs +67 -0
  60. package/dist/packages/stack/src/plugins/media/client/utils/image-compression.mjs +65 -0
  61. package/dist/packages/stack/src/plugins/media/db.cjs +62 -0
  62. package/dist/packages/stack/src/plugins/media/db.mjs +60 -0
  63. package/dist/packages/stack/src/plugins/media/schemas.cjs +41 -0
  64. package/dist/packages/stack/src/plugins/media/schemas.mjs +35 -0
  65. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.cjs +18 -1
  66. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.mjs +19 -2
  67. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.cjs +2 -2
  68. package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.mjs +2 -2
  69. package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.cjs +3 -2
  70. package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.mjs +3 -2
  71. package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.cjs +12 -5
  72. package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.mjs +12 -5
  73. package/dist/plugins/blog/api/index.d.cts +2 -2
  74. package/dist/plugins/blog/api/index.d.mts +2 -2
  75. package/dist/plugins/blog/api/index.d.ts +2 -2
  76. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  77. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  78. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  79. package/dist/plugins/blog/client/index.d.cts +60 -3
  80. package/dist/plugins/blog/client/index.d.mts +60 -3
  81. package/dist/plugins/blog/client/index.d.ts +60 -3
  82. package/dist/plugins/blog/query-keys.d.cts +2 -2
  83. package/dist/plugins/blog/query-keys.d.mts +2 -2
  84. package/dist/plugins/blog/query-keys.d.ts +2 -2
  85. package/dist/plugins/cms/client/index.d.cts +73 -3
  86. package/dist/plugins/cms/client/index.d.mts +73 -3
  87. package/dist/plugins/cms/client/index.d.ts +73 -3
  88. package/dist/plugins/kanban/api/index.d.cts +1 -1
  89. package/dist/plugins/kanban/api/index.d.mts +1 -1
  90. package/dist/plugins/kanban/api/index.d.ts +1 -1
  91. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  92. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  93. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  94. package/dist/plugins/kanban/client/index.d.cts +1 -1
  95. package/dist/plugins/kanban/client/index.d.mts +1 -1
  96. package/dist/plugins/kanban/client/index.d.ts +1 -1
  97. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  98. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  99. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  100. package/dist/plugins/media/api/adapters/s3.cjs +106 -0
  101. package/dist/plugins/media/api/adapters/s3.d.cts +60 -0
  102. package/dist/plugins/media/api/adapters/s3.d.mts +60 -0
  103. package/dist/plugins/media/api/adapters/s3.d.ts +60 -0
  104. package/dist/plugins/media/api/adapters/s3.mjs +104 -0
  105. package/dist/plugins/media/api/adapters/vercel-blob.cjs +54 -0
  106. package/dist/plugins/media/api/adapters/vercel-blob.d.cts +41 -0
  107. package/dist/plugins/media/api/adapters/vercel-blob.d.mts +41 -0
  108. package/dist/plugins/media/api/adapters/vercel-blob.d.ts +41 -0
  109. package/dist/plugins/media/api/adapters/vercel-blob.mjs +52 -0
  110. package/dist/plugins/media/api/index.cjs +26 -0
  111. package/dist/plugins/media/api/index.d.cts +116 -0
  112. package/dist/plugins/media/api/index.d.mts +116 -0
  113. package/dist/plugins/media/api/index.d.ts +116 -0
  114. package/dist/plugins/media/api/index.mjs +6 -0
  115. package/dist/plugins/media/client/components/index.cjs +10 -0
  116. package/dist/plugins/media/client/components/index.d.cts +55 -0
  117. package/dist/plugins/media/client/components/index.d.mts +55 -0
  118. package/dist/plugins/media/client/components/index.d.ts +55 -0
  119. package/dist/plugins/media/client/components/index.mjs +2 -0
  120. package/dist/plugins/media/client/hooks/index.cjs +13 -0
  121. package/dist/plugins/media/client/hooks/index.d.cts +53 -0
  122. package/dist/plugins/media/client/hooks/index.d.mts +53 -0
  123. package/dist/plugins/media/client/hooks/index.d.ts +53 -0
  124. package/dist/plugins/media/client/hooks/index.mjs +1 -0
  125. package/dist/plugins/media/client/index.cjs +9 -0
  126. package/dist/plugins/media/client/index.d.cts +242 -0
  127. package/dist/plugins/media/client/index.d.mts +242 -0
  128. package/dist/plugins/media/client/index.d.ts +242 -0
  129. package/dist/plugins/media/client/index.mjs +2 -0
  130. package/dist/plugins/media/client.css +1 -0
  131. package/dist/plugins/media/query-keys.cjs +72 -0
  132. package/dist/plugins/media/query-keys.d.cts +49 -0
  133. package/dist/plugins/media/query-keys.d.mts +49 -0
  134. package/dist/plugins/media/query-keys.d.ts +49 -0
  135. package/dist/plugins/media/query-keys.mjs +70 -0
  136. package/dist/plugins/media/style.css +1 -0
  137. package/dist/shared/{stack.DOZ1EXjM.d.mts → stack.6mEHS2WH.d.mts} +3 -3
  138. package/dist/shared/{stack.DX-tQ93o.d.cts → stack.AJTXI7kw.d.cts} +3 -3
  139. package/dist/shared/{stack.DRpeDS6X.d.ts → stack.BMx2QYOK.d.ts} +25 -0
  140. package/dist/shared/stack.BUTXWiG-.d.ts +286 -0
  141. package/dist/shared/stack.C7Y9sBDg.d.mts +286 -0
  142. package/dist/shared/stack.C7vfOBmO.d.mts +63 -0
  143. package/dist/shared/stack.CAni8dnD.d.cts +63 -0
  144. package/dist/shared/stack.CLcnSF_b.d.cts +25 -0
  145. package/dist/shared/stack.CLcnSF_b.d.mts +25 -0
  146. package/dist/shared/stack.CLcnSF_b.d.ts +25 -0
  147. package/dist/shared/stack.CYSwntXC.d.ts +63 -0
  148. package/dist/shared/{stack.Jb0kQDJC.d.mts → stack.Cd6McBu1.d.mts} +25 -0
  149. package/dist/shared/stack.CoBj86jf.d.cts +109 -0
  150. package/dist/shared/stack.CoBj86jf.d.mts +109 -0
  151. package/dist/shared/stack.CoBj86jf.d.ts +109 -0
  152. package/dist/shared/{stack.BXxrFL9R.d.ts → stack.D7HSzZdG.d.ts} +5 -5
  153. package/dist/shared/{stack.DzOhpIYM.d.mts → stack.DjgpFWq3.d.cts} +5 -5
  154. package/dist/shared/{stack.BxFl46lB.d.cts → stack.DxQl8Wa1.d.cts} +25 -0
  155. package/dist/shared/{stack.BSqJrCTM.d.cts → stack.IUeyQKrm.d.mts} +5 -5
  156. package/dist/shared/{stack.VF6FhyZw.d.ts → stack.QYn-Px94.d.ts} +3 -3
  157. package/dist/shared/stack.vxskCkim.d.cts +286 -0
  158. package/package.json +113 -4
  159. package/src/plugins/blog/client/components/forms/image-field.tsx +35 -4
  160. package/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +67 -12
  161. package/src/plugins/blog/client/components/forms/markdown-editor.tsx +106 -10
  162. package/src/plugins/blog/client/overrides.ts +58 -1
  163. package/src/plugins/cms/client/components/forms/content-form.tsx +26 -7
  164. package/src/plugins/cms/client/components/forms/file-upload.tsx +73 -15
  165. package/src/plugins/cms/client/overrides.ts +57 -2
  166. package/src/plugins/kanban/client/components/forms/board-form.tsx +1 -1
  167. package/src/plugins/kanban/client/components/forms/task-form.tsx +7 -1
  168. package/src/plugins/kanban/client/overrides.ts +25 -0
  169. package/src/plugins/media/__tests__/__stubs__/vercel-blob-server.ts +9 -0
  170. package/src/plugins/media/__tests__/getters.test.ts +274 -0
  171. package/src/plugins/media/__tests__/mutations.test.ts +299 -0
  172. package/src/plugins/media/__tests__/plugin.test.ts +752 -0
  173. package/src/plugins/media/__tests__/query-key-defs.test.ts +54 -0
  174. package/src/plugins/media/__tests__/storage-adapters.test.ts +351 -0
  175. package/src/plugins/media/api/adapters/local.ts +79 -0
  176. package/src/plugins/media/api/adapters/s3.ts +198 -0
  177. package/src/plugins/media/api/adapters/vercel-blob.ts +132 -0
  178. package/src/plugins/media/api/getters.ts +174 -0
  179. package/src/plugins/media/api/index.ts +41 -0
  180. package/src/plugins/media/api/mutations.ts +179 -0
  181. package/src/plugins/media/api/plugin.ts +855 -0
  182. package/src/plugins/media/api/query-key-defs.ts +41 -0
  183. package/src/plugins/media/api/serializers.ts +28 -0
  184. package/src/plugins/media/api/storage-adapter.ts +139 -0
  185. package/src/plugins/media/client/components/index.tsx +6 -0
  186. package/src/plugins/media/client/components/media-picker/asset-card.tsx +150 -0
  187. package/src/plugins/media/client/components/media-picker/asset-preview-button.tsx +67 -0
  188. package/src/plugins/media/client/components/media-picker/browse-tab.tsx +116 -0
  189. package/src/plugins/media/client/components/media-picker/folder-tree.tsx +188 -0
  190. package/src/plugins/media/client/components/media-picker/index.tsx +347 -0
  191. package/src/plugins/media/client/components/media-picker/upload-tab.tsx +108 -0
  192. package/src/plugins/media/client/components/media-picker/url-tab.tsx +72 -0
  193. package/src/plugins/media/client/components/media-picker/utils.ts +17 -0
  194. package/src/plugins/media/client/components/pages/library-page.internal.tsx +134 -0
  195. package/src/plugins/media/client/components/pages/library-page.tsx +42 -0
  196. package/src/plugins/media/client/hooks/index.tsx +9 -0
  197. package/src/plugins/media/client/hooks/use-media.tsx +289 -0
  198. package/src/plugins/media/client/index.ts +4 -0
  199. package/src/plugins/media/client/overrides.ts +127 -0
  200. package/src/plugins/media/client/plugin.tsx +184 -0
  201. package/src/plugins/media/client/upload.ts +171 -0
  202. package/src/plugins/media/client/utils/image-compression.ts +131 -0
  203. package/src/plugins/media/client.css +1 -0
  204. package/src/plugins/media/db.ts +62 -0
  205. package/src/plugins/media/query-keys.ts +96 -0
  206. package/src/plugins/media/schemas.ts +37 -0
  207. package/src/plugins/media/style.css +1 -0
  208. package/src/plugins/media/types.ts +26 -0
  209. package/dist/shared/{stack.BWp0hcm9.d.ts → stack.BQmuNl5p.d.cts} +3 -3
  210. package/dist/shared/{stack.BWp0hcm9.d.cts → stack.BQmuNl5p.d.mts} +3 -3
  211. package/dist/shared/{stack.BWp0hcm9.d.mts → stack.BQmuNl5p.d.ts} +3 -3
  212. package/dist/shared/{stack.BvCR4-9H.d.ts → stack.D4Cea8II.d.ts} +3 -3
  213. package/dist/shared/{stack.CWxAl9K3.d.mts → stack.HE_IvqV5.d.mts} +3 -3
  214. package/dist/shared/{stack.BOokfhZD.d.cts → stack.Rtcvl8sS.d.cts} +3 -3
package/README.md CHANGED
@@ -37,6 +37,7 @@ Enable the features you need and keep building your product.
37
37
  | **Form Builder** | Dynamic form builder with drag-and-drop editor, submissions, and validation |
38
38
  | **UI Builder** | Visual drag-and-drop page builder with component registry and public rendering |
39
39
  | **Kanban** | Project management with boards, columns, tasks, drag-and-drop, and priority levels |
40
+ | **Media** | Media library with uploads, folders, picker UI, URL registration, and reusable image inputs |
40
41
  | **OpenAPI** | Auto-generated API documentation with interactive Scalar UI |
41
42
  | **Route Docs** | Auto-generated client route documentation with interactive navigation |
42
43
  | **Better Auth UI** | Beautiful shadcn/ui authentication components for better-auth |
@@ -121,8 +122,8 @@ Supports Prisma, Drizzle, MongoDB and Kysely SQL dialects.
121
122
  Each plugin's UI layer is available as a [shadcn registry](https://ui.shadcn.com/docs/registry) block. Use it to **eject and fully customize** the page components while keeping all data-fetching and API logic from `@btst/stack`:
122
123
 
123
124
  ```bash
124
- # Install a single plugin's UI
125
- npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-blog.json
125
+ # Install a single plugin's UI (for example, Media)
126
+ npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/btst-media.json
126
127
 
127
128
  # Or install the full collection
128
129
  npx shadcn@latest add https://github.com/better-stack-ai/better-stack/blob/main/packages/stack/registry/registry.json
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentType } from 'react';
2
+ import { ComponentType, MutableRefObject } from 'react';
3
3
 
4
4
  type MarkdownContentProps = {
5
5
  /** The markdown string to render */
@@ -23,8 +23,21 @@ interface MarkdownEditorProps {
23
23
  uploadImage?: (file: File) => Promise<string>;
24
24
  /** Placeholder text shown when the editor is empty. */
25
25
  placeholder?: string;
26
+ /**
27
+ * Optional ref that will be populated with an `insertImage(url)` function.
28
+ * Call `insertImageRef.current?.(url)` to programmatically insert an image.
29
+ * The URL must be a valid, percent-encoded URL (storage adapters guarantee this).
30
+ */
31
+ insertImageRef?: MutableRefObject<((url: string) => void) | null>;
32
+ /**
33
+ * When provided, clicking the Crepe image block's upload area opens a media
34
+ * picker instead of the native file dialog. The callback receives a `setUrl`
35
+ * function — call it with the chosen URL to set it into the image block.
36
+ * The URL must be a valid, percent-encoded URL (storage adapters guarantee this).
37
+ */
38
+ openMediaPickerForImageBlock?: (setUrl: (url: string) => void) => void;
26
39
  }
27
- declare function MarkdownEditor({ value, onChange, className, uploadImage, placeholder, }: MarkdownEditorProps): react_jsx_runtime.JSX.Element;
40
+ declare function MarkdownEditor({ value, onChange, className, uploadImage, placeholder, insertImageRef, openMediaPickerForImageBlock, }: MarkdownEditorProps): react_jsx_runtime.JSX.Element;
28
41
 
29
42
  export { MarkdownContent, MarkdownEditor };
30
43
  export type { MarkdownContentProps, MarkdownEditorProps };
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentType } from 'react';
2
+ import { ComponentType, MutableRefObject } from 'react';
3
3
 
4
4
  type MarkdownContentProps = {
5
5
  /** The markdown string to render */
@@ -23,8 +23,21 @@ interface MarkdownEditorProps {
23
23
  uploadImage?: (file: File) => Promise<string>;
24
24
  /** Placeholder text shown when the editor is empty. */
25
25
  placeholder?: string;
26
+ /**
27
+ * Optional ref that will be populated with an `insertImage(url)` function.
28
+ * Call `insertImageRef.current?.(url)` to programmatically insert an image.
29
+ * The URL must be a valid, percent-encoded URL (storage adapters guarantee this).
30
+ */
31
+ insertImageRef?: MutableRefObject<((url: string) => void) | null>;
32
+ /**
33
+ * When provided, clicking the Crepe image block's upload area opens a media
34
+ * picker instead of the native file dialog. The callback receives a `setUrl`
35
+ * function — call it with the chosen URL to set it into the image block.
36
+ * The URL must be a valid, percent-encoded URL (storage adapters guarantee this).
37
+ */
38
+ openMediaPickerForImageBlock?: (setUrl: (url: string) => void) => void;
26
39
  }
27
- declare function MarkdownEditor({ value, onChange, className, uploadImage, placeholder, }: MarkdownEditorProps): react_jsx_runtime.JSX.Element;
40
+ declare function MarkdownEditor({ value, onChange, className, uploadImage, placeholder, insertImageRef, openMediaPickerForImageBlock, }: MarkdownEditorProps): react_jsx_runtime.JSX.Element;
28
41
 
29
42
  export { MarkdownContent, MarkdownEditor };
30
43
  export type { MarkdownContentProps, MarkdownEditorProps };
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentType } from 'react';
2
+ import { ComponentType, MutableRefObject } from 'react';
3
3
 
4
4
  type MarkdownContentProps = {
5
5
  /** The markdown string to render */
@@ -23,8 +23,21 @@ interface MarkdownEditorProps {
23
23
  uploadImage?: (file: File) => Promise<string>;
24
24
  /** Placeholder text shown when the editor is empty. */
25
25
  placeholder?: string;
26
+ /**
27
+ * Optional ref that will be populated with an `insertImage(url)` function.
28
+ * Call `insertImageRef.current?.(url)` to programmatically insert an image.
29
+ * The URL must be a valid, percent-encoded URL (storage adapters guarantee this).
30
+ */
31
+ insertImageRef?: MutableRefObject<((url: string) => void) | null>;
32
+ /**
33
+ * When provided, clicking the Crepe image block's upload area opens a media
34
+ * picker instead of the native file dialog. The callback receives a `setUrl`
35
+ * function — call it with the chosen URL to set it into the image block.
36
+ * The URL must be a valid, percent-encoded URL (storage adapters guarantee this).
37
+ */
38
+ openMediaPickerForImageBlock?: (setUrl: (url: string) => void) => void;
26
39
  }
27
- declare function MarkdownEditor({ value, onChange, className, uploadImage, placeholder, }: MarkdownEditorProps): react_jsx_runtime.JSX.Element;
40
+ declare function MarkdownEditor({ value, onChange, className, uploadImage, placeholder, insertImageRef, openMediaPickerForImageBlock, }: MarkdownEditorProps): react_jsx_runtime.JSX.Element;
28
41
 
29
42
  export { MarkdownContent, MarkdownEditor };
30
43
  export type { MarkdownContentProps, MarkdownEditorProps };
@@ -18,8 +18,37 @@ function FeaturedImageField({
18
18
  }) {
19
19
  const fileInputRef = React.useRef(null);
20
20
  const [isUploading, setIsUploading] = React.useState(false);
21
- const { uploadImage, Image, localization } = context.usePluginOverrides("blog", { localization: index.BLOG_LOCALIZATION });
21
+ const {
22
+ uploadImage,
23
+ Image,
24
+ localization,
25
+ imageInputField: ImageInput
26
+ } = context.usePluginOverrides(
27
+ "blog",
28
+ { localization: index.BLOG_LOCALIZATION }
29
+ );
22
30
  const ImageComponent = Image ? Image : DefaultImage;
31
+ if (ImageInput) {
32
+ return /* @__PURE__ */ jsxRuntime.jsxs(form.FormItem, { className: "flex flex-col", children: [
33
+ /* @__PURE__ */ jsxRuntime.jsxs(form.FormLabel, { children: [
34
+ localization.BLOG_FORMS_FEATURED_IMAGE_LABEL,
35
+ isRequired && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-destructive", children: [
36
+ " ",
37
+ localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK
38
+ ] })
39
+ ] }),
40
+ /* @__PURE__ */ jsxRuntime.jsx(form.FormControl, { children: /* @__PURE__ */ jsxRuntime.jsx(
41
+ ImageInput,
42
+ {
43
+ value: value || "",
44
+ onChange,
45
+ isRequired
46
+ }
47
+ ) }),
48
+ /* @__PURE__ */ jsxRuntime.jsx(form.FormDescription, {}),
49
+ /* @__PURE__ */ jsxRuntime.jsx(form.FormMessage, {})
50
+ ] });
51
+ }
23
52
  const handleImageUpload = async (event) => {
24
53
  const file = event.target.files?.[0];
25
54
  if (!file) return;
@@ -16,8 +16,37 @@ function FeaturedImageField({
16
16
  }) {
17
17
  const fileInputRef = useRef(null);
18
18
  const [isUploading, setIsUploading] = useState(false);
19
- const { uploadImage, Image, localization } = usePluginOverrides("blog", { localization: BLOG_LOCALIZATION });
19
+ const {
20
+ uploadImage,
21
+ Image,
22
+ localization,
23
+ imageInputField: ImageInput
24
+ } = usePluginOverrides(
25
+ "blog",
26
+ { localization: BLOG_LOCALIZATION }
27
+ );
20
28
  const ImageComponent = Image ? Image : DefaultImage;
29
+ if (ImageInput) {
30
+ return /* @__PURE__ */ jsxs(FormItem, { className: "flex flex-col", children: [
31
+ /* @__PURE__ */ jsxs(FormLabel, { children: [
32
+ localization.BLOG_FORMS_FEATURED_IMAGE_LABEL,
33
+ isRequired && /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
34
+ " ",
35
+ localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK
36
+ ] })
37
+ ] }),
38
+ /* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(
39
+ ImageInput,
40
+ {
41
+ value: value || "",
42
+ onChange,
43
+ isRequired
44
+ }
45
+ ) }),
46
+ /* @__PURE__ */ jsx(FormDescription, {}),
47
+ /* @__PURE__ */ jsx(FormMessage, {})
48
+ ] });
49
+ }
21
50
  const handleImageUpload = async (event) => {
22
51
  const file = event.target.files?.[0];
23
52
  if (!file) return;
@@ -2,22 +2,62 @@
2
2
  'use strict';
3
3
 
4
4
  const jsxRuntime = require('react/jsx-runtime');
5
+ const React = require('react');
5
6
  const context = require('@btst/stack/context');
6
7
  const index = require('../../localization/index.cjs');
7
8
  const markdownEditor = require('./markdown-editor.cjs');
8
9
 
9
10
  function MarkdownEditorWithOverrides(props) {
10
- const { uploadImage, localization } = context.usePluginOverrides("blog", {
11
- localization: index.BLOG_LOCALIZATION
12
- });
13
- return /* @__PURE__ */ jsxRuntime.jsx(
14
- markdownEditor.MarkdownEditor,
15
- {
16
- ...props,
17
- uploadImage,
18
- placeholder: localization?.BLOG_FORMS_EDITOR_PLACEHOLDER
11
+ const {
12
+ uploadImage,
13
+ imagePicker: ImagePickerTrigger,
14
+ localization
15
+ } = context.usePluginOverrides(
16
+ "blog",
17
+ { localization: index.BLOG_LOCALIZATION }
18
+ );
19
+ const insertImageRef = React.useRef(null);
20
+ const pendingInsertUrlRef = React.useRef(null);
21
+ const triggerContainerRef = React.useRef(null);
22
+ const handleSelect = React.useCallback((url) => {
23
+ if (pendingInsertUrlRef.current) {
24
+ pendingInsertUrlRef.current(url);
25
+ pendingInsertUrlRef.current = null;
26
+ } else {
27
+ insertImageRef.current?.(url);
19
28
  }
29
+ }, []);
30
+ const openMediaPickerForImageBlock = React.useCallback(
31
+ (setUrl) => {
32
+ pendingInsertUrlRef.current = setUrl;
33
+ const btn = triggerContainerRef.current?.querySelector(
34
+ '[data-testid="open-media-picker"]'
35
+ );
36
+ btn?.click();
37
+ },
38
+ []
20
39
  );
40
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
41
+ /* @__PURE__ */ jsxRuntime.jsx(
42
+ markdownEditor.MarkdownEditor,
43
+ {
44
+ ...props,
45
+ uploadImage,
46
+ placeholder: localization?.BLOG_FORMS_EDITOR_PLACEHOLDER,
47
+ insertImageRef,
48
+ openMediaPickerForImageBlock: ImagePickerTrigger ? openMediaPickerForImageBlock : void 0
49
+ }
50
+ ),
51
+ ImagePickerTrigger && /* @__PURE__ */ jsxRuntime.jsx(
52
+ "div",
53
+ {
54
+ ref: triggerContainerRef,
55
+ className: "flex justify-end mt-1",
56
+ "data-testid": "image-picker-trigger",
57
+ children: /* @__PURE__ */ jsxRuntime.jsx(ImagePickerTrigger, { onSelect: handleSelect })
58
+ }
59
+ )
60
+ ] });
21
61
  }
22
62
 
23
63
  exports.MarkdownEditorWithOverrides = MarkdownEditorWithOverrides;
@@ -1,21 +1,61 @@
1
1
  "use client";
2
- import { jsx } from 'react/jsx-runtime';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { useRef, useCallback } from 'react';
3
4
  import { usePluginOverrides } from '@btst/stack/context';
4
5
  import { BLOG_LOCALIZATION } from '../../localization/index.mjs';
5
6
  import { MarkdownEditor } from './markdown-editor.mjs';
6
7
 
7
8
  function MarkdownEditorWithOverrides(props) {
8
- const { uploadImage, localization } = usePluginOverrides("blog", {
9
- localization: BLOG_LOCALIZATION
10
- });
11
- return /* @__PURE__ */ jsx(
12
- MarkdownEditor,
13
- {
14
- ...props,
15
- uploadImage,
16
- placeholder: localization?.BLOG_FORMS_EDITOR_PLACEHOLDER
9
+ const {
10
+ uploadImage,
11
+ imagePicker: ImagePickerTrigger,
12
+ localization
13
+ } = usePluginOverrides(
14
+ "blog",
15
+ { localization: BLOG_LOCALIZATION }
16
+ );
17
+ const insertImageRef = useRef(null);
18
+ const pendingInsertUrlRef = useRef(null);
19
+ const triggerContainerRef = useRef(null);
20
+ const handleSelect = useCallback((url) => {
21
+ if (pendingInsertUrlRef.current) {
22
+ pendingInsertUrlRef.current(url);
23
+ pendingInsertUrlRef.current = null;
24
+ } else {
25
+ insertImageRef.current?.(url);
17
26
  }
27
+ }, []);
28
+ const openMediaPickerForImageBlock = useCallback(
29
+ (setUrl) => {
30
+ pendingInsertUrlRef.current = setUrl;
31
+ const btn = triggerContainerRef.current?.querySelector(
32
+ '[data-testid="open-media-picker"]'
33
+ );
34
+ btn?.click();
35
+ },
36
+ []
18
37
  );
38
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
39
+ /* @__PURE__ */ jsx(
40
+ MarkdownEditor,
41
+ {
42
+ ...props,
43
+ uploadImage,
44
+ placeholder: localization?.BLOG_FORMS_EDITOR_PLACEHOLDER,
45
+ insertImageRef,
46
+ openMediaPickerForImageBlock: ImagePickerTrigger ? openMediaPickerForImageBlock : void 0
47
+ }
48
+ ),
49
+ ImagePickerTrigger && /* @__PURE__ */ jsx(
50
+ "div",
51
+ {
52
+ ref: triggerContainerRef,
53
+ className: "flex justify-end mt-1",
54
+ "data-testid": "image-picker-trigger",
55
+ children: /* @__PURE__ */ jsx(ImagePickerTrigger, { onSelect: handleSelect })
56
+ }
57
+ )
58
+ ] });
19
59
  }
20
60
 
21
61
  export { MarkdownEditorWithOverrides };
@@ -16,7 +16,9 @@ function MarkdownEditor({
16
16
  onChange,
17
17
  className,
18
18
  uploadImage,
19
- placeholder = "Write something..."
19
+ placeholder = "Write something...",
20
+ insertImageRef,
21
+ openMediaPickerForImageBlock
20
22
  }) {
21
23
  const containerRef = React.useRef(null);
22
24
  const crepeRef = React.useRef(null);
@@ -24,12 +26,25 @@ function MarkdownEditor({
24
26
  const [isReady, setIsReady] = React.useState(false);
25
27
  const onChangeRef = React.useRef(onChange);
26
28
  const initialValueRef = React.useRef(value ?? "");
29
+ const openMediaPickerRef = React.useRef(
30
+ openMediaPickerForImageBlock
31
+ );
27
32
  const throttledOnChangeRef = React.useRef(null);
28
33
  onChangeRef.current = onChange;
34
+ openMediaPickerRef.current = openMediaPickerForImageBlock;
29
35
  React.useLayoutEffect(() => {
30
36
  if (crepeRef.current) return;
31
37
  const container = containerRef.current;
32
38
  if (!container) return;
39
+ const hasMediaPicker = !!openMediaPickerRef.current;
40
+ const imageBlockConfig = {};
41
+ if (uploadImage) {
42
+ imageBlockConfig.onUpload = async (file) => uploadImage(file);
43
+ }
44
+ if (hasMediaPicker) {
45
+ imageBlockConfig.blockUploadPlaceholderText = "Media Picker";
46
+ imageBlockConfig.inlineUploadPlaceholderText = "Media Picker";
47
+ }
33
48
  const crepe$1 = new crepe.Crepe({
34
49
  root: container,
35
50
  defaultValue: initialValueRef.current,
@@ -37,16 +52,35 @@ function MarkdownEditor({
37
52
  [crepe.CrepeFeature.Placeholder]: {
38
53
  text: placeholder
39
54
  },
40
- ...uploadImage ? {
41
- [crepe.CrepeFeature.ImageBlock]: {
42
- onUpload: async (file) => {
43
- const url = await uploadImage(file);
44
- return url;
45
- }
46
- }
47
- } : {}
55
+ ...Object.keys(imageBlockConfig).length > 0 ? { [crepe.CrepeFeature.ImageBlock]: imageBlockConfig } : {}
48
56
  }
49
57
  });
58
+ const interceptHandler = (e) => {
59
+ if (!openMediaPickerRef.current) return;
60
+ const target = e.target;
61
+ const inPlaceholder = target.closest(".image-edit .placeholder");
62
+ if (!inPlaceholder) return;
63
+ if (target.matches("input")) return;
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ const imageEdit = inPlaceholder.closest(".image-edit");
67
+ const linkInput = imageEdit?.querySelector(
68
+ ".link-input-area"
69
+ );
70
+ openMediaPickerRef.current((url) => {
71
+ if (!linkInput) return;
72
+ const nativeSetter = Object.getOwnPropertyDescriptor(
73
+ HTMLInputElement.prototype,
74
+ "value"
75
+ )?.set;
76
+ nativeSetter?.call(linkInput, url);
77
+ linkInput.dispatchEvent(new Event("input", { bubbles: true }));
78
+ linkInput.dispatchEvent(
79
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true })
80
+ );
81
+ });
82
+ };
83
+ container.addEventListener("click", interceptHandler, true);
50
84
  throttledOnChangeRef.current = utils.throttle((markdown) => {
51
85
  if (onChangeRef.current) onChangeRef.current(markdown);
52
86
  }, 200);
@@ -61,6 +95,7 @@ function MarkdownEditor({
61
95
  });
62
96
  crepeRef.current = crepe$1;
63
97
  return () => {
98
+ container.removeEventListener("click", interceptHandler, true);
64
99
  try {
65
100
  isReadyRef.current = false;
66
101
  throttledOnChangeRef.current?.cancel?.();
@@ -99,6 +134,39 @@ function MarkdownEditor({
99
134
  view.dispatch(tr);
100
135
  });
101
136
  }, [value, isReady]);
137
+ React.useLayoutEffect(() => {
138
+ if (!insertImageRef) return;
139
+ insertImageRef.current = (url) => {
140
+ if (!crepeRef.current || !isReadyRef.current) return;
141
+ try {
142
+ const currentMarkdown = crepeRef.current.getMarkdown?.() ?? "";
143
+ const imageMarkdown = `
144
+
145
+ ![](${url})
146
+
147
+ `;
148
+ const newMarkdown = currentMarkdown.trimEnd() + imageMarkdown;
149
+ crepeRef.current.editor.action((ctx) => {
150
+ const view = ctx.get(core.editorViewCtx);
151
+ const parser = ctx.get(core.parserCtx);
152
+ const doc = parser(newMarkdown);
153
+ if (!doc) return;
154
+ const state = view.state;
155
+ const tr = state.tr.replace(
156
+ 0,
157
+ state.doc.content.size,
158
+ new model.Slice(doc.content, 0, 0)
159
+ );
160
+ view.dispatch(tr);
161
+ });
162
+ if (onChangeRef.current) onChangeRef.current(newMarkdown);
163
+ } catch {
164
+ }
165
+ };
166
+ return () => {
167
+ if (insertImageRef) insertImageRef.current = null;
168
+ };
169
+ }, [insertImageRef]);
102
170
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: utils.cn("milkdown-custom", className) });
103
171
  }
104
172
 
@@ -14,7 +14,9 @@ function MarkdownEditor({
14
14
  onChange,
15
15
  className,
16
16
  uploadImage,
17
- placeholder = "Write something..."
17
+ placeholder = "Write something...",
18
+ insertImageRef,
19
+ openMediaPickerForImageBlock
18
20
  }) {
19
21
  const containerRef = useRef(null);
20
22
  const crepeRef = useRef(null);
@@ -22,12 +24,25 @@ function MarkdownEditor({
22
24
  const [isReady, setIsReady] = useState(false);
23
25
  const onChangeRef = useRef(onChange);
24
26
  const initialValueRef = useRef(value ?? "");
27
+ const openMediaPickerRef = useRef(
28
+ openMediaPickerForImageBlock
29
+ );
25
30
  const throttledOnChangeRef = useRef(null);
26
31
  onChangeRef.current = onChange;
32
+ openMediaPickerRef.current = openMediaPickerForImageBlock;
27
33
  useLayoutEffect(() => {
28
34
  if (crepeRef.current) return;
29
35
  const container = containerRef.current;
30
36
  if (!container) return;
37
+ const hasMediaPicker = !!openMediaPickerRef.current;
38
+ const imageBlockConfig = {};
39
+ if (uploadImage) {
40
+ imageBlockConfig.onUpload = async (file) => uploadImage(file);
41
+ }
42
+ if (hasMediaPicker) {
43
+ imageBlockConfig.blockUploadPlaceholderText = "Media Picker";
44
+ imageBlockConfig.inlineUploadPlaceholderText = "Media Picker";
45
+ }
31
46
  const crepe = new Crepe({
32
47
  root: container,
33
48
  defaultValue: initialValueRef.current,
@@ -35,16 +50,35 @@ function MarkdownEditor({
35
50
  [CrepeFeature.Placeholder]: {
36
51
  text: placeholder
37
52
  },
38
- ...uploadImage ? {
39
- [CrepeFeature.ImageBlock]: {
40
- onUpload: async (file) => {
41
- const url = await uploadImage(file);
42
- return url;
43
- }
44
- }
45
- } : {}
53
+ ...Object.keys(imageBlockConfig).length > 0 ? { [CrepeFeature.ImageBlock]: imageBlockConfig } : {}
46
54
  }
47
55
  });
56
+ const interceptHandler = (e) => {
57
+ if (!openMediaPickerRef.current) return;
58
+ const target = e.target;
59
+ const inPlaceholder = target.closest(".image-edit .placeholder");
60
+ if (!inPlaceholder) return;
61
+ if (target.matches("input")) return;
62
+ e.preventDefault();
63
+ e.stopPropagation();
64
+ const imageEdit = inPlaceholder.closest(".image-edit");
65
+ const linkInput = imageEdit?.querySelector(
66
+ ".link-input-area"
67
+ );
68
+ openMediaPickerRef.current((url) => {
69
+ if (!linkInput) return;
70
+ const nativeSetter = Object.getOwnPropertyDescriptor(
71
+ HTMLInputElement.prototype,
72
+ "value"
73
+ )?.set;
74
+ nativeSetter?.call(linkInput, url);
75
+ linkInput.dispatchEvent(new Event("input", { bubbles: true }));
76
+ linkInput.dispatchEvent(
77
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true })
78
+ );
79
+ });
80
+ };
81
+ container.addEventListener("click", interceptHandler, true);
48
82
  throttledOnChangeRef.current = throttle((markdown) => {
49
83
  if (onChangeRef.current) onChangeRef.current(markdown);
50
84
  }, 200);
@@ -59,6 +93,7 @@ function MarkdownEditor({
59
93
  });
60
94
  crepeRef.current = crepe;
61
95
  return () => {
96
+ container.removeEventListener("click", interceptHandler, true);
62
97
  try {
63
98
  isReadyRef.current = false;
64
99
  throttledOnChangeRef.current?.cancel?.();
@@ -97,6 +132,39 @@ function MarkdownEditor({
97
132
  view.dispatch(tr);
98
133
  });
99
134
  }, [value, isReady]);
135
+ useLayoutEffect(() => {
136
+ if (!insertImageRef) return;
137
+ insertImageRef.current = (url) => {
138
+ if (!crepeRef.current || !isReadyRef.current) return;
139
+ try {
140
+ const currentMarkdown = crepeRef.current.getMarkdown?.() ?? "";
141
+ const imageMarkdown = `
142
+
143
+ ![](${url})
144
+
145
+ `;
146
+ const newMarkdown = currentMarkdown.trimEnd() + imageMarkdown;
147
+ crepeRef.current.editor.action((ctx) => {
148
+ const view = ctx.get(editorViewCtx);
149
+ const parser = ctx.get(parserCtx);
150
+ const doc = parser(newMarkdown);
151
+ if (!doc) return;
152
+ const state = view.state;
153
+ const tr = state.tr.replace(
154
+ 0,
155
+ state.doc.content.size,
156
+ new Slice(doc.content, 0, 0)
157
+ );
158
+ view.dispatch(tr);
159
+ });
160
+ if (onChangeRef.current) onChangeRef.current(newMarkdown);
161
+ } catch {
162
+ }
163
+ };
164
+ return () => {
165
+ if (insertImageRef) insertImageRef.current = null;
166
+ };
167
+ }, [insertImageRef]);
100
168
  return /* @__PURE__ */ jsx("div", { ref: containerRef, className: cn("milkdown-custom", className) });
101
169
  }
102
170
 
@@ -16,25 +16,36 @@ const index = require('../../localization/index.cjs');
16
16
  const fileUpload = require('./file-upload.cjs');
17
17
  const relationField = require('./relation-field.cjs');
18
18
 
19
- function buildFieldConfigFromJsonSchema(jsonSchema, uploadImage, fieldComponents) {
19
+ function buildFieldConfigFromJsonSchema(jsonSchema, uploadImage, fieldComponents, imagePicker, imageInputField) {
20
20
  const baseConfig = helpers.buildFieldConfigFromJsonSchema(jsonSchema, fieldComponents);
21
21
  const properties = jsonSchema.properties;
22
22
  if (!properties) return baseConfig;
23
23
  for (const [key, prop] of Object.entries(properties)) {
24
24
  if (prop.fieldType === "file" && !fieldComponents?.["file"]) {
25
- if (!uploadImage) {
25
+ if (!uploadImage && !imageInputField) {
26
26
  baseConfig[key] = {
27
27
  ...baseConfig[key],
28
28
  fieldType: () => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-md border border-destructive bg-destructive/10 p-3 text-sm text-destructive", children: [
29
29
  "File upload requires an ",
30
30
  /* @__PURE__ */ jsxRuntime.jsx("code", { children: "uploadImage" }),
31
+ " or",
32
+ " ",
33
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: "imageInputField" }),
31
34
  " function in CMS overrides."
32
35
  ] })
33
36
  };
34
37
  } else {
35
38
  baseConfig[key] = {
36
39
  ...baseConfig[key],
37
- fieldType: (props) => /* @__PURE__ */ jsxRuntime.jsx(fileUpload.CMSFileUpload, { ...props, uploadImage })
40
+ fieldType: (props) => /* @__PURE__ */ jsxRuntime.jsx(
41
+ fileUpload.CMSFileUpload,
42
+ {
43
+ ...props,
44
+ uploadImage: uploadImage ?? (() => Promise.resolve("")),
45
+ imageInputField,
46
+ imagePicker
47
+ }
48
+ )
38
49
  };
39
50
  }
40
51
  }
@@ -75,6 +86,8 @@ function ContentForm({
75
86
  const {
76
87
  localization: customLocalization,
77
88
  uploadImage,
89
+ imagePicker,
90
+ imageInputField,
78
91
  fieldComponents
79
92
  } = context.usePluginOverrides("cms");
80
93
  const localization = { ...index.CMS_LOCALIZATION, ...customLocalization };
@@ -115,8 +128,14 @@ function ContentForm({
115
128
  }
116
129
  }, [jsonSchema]);
117
130
  const fieldConfig = React.useMemo(
118
- () => buildFieldConfigFromJsonSchema(jsonSchema, uploadImage, fieldComponents),
119
- [jsonSchema, uploadImage, fieldComponents]
131
+ () => buildFieldConfigFromJsonSchema(
132
+ jsonSchema,
133
+ uploadImage,
134
+ fieldComponents,
135
+ imagePicker,
136
+ imageInputField
137
+ ),
138
+ [jsonSchema, uploadImage, fieldComponents, imagePicker, imageInputField]
120
139
  );
121
140
  const slugSourceField = React.useMemo(
122
141
  () => findSlugSourceField(jsonSchema),