@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.
- package/README.md +3 -2
- package/dist/components/markdown/index.d.cts +15 -2
- package/dist/components/markdown/index.d.mts +15 -2
- package/dist/components/markdown/index.d.ts +15 -2
- package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.cjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.mjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.cjs +49 -9
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.mjs +50 -10
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.cjs +77 -9
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.mjs +77 -9
- package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -5
- package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.mjs +24 -5
- package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.cjs +47 -13
- package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.mjs +47 -13
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.cjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.mjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +6 -2
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +6 -2
- package/dist/packages/stack/src/plugins/media/api/adapters/local.cjs +55 -0
- package/dist/packages/stack/src/plugins/media/api/adapters/local.mjs +37 -0
- package/dist/packages/stack/src/plugins/media/api/getters.cjs +83 -0
- package/dist/packages/stack/src/plugins/media/api/getters.mjs +78 -0
- package/dist/packages/stack/src/plugins/media/api/mutations.cjs +88 -0
- package/dist/packages/stack/src/plugins/media/api/mutations.mjs +82 -0
- package/dist/packages/stack/src/plugins/media/api/plugin.cjs +525 -0
- package/dist/packages/stack/src/plugins/media/api/plugin.mjs +523 -0
- package/dist/packages/stack/src/plugins/media/api/query-key-defs.cjs +19 -0
- package/dist/packages/stack/src/plugins/media/api/query-key-defs.mjs +16 -0
- package/dist/packages/stack/src/plugins/media/api/serializers.cjs +17 -0
- package/dist/packages/stack/src/plugins/media/api/serializers.mjs +14 -0
- package/dist/packages/stack/src/plugins/media/api/storage-adapter.cjs +15 -0
- package/dist/packages/stack/src/plugins/media/api/storage-adapter.mjs +11 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.cjs +129 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.mjs +127 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.cjs +58 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.mjs +56 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.cjs +94 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.mjs +92 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.cjs +171 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.mjs +168 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.cjs +308 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.mjs +305 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.cjs +104 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.mjs +102 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.cjs +70 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.mjs +68 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.cjs +21 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.mjs +17 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.cjs +35 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.cjs +125 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.mjs +123 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.mjs +33 -0
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +222 -0
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +214 -0
- package/dist/packages/stack/src/plugins/media/client/plugin.cjs +94 -0
- package/dist/packages/stack/src/plugins/media/client/plugin.mjs +92 -0
- package/dist/packages/stack/src/plugins/media/client/upload.cjs +121 -0
- package/dist/packages/stack/src/plugins/media/client/upload.mjs +119 -0
- package/dist/packages/stack/src/plugins/media/client/utils/image-compression.cjs +67 -0
- package/dist/packages/stack/src/plugins/media/client/utils/image-compression.mjs +65 -0
- package/dist/packages/stack/src/plugins/media/db.cjs +62 -0
- package/dist/packages/stack/src/plugins/media/db.mjs +60 -0
- package/dist/packages/stack/src/plugins/media/schemas.cjs +41 -0
- package/dist/packages/stack/src/plugins/media/schemas.mjs +35 -0
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.cjs +18 -1
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.mjs +19 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.cjs +2 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.mjs +2 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.cjs +3 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.mjs +3 -2
- package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.cjs +12 -5
- package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.mjs +12 -5
- package/dist/plugins/blog/api/index.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +60 -3
- package/dist/plugins/blog/client/index.d.mts +60 -3
- package/dist/plugins/blog/client/index.d.ts +60 -3
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/cms/client/index.d.cts +73 -3
- package/dist/plugins/cms/client/index.d.mts +73 -3
- package/dist/plugins/cms/client/index.d.ts +73 -3
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/plugins/media/api/adapters/s3.cjs +106 -0
- package/dist/plugins/media/api/adapters/s3.d.cts +60 -0
- package/dist/plugins/media/api/adapters/s3.d.mts +60 -0
- package/dist/plugins/media/api/adapters/s3.d.ts +60 -0
- package/dist/plugins/media/api/adapters/s3.mjs +104 -0
- package/dist/plugins/media/api/adapters/vercel-blob.cjs +54 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.cts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.mts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.ts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.mjs +52 -0
- package/dist/plugins/media/api/index.cjs +26 -0
- package/dist/plugins/media/api/index.d.cts +116 -0
- package/dist/plugins/media/api/index.d.mts +116 -0
- package/dist/plugins/media/api/index.d.ts +116 -0
- package/dist/plugins/media/api/index.mjs +6 -0
- package/dist/plugins/media/client/components/index.cjs +10 -0
- package/dist/plugins/media/client/components/index.d.cts +55 -0
- package/dist/plugins/media/client/components/index.d.mts +55 -0
- package/dist/plugins/media/client/components/index.d.ts +55 -0
- package/dist/plugins/media/client/components/index.mjs +2 -0
- package/dist/plugins/media/client/hooks/index.cjs +13 -0
- package/dist/plugins/media/client/hooks/index.d.cts +53 -0
- package/dist/plugins/media/client/hooks/index.d.mts +53 -0
- package/dist/plugins/media/client/hooks/index.d.ts +53 -0
- package/dist/plugins/media/client/hooks/index.mjs +1 -0
- package/dist/plugins/media/client/index.cjs +9 -0
- package/dist/plugins/media/client/index.d.cts +242 -0
- package/dist/plugins/media/client/index.d.mts +242 -0
- package/dist/plugins/media/client/index.d.ts +242 -0
- package/dist/plugins/media/client/index.mjs +2 -0
- package/dist/plugins/media/client.css +1 -0
- package/dist/plugins/media/query-keys.cjs +72 -0
- package/dist/plugins/media/query-keys.d.cts +49 -0
- package/dist/plugins/media/query-keys.d.mts +49 -0
- package/dist/plugins/media/query-keys.d.ts +49 -0
- package/dist/plugins/media/query-keys.mjs +70 -0
- package/dist/plugins/media/style.css +1 -0
- package/dist/shared/{stack.DOZ1EXjM.d.mts → stack.6mEHS2WH.d.mts} +3 -3
- package/dist/shared/{stack.DX-tQ93o.d.cts → stack.AJTXI7kw.d.cts} +3 -3
- package/dist/shared/{stack.DRpeDS6X.d.ts → stack.BMx2QYOK.d.ts} +25 -0
- package/dist/shared/stack.BUTXWiG-.d.ts +286 -0
- package/dist/shared/stack.C7Y9sBDg.d.mts +286 -0
- package/dist/shared/stack.C7vfOBmO.d.mts +63 -0
- package/dist/shared/stack.CAni8dnD.d.cts +63 -0
- package/dist/shared/stack.CLcnSF_b.d.cts +25 -0
- package/dist/shared/stack.CLcnSF_b.d.mts +25 -0
- package/dist/shared/stack.CLcnSF_b.d.ts +25 -0
- package/dist/shared/stack.CYSwntXC.d.ts +63 -0
- package/dist/shared/{stack.Jb0kQDJC.d.mts → stack.Cd6McBu1.d.mts} +25 -0
- package/dist/shared/stack.CoBj86jf.d.cts +109 -0
- package/dist/shared/stack.CoBj86jf.d.mts +109 -0
- package/dist/shared/stack.CoBj86jf.d.ts +109 -0
- package/dist/shared/{stack.BXxrFL9R.d.ts → stack.D7HSzZdG.d.ts} +5 -5
- package/dist/shared/{stack.DzOhpIYM.d.mts → stack.DjgpFWq3.d.cts} +5 -5
- package/dist/shared/{stack.BxFl46lB.d.cts → stack.DxQl8Wa1.d.cts} +25 -0
- package/dist/shared/{stack.BSqJrCTM.d.cts → stack.IUeyQKrm.d.mts} +5 -5
- package/dist/shared/{stack.VF6FhyZw.d.ts → stack.QYn-Px94.d.ts} +3 -3
- package/dist/shared/stack.vxskCkim.d.cts +286 -0
- package/package.json +113 -4
- package/src/plugins/blog/client/components/forms/image-field.tsx +35 -4
- package/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +67 -12
- package/src/plugins/blog/client/components/forms/markdown-editor.tsx +106 -10
- package/src/plugins/blog/client/overrides.ts +58 -1
- package/src/plugins/cms/client/components/forms/content-form.tsx +26 -7
- package/src/plugins/cms/client/components/forms/file-upload.tsx +73 -15
- package/src/plugins/cms/client/overrides.ts +57 -2
- package/src/plugins/kanban/client/components/forms/board-form.tsx +1 -1
- package/src/plugins/kanban/client/components/forms/task-form.tsx +7 -1
- package/src/plugins/kanban/client/overrides.ts +25 -0
- package/src/plugins/media/__tests__/__stubs__/vercel-blob-server.ts +9 -0
- package/src/plugins/media/__tests__/getters.test.ts +274 -0
- package/src/plugins/media/__tests__/mutations.test.ts +299 -0
- package/src/plugins/media/__tests__/plugin.test.ts +752 -0
- package/src/plugins/media/__tests__/query-key-defs.test.ts +54 -0
- package/src/plugins/media/__tests__/storage-adapters.test.ts +351 -0
- package/src/plugins/media/api/adapters/local.ts +79 -0
- package/src/plugins/media/api/adapters/s3.ts +198 -0
- package/src/plugins/media/api/adapters/vercel-blob.ts +132 -0
- package/src/plugins/media/api/getters.ts +174 -0
- package/src/plugins/media/api/index.ts +41 -0
- package/src/plugins/media/api/mutations.ts +179 -0
- package/src/plugins/media/api/plugin.ts +855 -0
- package/src/plugins/media/api/query-key-defs.ts +41 -0
- package/src/plugins/media/api/serializers.ts +28 -0
- package/src/plugins/media/api/storage-adapter.ts +139 -0
- package/src/plugins/media/client/components/index.tsx +6 -0
- package/src/plugins/media/client/components/media-picker/asset-card.tsx +150 -0
- package/src/plugins/media/client/components/media-picker/asset-preview-button.tsx +67 -0
- package/src/plugins/media/client/components/media-picker/browse-tab.tsx +116 -0
- package/src/plugins/media/client/components/media-picker/folder-tree.tsx +188 -0
- package/src/plugins/media/client/components/media-picker/index.tsx +347 -0
- package/src/plugins/media/client/components/media-picker/upload-tab.tsx +108 -0
- package/src/plugins/media/client/components/media-picker/url-tab.tsx +72 -0
- package/src/plugins/media/client/components/media-picker/utils.ts +17 -0
- package/src/plugins/media/client/components/pages/library-page.internal.tsx +134 -0
- package/src/plugins/media/client/components/pages/library-page.tsx +42 -0
- package/src/plugins/media/client/hooks/index.tsx +9 -0
- package/src/plugins/media/client/hooks/use-media.tsx +289 -0
- package/src/plugins/media/client/index.ts +4 -0
- package/src/plugins/media/client/overrides.ts +127 -0
- package/src/plugins/media/client/plugin.tsx +184 -0
- package/src/plugins/media/client/upload.ts +171 -0
- package/src/plugins/media/client/utils/image-compression.ts +131 -0
- package/src/plugins/media/client.css +1 -0
- package/src/plugins/media/db.ts +62 -0
- package/src/plugins/media/query-keys.ts +96 -0
- package/src/plugins/media/schemas.ts +37 -0
- package/src/plugins/media/style.css +1 -0
- package/src/plugins/media/types.ts +26 -0
- package/dist/shared/{stack.BWp0hcm9.d.ts → stack.BQmuNl5p.d.cts} +3 -3
- package/dist/shared/{stack.BWp0hcm9.d.cts → stack.BQmuNl5p.d.mts} +3 -3
- package/dist/shared/{stack.BWp0hcm9.d.mts → stack.BQmuNl5p.d.ts} +3 -3
- package/dist/shared/{stack.BvCR4-9H.d.ts → stack.D4Cea8II.d.ts} +3 -3
- package/dist/shared/{stack.CWxAl9K3.d.mts → stack.HE_IvqV5.d.mts} +3 -3
- package/dist/shared/{stack.BOokfhZD.d.cts → stack.Rtcvl8sS.d.cts} +3 -3
|
@@ -8,7 +8,12 @@ import { editorViewCtx, parserCtx } from "@milkdown/kit/core";
|
|
|
8
8
|
import { listener, listenerCtx } from "@milkdown/kit/plugin/listener";
|
|
9
9
|
import { Slice } from "@milkdown/kit/prose/model";
|
|
10
10
|
import { Selection } from "@milkdown/kit/prose/state";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
useLayoutEffect,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
type MutableRefObject,
|
|
16
|
+
} from "react";
|
|
12
17
|
|
|
13
18
|
export interface MarkdownEditorProps {
|
|
14
19
|
value?: string;
|
|
@@ -18,6 +23,19 @@ export interface MarkdownEditorProps {
|
|
|
18
23
|
uploadImage?: (file: File) => Promise<string>;
|
|
19
24
|
/** Placeholder text shown when the editor is empty. */
|
|
20
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;
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
export function MarkdownEditor({
|
|
@@ -26,6 +44,8 @@ export function MarkdownEditor({
|
|
|
26
44
|
className,
|
|
27
45
|
uploadImage,
|
|
28
46
|
placeholder = "Write something...",
|
|
47
|
+
insertImageRef,
|
|
48
|
+
openMediaPickerForImageBlock,
|
|
29
49
|
}: MarkdownEditorProps) {
|
|
30
50
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
31
51
|
const crepeRef = useRef<Crepe | null>(null);
|
|
@@ -33,6 +53,9 @@ export function MarkdownEditor({
|
|
|
33
53
|
const [isReady, setIsReady] = useState(false);
|
|
34
54
|
const onChangeRef = useRef<typeof onChange>(onChange);
|
|
35
55
|
const initialValueRef = useRef<string>(value ?? "");
|
|
56
|
+
const openMediaPickerRef = useRef<typeof openMediaPickerForImageBlock>(
|
|
57
|
+
openMediaPickerForImageBlock,
|
|
58
|
+
);
|
|
36
59
|
type ThrottledFn = ((markdown: string) => void) & {
|
|
37
60
|
cancel?: () => void;
|
|
38
61
|
flush?: () => void;
|
|
@@ -40,12 +63,24 @@ export function MarkdownEditor({
|
|
|
40
63
|
const throttledOnChangeRef = useRef<ThrottledFn | null>(null);
|
|
41
64
|
|
|
42
65
|
onChangeRef.current = onChange;
|
|
66
|
+
openMediaPickerRef.current = openMediaPickerForImageBlock;
|
|
43
67
|
|
|
44
68
|
useLayoutEffect(() => {
|
|
45
69
|
if (crepeRef.current) return;
|
|
46
70
|
const container = containerRef.current;
|
|
47
71
|
if (!container) return;
|
|
48
72
|
|
|
73
|
+
const hasMediaPicker = !!openMediaPickerRef.current;
|
|
74
|
+
|
|
75
|
+
const imageBlockConfig: Record<string, unknown> = {};
|
|
76
|
+
if (uploadImage) {
|
|
77
|
+
imageBlockConfig.onUpload = async (file: File) => uploadImage(file);
|
|
78
|
+
}
|
|
79
|
+
if (hasMediaPicker) {
|
|
80
|
+
imageBlockConfig.blockUploadPlaceholderText = "Media Picker";
|
|
81
|
+
imageBlockConfig.inlineUploadPlaceholderText = "Media Picker";
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
const crepe = new Crepe({
|
|
50
85
|
root: container,
|
|
51
86
|
defaultValue: initialValueRef.current,
|
|
@@ -53,19 +88,47 @@ export function MarkdownEditor({
|
|
|
53
88
|
[CrepeFeature.Placeholder]: {
|
|
54
89
|
text: placeholder,
|
|
55
90
|
},
|
|
56
|
-
...(
|
|
57
|
-
? {
|
|
58
|
-
[CrepeFeature.ImageBlock]: {
|
|
59
|
-
onUpload: async (file: File) => {
|
|
60
|
-
const url = await uploadImage(file);
|
|
61
|
-
return url;
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
}
|
|
91
|
+
...(Object.keys(imageBlockConfig).length > 0
|
|
92
|
+
? { [CrepeFeature.ImageBlock]: imageBlockConfig }
|
|
65
93
|
: {}),
|
|
66
94
|
},
|
|
67
95
|
});
|
|
68
96
|
|
|
97
|
+
// Intercept clicks on Crepe image-block upload placeholders so that the
|
|
98
|
+
// native file dialog is suppressed and the media picker is opened instead.
|
|
99
|
+
const interceptHandler = (e: MouseEvent) => {
|
|
100
|
+
if (!openMediaPickerRef.current) return;
|
|
101
|
+
const target = e.target as Element;
|
|
102
|
+
// Only intercept clicks inside the upload placeholder area.
|
|
103
|
+
const inPlaceholder = target.closest(".image-edit .placeholder");
|
|
104
|
+
if (!inPlaceholder) return;
|
|
105
|
+
// Let the hidden file <input> itself through (shouldn't receive clicks normally).
|
|
106
|
+
if ((target as HTMLElement).matches("input")) return;
|
|
107
|
+
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
|
|
111
|
+
const imageEdit = inPlaceholder.closest(".image-edit");
|
|
112
|
+
const linkInput = imageEdit?.querySelector(
|
|
113
|
+
".link-input-area",
|
|
114
|
+
) as HTMLInputElement | null;
|
|
115
|
+
|
|
116
|
+
openMediaPickerRef.current((url: string) => {
|
|
117
|
+
if (!linkInput) return;
|
|
118
|
+
// Use the native setter so Vue's reactivity picks up the change.
|
|
119
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
120
|
+
HTMLInputElement.prototype,
|
|
121
|
+
"value",
|
|
122
|
+
)?.set;
|
|
123
|
+
nativeSetter?.call(linkInput, url);
|
|
124
|
+
linkInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
125
|
+
linkInput.dispatchEvent(
|
|
126
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
container.addEventListener("click", interceptHandler, true);
|
|
131
|
+
|
|
69
132
|
// Prepare throttled onChange once per editor instance
|
|
70
133
|
throttledOnChangeRef.current = throttle((markdown: string) => {
|
|
71
134
|
if (onChangeRef.current) onChangeRef.current(markdown);
|
|
@@ -86,6 +149,7 @@ export function MarkdownEditor({
|
|
|
86
149
|
crepeRef.current = crepe;
|
|
87
150
|
|
|
88
151
|
return () => {
|
|
152
|
+
container.removeEventListener("click", interceptHandler, true);
|
|
89
153
|
try {
|
|
90
154
|
isReadyRef.current = false;
|
|
91
155
|
throttledOnChangeRef.current?.cancel?.();
|
|
@@ -133,6 +197,38 @@ export function MarkdownEditor({
|
|
|
133
197
|
});
|
|
134
198
|
}, [value, isReady]);
|
|
135
199
|
|
|
200
|
+
// Expose insertImage via ref so the parent can insert images programmatically
|
|
201
|
+
useLayoutEffect(() => {
|
|
202
|
+
if (!insertImageRef) return;
|
|
203
|
+
insertImageRef.current = (url: string) => {
|
|
204
|
+
if (!crepeRef.current || !isReadyRef.current) return;
|
|
205
|
+
try {
|
|
206
|
+
const currentMarkdown = crepeRef.current.getMarkdown?.() ?? "";
|
|
207
|
+
const imageMarkdown = `\n\n\n\n`;
|
|
208
|
+
const newMarkdown = currentMarkdown.trimEnd() + imageMarkdown;
|
|
209
|
+
crepeRef.current.editor.action((ctx) => {
|
|
210
|
+
const view = ctx.get(editorViewCtx);
|
|
211
|
+
const parser = ctx.get(parserCtx);
|
|
212
|
+
const doc = parser(newMarkdown);
|
|
213
|
+
if (!doc) return;
|
|
214
|
+
const state = view.state;
|
|
215
|
+
const tr = state.tr.replace(
|
|
216
|
+
0,
|
|
217
|
+
state.doc.content.size,
|
|
218
|
+
new Slice(doc.content, 0, 0),
|
|
219
|
+
);
|
|
220
|
+
view.dispatch(tr);
|
|
221
|
+
});
|
|
222
|
+
if (onChangeRef.current) onChangeRef.current(newMarkdown);
|
|
223
|
+
} catch {
|
|
224
|
+
// Editor may not be ready yet
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
return () => {
|
|
228
|
+
if (insertImageRef) insertImageRef.current = null;
|
|
229
|
+
};
|
|
230
|
+
}, [insertImageRef]);
|
|
231
|
+
|
|
136
232
|
return (
|
|
137
233
|
<div ref={containerRef} className={cn("milkdown-custom", className)} />
|
|
138
234
|
);
|
|
@@ -2,6 +2,18 @@ import type { SerializedPost } from "../types";
|
|
|
2
2
|
import type { ComponentType, ReactNode } from "react";
|
|
3
3
|
import type { BlogLocalization } from "./localization";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Props for the overridable blog featured image input component.
|
|
7
|
+
*/
|
|
8
|
+
export interface BlogImageInputFieldProps {
|
|
9
|
+
/** Current image URL value */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Called when the image URL changes */
|
|
12
|
+
onChange: (value: string) => void;
|
|
13
|
+
/** Whether the field is required */
|
|
14
|
+
isRequired?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
/**
|
|
6
18
|
* Context passed to lifecycle hooks
|
|
7
19
|
*/
|
|
@@ -48,9 +60,54 @@ export interface BlogPluginOverrides {
|
|
|
48
60
|
React.ImgHTMLAttributes<HTMLImageElement> & Record<string, any>
|
|
49
61
|
>;
|
|
50
62
|
/**
|
|
51
|
-
* Function used to upload
|
|
63
|
+
* Function used to upload a new image file and return its URL.
|
|
64
|
+
* This is separate from `imagePicker`, which selects an existing asset URL.
|
|
52
65
|
*/
|
|
53
66
|
uploadImage: (file: File) => Promise<string>;
|
|
67
|
+
/**
|
|
68
|
+
* Optional custom component for the featured image field.
|
|
69
|
+
*
|
|
70
|
+
* When provided it replaces the default file-upload input entirely.
|
|
71
|
+
* The component receives `value` (current URL string) and `onChange` (setter).
|
|
72
|
+
*
|
|
73
|
+
* Typical use case: render a preview when a value is set, and a media-picker
|
|
74
|
+
* trigger when no value is set.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* imageInputField: ({ value, onChange }) =>
|
|
79
|
+
* value ? (
|
|
80
|
+
* <div>
|
|
81
|
+
* <img src={value} alt="Preview" />
|
|
82
|
+
* <MediaPicker trigger={<button>Change</button>} accept={["image/*"]}
|
|
83
|
+
* onSelect={(assets) => onChange(assets[0].url)} />
|
|
84
|
+
* </div>
|
|
85
|
+
* ) : (
|
|
86
|
+
* <MediaPicker trigger={<button>Browse media</button>} accept={["image/*"]}
|
|
87
|
+
* onSelect={(assets) => onChange(assets[0].url)} />
|
|
88
|
+
* )
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
imageInputField?: ComponentType<BlogImageInputFieldProps>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Optional trigger component for a media picker.
|
|
95
|
+
* When provided, it is rendered adjacent to the Markdown editor and allows
|
|
96
|
+
* users to browse and select previously uploaded assets.
|
|
97
|
+
* Receives `onSelect(url)` — insert the chosen URL into the editor.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* imagePicker: ({ onSelect }) => (
|
|
102
|
+
* <MediaPicker
|
|
103
|
+
* trigger={<Button size="sm" variant="outline">Browse media</Button>}
|
|
104
|
+
* accept={["image/*"]}
|
|
105
|
+
* onSelect={(assets) => onSelect(assets[0].url)}
|
|
106
|
+
* />
|
|
107
|
+
* )
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
imagePicker?: ComponentType<{ onSelect: (url: string) => void }>;
|
|
54
111
|
/**
|
|
55
112
|
* Localization object for the blog plugin
|
|
56
113
|
*/
|
|
@@ -56,6 +56,12 @@ function buildFieldConfigFromJsonSchema(
|
|
|
56
56
|
string,
|
|
57
57
|
React.ComponentType<AutoFormInputComponentProps>
|
|
58
58
|
>,
|
|
59
|
+
imagePicker?: React.ComponentType<{ onSelect: (url: string) => void }>,
|
|
60
|
+
imageInputField?: React.ComponentType<{
|
|
61
|
+
value: string;
|
|
62
|
+
onChange: (value: string) => void;
|
|
63
|
+
isRequired?: boolean;
|
|
64
|
+
}>,
|
|
59
65
|
): FieldConfig<Record<string, unknown>> {
|
|
60
66
|
// Get base config from shared utility (handles fieldType from JSON Schema)
|
|
61
67
|
const baseConfig = buildFieldConfigBase(jsonSchema, fieldComponents);
|
|
@@ -73,14 +79,14 @@ function buildFieldConfigFromJsonSchema(
|
|
|
73
79
|
// Handle "file" fieldType when there's NO custom component for "file"
|
|
74
80
|
if (prop.fieldType === "file" && !fieldComponents?.["file"]) {
|
|
75
81
|
// Use CMSFileUpload as the default file component
|
|
76
|
-
if (!uploadImage) {
|
|
77
|
-
// Show a clear error message if uploadImage is
|
|
82
|
+
if (!uploadImage && !imageInputField) {
|
|
83
|
+
// Show a clear error message if neither uploadImage nor imageInputField is provided
|
|
78
84
|
baseConfig[key] = {
|
|
79
85
|
...baseConfig[key],
|
|
80
86
|
fieldType: () => (
|
|
81
87
|
<div className="rounded-md border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
|
82
|
-
File upload requires an <code>uploadImage</code>
|
|
83
|
-
overrides.
|
|
88
|
+
File upload requires an <code>uploadImage</code> or{" "}
|
|
89
|
+
<code>imageInputField</code> function in CMS overrides.
|
|
84
90
|
</div>
|
|
85
91
|
),
|
|
86
92
|
};
|
|
@@ -88,7 +94,12 @@ function buildFieldConfigFromJsonSchema(
|
|
|
88
94
|
baseConfig[key] = {
|
|
89
95
|
...baseConfig[key],
|
|
90
96
|
fieldType: (props: AutoFormInputComponentProps) => (
|
|
91
|
-
<CMSFileUpload
|
|
97
|
+
<CMSFileUpload
|
|
98
|
+
{...props}
|
|
99
|
+
uploadImage={uploadImage ?? (() => Promise.resolve(""))}
|
|
100
|
+
imageInputField={imageInputField}
|
|
101
|
+
imagePicker={imagePicker}
|
|
102
|
+
/>
|
|
92
103
|
),
|
|
93
104
|
};
|
|
94
105
|
}
|
|
@@ -151,6 +162,8 @@ export function ContentForm({
|
|
|
151
162
|
const {
|
|
152
163
|
localization: customLocalization,
|
|
153
164
|
uploadImage,
|
|
165
|
+
imagePicker,
|
|
166
|
+
imageInputField,
|
|
154
167
|
fieldComponents,
|
|
155
168
|
} = usePluginOverrides<CMSPluginOverrides>("cms");
|
|
156
169
|
const localization = { ...CMS_LOCALIZATION, ...customLocalization };
|
|
@@ -214,8 +227,14 @@ export function ContentForm({
|
|
|
214
227
|
// Build field config for AutoForm (fieldType is now embedded in jsonSchema)
|
|
215
228
|
const fieldConfig = useMemo(
|
|
216
229
|
() =>
|
|
217
|
-
buildFieldConfigFromJsonSchema(
|
|
218
|
-
|
|
230
|
+
buildFieldConfigFromJsonSchema(
|
|
231
|
+
jsonSchema,
|
|
232
|
+
uploadImage,
|
|
233
|
+
fieldComponents,
|
|
234
|
+
imagePicker,
|
|
235
|
+
imageInputField,
|
|
236
|
+
),
|
|
237
|
+
[jsonSchema, uploadImage, fieldComponents, imagePicker, imageInputField],
|
|
219
238
|
);
|
|
220
239
|
|
|
221
240
|
// Find the field to use for slug auto-generation
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
type ChangeEvent,
|
|
8
|
+
type ComponentType,
|
|
9
|
+
} from "react";
|
|
4
10
|
import { toast } from "sonner";
|
|
5
11
|
import type { AutoFormInputComponentProps } from "@workspace/ui/components/auto-form/types";
|
|
6
12
|
import { Input } from "@workspace/ui/components/input";
|
|
@@ -23,6 +29,20 @@ export interface CMSFileUploadProps extends AutoFormInputComponentProps {
|
|
|
23
29
|
* This is required - consumers must provide an upload implementation.
|
|
24
30
|
*/
|
|
25
31
|
uploadImage: (file: File) => Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Optional custom component for the image field.
|
|
34
|
+
* When provided, it replaces the default file-upload input entirely.
|
|
35
|
+
*/
|
|
36
|
+
imageInputField?: ComponentType<{
|
|
37
|
+
value: string;
|
|
38
|
+
onChange: (value: string) => void;
|
|
39
|
+
isRequired?: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Optional trigger component for a media picker.
|
|
43
|
+
* When provided, it is rendered as a "Browse media" option.
|
|
44
|
+
*/
|
|
45
|
+
imagePicker?: ComponentType<{ onSelect: (url: string) => void }>;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
/**
|
|
@@ -54,6 +74,8 @@ export function CMSFileUpload({
|
|
|
54
74
|
fieldProps,
|
|
55
75
|
field,
|
|
56
76
|
uploadImage,
|
|
77
|
+
imageInputField: ImageInputField,
|
|
78
|
+
imagePicker: ImagePickerTrigger,
|
|
57
79
|
}: CMSFileUploadProps) {
|
|
58
80
|
// Exclude showLabel and value from props spread
|
|
59
81
|
// File inputs cannot have their value set programmatically (browser security)
|
|
@@ -63,6 +85,8 @@ export function CMSFileUpload({
|
|
|
63
85
|
...safeFieldProps
|
|
64
86
|
} = fieldProps;
|
|
65
87
|
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
88
|
+
|
|
89
|
+
// All hooks must be called unconditionally before any early return.
|
|
66
90
|
const [isUploading, setIsUploading] = useState(false);
|
|
67
91
|
const [previewUrl, setPreviewUrl] = useState<string | null>(
|
|
68
92
|
field.value || null,
|
|
@@ -80,7 +104,6 @@ export function CMSFileUpload({
|
|
|
80
104
|
const file = e.target.files?.[0];
|
|
81
105
|
if (!file) return;
|
|
82
106
|
|
|
83
|
-
// Check if it's an image
|
|
84
107
|
if (!file.type.startsWith("image/")) {
|
|
85
108
|
toast.error("Please select an image file");
|
|
86
109
|
return;
|
|
@@ -106,6 +129,29 @@ export function CMSFileUpload({
|
|
|
106
129
|
field.onChange("");
|
|
107
130
|
}, [field]);
|
|
108
131
|
|
|
132
|
+
// When a custom imageInputField component is provided via overrides, delegate to it.
|
|
133
|
+
if (ImageInputField) {
|
|
134
|
+
return (
|
|
135
|
+
<FormItem>
|
|
136
|
+
{showLabel && (
|
|
137
|
+
<AutoFormLabel
|
|
138
|
+
label={fieldConfigItem?.label || label}
|
|
139
|
+
isRequired={isRequired}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
<FormControl>
|
|
143
|
+
<ImageInputField
|
|
144
|
+
value={field.value || ""}
|
|
145
|
+
onChange={field.onChange}
|
|
146
|
+
isRequired={isRequired}
|
|
147
|
+
/>
|
|
148
|
+
</FormControl>
|
|
149
|
+
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
150
|
+
<FormMessage />
|
|
151
|
+
</FormItem>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
109
155
|
return (
|
|
110
156
|
<FormItem>
|
|
111
157
|
{showLabel && (
|
|
@@ -116,19 +162,31 @@ export function CMSFileUpload({
|
|
|
116
162
|
)}
|
|
117
163
|
{!previewUrl && (
|
|
118
164
|
<FormControl>
|
|
119
|
-
<div className="
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<
|
|
165
|
+
<div className="space-y-2">
|
|
166
|
+
<div className="relative">
|
|
167
|
+
<Input
|
|
168
|
+
type="file"
|
|
169
|
+
accept="image/*"
|
|
170
|
+
{...safeFieldProps}
|
|
171
|
+
onChange={handleFileChange}
|
|
172
|
+
disabled={isUploading}
|
|
173
|
+
className="cursor-pointer"
|
|
174
|
+
data-testid="image-upload-input"
|
|
175
|
+
/>
|
|
176
|
+
{isUploading && (
|
|
177
|
+
<div className="absolute inset-0 flex items-center justify-center bg-background/80">
|
|
178
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
{ImagePickerTrigger && (
|
|
183
|
+
<div data-testid="image-picker-trigger">
|
|
184
|
+
<ImagePickerTrigger
|
|
185
|
+
onSelect={(url: string) => {
|
|
186
|
+
setPreviewUrl(url);
|
|
187
|
+
field.onChange(url);
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
132
190
|
</div>
|
|
133
191
|
)}
|
|
134
192
|
</div>
|
|
@@ -2,6 +2,18 @@ import type { ComponentType } from "react";
|
|
|
2
2
|
import type { CMSLocalization } from "./localization";
|
|
3
3
|
import type { AutoFormInputComponentProps } from "@workspace/ui/components/auto-form/types";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Props for the overridable CMS image input field component.
|
|
7
|
+
*/
|
|
8
|
+
export interface CmsImageInputFieldProps {
|
|
9
|
+
/** Current image URL value */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Called when the image URL changes */
|
|
12
|
+
onChange: (value: string) => void;
|
|
13
|
+
/** Whether the field is required */
|
|
14
|
+
isRequired?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
/**
|
|
6
18
|
* Context passed to lifecycle hooks
|
|
7
19
|
*/
|
|
@@ -46,11 +58,54 @@ export interface CMSPluginOverrides {
|
|
|
46
58
|
>;
|
|
47
59
|
|
|
48
60
|
/**
|
|
49
|
-
* Function used to upload
|
|
50
|
-
* Used by the default "file" field component
|
|
61
|
+
* Function used to upload a new image file and return its URL.
|
|
62
|
+
* Used by the default "file" field component when not selecting an existing
|
|
63
|
+
* asset via `imagePicker` or `imageInputField`.
|
|
51
64
|
*/
|
|
52
65
|
uploadImage?: (file: File) => Promise<string>;
|
|
53
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Optional custom component for image fields (fieldType: "file").
|
|
69
|
+
*
|
|
70
|
+
* When provided it replaces the default file-upload input entirely.
|
|
71
|
+
* The component receives `value` (current URL string) and `onChange` (setter).
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```tsx
|
|
75
|
+
* imageInputField: ({ value, onChange }) =>
|
|
76
|
+
* value ? (
|
|
77
|
+
* <div>
|
|
78
|
+
* <img src={value} alt="Preview" />
|
|
79
|
+
* <MediaPicker trigger={<button>Change</button>} accept={["image/*"]}
|
|
80
|
+
* onSelect={(assets) => onChange(assets[0].url)} />
|
|
81
|
+
* </div>
|
|
82
|
+
* ) : (
|
|
83
|
+
* <MediaPicker trigger={<button>Browse media</button>} accept={["image/*"]}
|
|
84
|
+
* onSelect={(assets) => onChange(assets[0].url)} />
|
|
85
|
+
* )
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
imageInputField?: ComponentType<CmsImageInputFieldProps>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Optional trigger component for a media picker.
|
|
92
|
+
* When provided, it is rendered inside the default "file" field component as a
|
|
93
|
+
* "Browse media" option, letting users select a previously uploaded asset.
|
|
94
|
+
* Receives `onSelect(url)` — the URL is set as the field value.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```tsx
|
|
98
|
+
* imagePicker: ({ onSelect }) => (
|
|
99
|
+
* <MediaPicker
|
|
100
|
+
* trigger={<Button size="sm" variant="outline">Browse media</Button>}
|
|
101
|
+
* accept={["image/*"]}
|
|
102
|
+
* onSelect={(assets) => onSelect(assets[0].url)}
|
|
103
|
+
* />
|
|
104
|
+
* )
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
imagePicker?: ComponentType<{ onSelect: (url: string) => void }>;
|
|
108
|
+
|
|
54
109
|
/**
|
|
55
110
|
* Custom field components for AutoForm fields.
|
|
56
111
|
*
|
|
@@ -50,7 +50,7 @@ export function BoardForm({ board, onClose, onSuccess }: BoardFormProps) {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
53
|
+
<form onSubmit={handleSubmit} className="space-y-4 overflow-x-hidden">
|
|
54
54
|
<div className="space-y-2">
|
|
55
55
|
<Label htmlFor="name">Name *</Label>
|
|
56
56
|
<Input
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
} from "@workspace/ui/components/select";
|
|
15
15
|
import { MinimalTiptapEditor } from "@workspace/ui/components/minimal-tiptap";
|
|
16
16
|
import SearchSelect from "@workspace/ui/components/search-select";
|
|
17
|
+
import { usePluginOverrides } from "@btst/stack/context";
|
|
17
18
|
import { useTaskMutations, useSearchUsers } from "../../hooks/kanban-hooks";
|
|
19
|
+
import type { KanbanPluginOverrides } from "../../overrides";
|
|
18
20
|
import { PRIORITY_OPTIONS } from "../../../utils";
|
|
19
21
|
import type {
|
|
20
22
|
SerializedColumn,
|
|
@@ -44,6 +46,8 @@ export function TaskForm({
|
|
|
44
46
|
onDelete,
|
|
45
47
|
}: TaskFormProps) {
|
|
46
48
|
const isEditing = !!taskId;
|
|
49
|
+
const { uploadImage, imagePicker: imagePickerTrigger } =
|
|
50
|
+
usePluginOverrides<KanbanPluginOverrides>("kanban");
|
|
47
51
|
const {
|
|
48
52
|
createTask,
|
|
49
53
|
updateTask,
|
|
@@ -155,7 +159,7 @@ export function TaskForm({
|
|
|
155
159
|
};
|
|
156
160
|
|
|
157
161
|
return (
|
|
158
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
162
|
+
<form onSubmit={handleSubmit} className="space-y-4 overflow-x-hidden">
|
|
159
163
|
<div className="space-y-2">
|
|
160
164
|
<Label htmlFor="title">Title *</Label>
|
|
161
165
|
<Input
|
|
@@ -227,6 +231,8 @@ export function TaskForm({
|
|
|
227
231
|
output="markdown"
|
|
228
232
|
placeholder="Describe the task..."
|
|
229
233
|
className="min-h-[150px]"
|
|
234
|
+
uploader={uploadImage}
|
|
235
|
+
imagePickerTrigger={imagePickerTrigger}
|
|
230
236
|
/>
|
|
231
237
|
</div>
|
|
232
238
|
|
|
@@ -73,6 +73,31 @@ export interface KanbanPluginOverrides {
|
|
|
73
73
|
*/
|
|
74
74
|
headers?: HeadersInit;
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Function used to upload a new image file from the task description editor
|
|
78
|
+
* and return its URL. This is separate from `imagePicker`, which selects an
|
|
79
|
+
* existing asset URL.
|
|
80
|
+
*/
|
|
81
|
+
uploadImage?: (file: File) => Promise<string>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Optional trigger component for a media picker.
|
|
85
|
+
* When provided, it appears inside the image insertion dialog of the task description editor,
|
|
86
|
+
* letting users browse and select previously uploaded assets.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* imagePicker: ({ onSelect }) => (
|
|
91
|
+
* <MediaPicker
|
|
92
|
+
* trigger={<Button size="sm" variant="outline">Browse media</Button>}
|
|
93
|
+
* accept={["image/*"]}
|
|
94
|
+
* onSelect={(assets) => onSelect(assets[0].url)}
|
|
95
|
+
* />
|
|
96
|
+
* )
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
imagePicker?: ComponentType<{ onSelect: (url: string) => void }>;
|
|
100
|
+
|
|
76
101
|
// ============ User Resolution (required for assignee features) ============
|
|
77
102
|
|
|
78
103
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stub for @vercel/blob/server — used in tests only.
|
|
3
|
+
* The real module is an optional peer dependency.
|
|
4
|
+
*/
|
|
5
|
+
export async function handleUpload(_options: unknown): Promise<unknown> {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"handleUpload is not available in the installed @vercel/blob version. Use a version that exports @vercel/blob/server.",
|
|
8
|
+
);
|
|
9
|
+
}
|