@btst/stack 2.8.0 → 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 +115 -6
- 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
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
|
|
2
|
+
import { a as StorageAdapter, b as S3UploadToken } from './stack.CoBj86jf.cjs';
|
|
3
|
+
import { c as AssetListResult, l as listAssets, a as listFolders, A as AssetListParams } from './stack.CAni8dnD.cjs';
|
|
4
|
+
import * as better_call from 'better-call';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { A as Asset, F as Folder } from './stack.CLcnSF_b.cjs';
|
|
7
|
+
|
|
8
|
+
declare const AssetListQuerySchema: z.ZodObject<{
|
|
9
|
+
folderId: z.ZodOptional<z.ZodString>;
|
|
10
|
+
mimeType: z.ZodOptional<z.ZodString>;
|
|
11
|
+
query: z.ZodOptional<z.ZodString>;
|
|
12
|
+
offset: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
13
|
+
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
declare const updateAssetSchema: z.ZodObject<{
|
|
16
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
17
|
+
folderId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
declare const createFolderSchema: z.ZodObject<{
|
|
20
|
+
name: z.ZodString;
|
|
21
|
+
parentId: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Context passed to media API hooks.
|
|
26
|
+
*/
|
|
27
|
+
interface MediaApiContext<TBody = unknown, TParams = unknown, TQuery = unknown> {
|
|
28
|
+
body?: TBody;
|
|
29
|
+
params?: TParams;
|
|
30
|
+
query?: TQuery;
|
|
31
|
+
request?: Request;
|
|
32
|
+
headers?: Headers;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Configuration hooks for the media backend plugin.
|
|
37
|
+
* All hooks are optional and allow consumers to customise behaviour.
|
|
38
|
+
*/
|
|
39
|
+
interface MediaBackendHooks {
|
|
40
|
+
/**
|
|
41
|
+
* Called before a file upload is allowed (both direct and signed adapters).
|
|
42
|
+
* Throw an Error to reject the upload (e.g. if the user is not authenticated).
|
|
43
|
+
*/
|
|
44
|
+
onBeforeUpload?: (meta: {
|
|
45
|
+
filename: string;
|
|
46
|
+
mimeType: string;
|
|
47
|
+
size?: number;
|
|
48
|
+
}, context: MediaApiContext) => Promise<void> | void;
|
|
49
|
+
/**
|
|
50
|
+
* Called after an asset record is created in the database.
|
|
51
|
+
*/
|
|
52
|
+
onAfterUpload?: (asset: Asset, context: MediaApiContext) => Promise<void> | void;
|
|
53
|
+
/**
|
|
54
|
+
* Called before an asset is deleted. Throw to prevent deletion.
|
|
55
|
+
*/
|
|
56
|
+
onBeforeDelete?: (asset: Asset, context: MediaApiContext) => Promise<void> | void;
|
|
57
|
+
/**
|
|
58
|
+
* Called after an asset has been deleted from the DB and storage.
|
|
59
|
+
*/
|
|
60
|
+
onAfterDelete?: (assetId: string, context: MediaApiContext) => Promise<void> | void;
|
|
61
|
+
/**
|
|
62
|
+
* Called before listing assets. Throw to deny access.
|
|
63
|
+
*/
|
|
64
|
+
onBeforeListAssets?: (filter: z.infer<typeof AssetListQuerySchema>, context: MediaApiContext) => Promise<void> | void;
|
|
65
|
+
/**
|
|
66
|
+
* Called before updating an asset (PATCH). Throw to deny access.
|
|
67
|
+
*/
|
|
68
|
+
onBeforeUpdateAsset?: (asset: Asset, updates: z.infer<typeof updateAssetSchema>, context: MediaApiContext) => Promise<void> | void;
|
|
69
|
+
/**
|
|
70
|
+
* Called before listing folders. Throw to deny access.
|
|
71
|
+
*/
|
|
72
|
+
onBeforeListFolders?: (filter: {
|
|
73
|
+
parentId?: string;
|
|
74
|
+
}, context: MediaApiContext) => Promise<void> | void;
|
|
75
|
+
/**
|
|
76
|
+
* Called before creating a folder. Throw to deny access.
|
|
77
|
+
*/
|
|
78
|
+
onBeforeCreateFolder?: (input: z.infer<typeof createFolderSchema>, context: MediaApiContext) => Promise<void> | void;
|
|
79
|
+
/**
|
|
80
|
+
* Called before deleting a folder. Throw to deny access.
|
|
81
|
+
*/
|
|
82
|
+
onBeforeDeleteFolder?: (folder: Folder, context: MediaApiContext) => Promise<void> | void;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Configuration for the media backend plugin.
|
|
86
|
+
*/
|
|
87
|
+
interface MediaBackendConfig {
|
|
88
|
+
/**
|
|
89
|
+
* The storage adapter to use for file uploads.
|
|
90
|
+
* - `localAdapter()` — writes to the local filesystem (dev / self-hosted)
|
|
91
|
+
* - `s3Adapter()` — presigned PUT URL (AWS S3, Cloudflare R2, MinIO)
|
|
92
|
+
* - `vercelBlobAdapter()` — signed direct upload via Vercel Blob
|
|
93
|
+
*/
|
|
94
|
+
storageAdapter: StorageAdapter;
|
|
95
|
+
/**
|
|
96
|
+
* Maximum file size in bytes.
|
|
97
|
+
* Enforced server-side for `localAdapter`.
|
|
98
|
+
* Passed into the Vercel Blob token for edge enforcement.
|
|
99
|
+
* Validated against the client-reported size for `s3Adapter`.
|
|
100
|
+
* @default 10485760 (10 MB)
|
|
101
|
+
*/
|
|
102
|
+
maxFileSizeBytes?: number;
|
|
103
|
+
/**
|
|
104
|
+
* MIME type allowlist (e.g. `["image/jpeg", "image/png"]`).
|
|
105
|
+
* If omitted, all MIME types are accepted.
|
|
106
|
+
* Enforced server-side for `localAdapter`.
|
|
107
|
+
* Passed to Vercel Blob token for edge enforcement.
|
|
108
|
+
* Validated against the client-reported MIME type for `s3Adapter`.
|
|
109
|
+
*/
|
|
110
|
+
allowedMimeTypes?: string[];
|
|
111
|
+
/**
|
|
112
|
+
* URL prefixes that are allowed when creating asset records via `POST /media/assets`.
|
|
113
|
+
* When omitted the plugin automatically derives a safe default from the storage adapter:
|
|
114
|
+
* - `s3Adapter` → the configured `publicBaseUrl`
|
|
115
|
+
* - `vercelBlobAdapter` → any URL whose hostname ends with `.public.blob.vercel-storage.com`
|
|
116
|
+
* - `localAdapter` → rejects client-supplied URLs; use `POST /media/upload` instead
|
|
117
|
+
*
|
|
118
|
+
* Provide this option only when you need to override the automatic default (e.g. to allow
|
|
119
|
+
* assets from a CDN in front of your storage that uses a different domain). When using
|
|
120
|
+
* `localAdapter`, setting `allowedUrlPrefixes` explicitly opts `POST /media/assets` back in.
|
|
121
|
+
*/
|
|
122
|
+
allowedUrlPrefixes?: string[];
|
|
123
|
+
/**
|
|
124
|
+
* Optional lifecycle hooks for the media backend plugin.
|
|
125
|
+
*/
|
|
126
|
+
hooks?: MediaBackendHooks;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Media backend plugin.
|
|
130
|
+
* Provides API endpoints for managing media assets and folders, and supports
|
|
131
|
+
* local, S3-compatible, and Vercel Blob storage backends.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* import { mediaBackendPlugin, localAdapter } from "@btst/stack/plugins/media/api";
|
|
136
|
+
*
|
|
137
|
+
* mediaBackendPlugin({
|
|
138
|
+
* storageAdapter: localAdapter(),
|
|
139
|
+
* hooks: {
|
|
140
|
+
* onBeforeUpload: async (_meta, ctx) => {
|
|
141
|
+
* const session = await getSession(ctx.headers as Headers);
|
|
142
|
+
* if (!session) throw new Error("Unauthorized");
|
|
143
|
+
* },
|
|
144
|
+
* },
|
|
145
|
+
* })
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
declare const mediaBackendPlugin: (config: MediaBackendConfig) => _btst_stack_plugins_api.BackendPlugin<{
|
|
149
|
+
readonly listAssets: better_call.StrictEndpoint<"/media/assets", {} & {
|
|
150
|
+
method: "GET";
|
|
151
|
+
} & {
|
|
152
|
+
query: better_call.StandardSchemaV1<{
|
|
153
|
+
folderId?: string | undefined;
|
|
154
|
+
mimeType?: string | undefined;
|
|
155
|
+
query?: string | undefined;
|
|
156
|
+
offset?: unknown;
|
|
157
|
+
limit?: unknown;
|
|
158
|
+
}, {
|
|
159
|
+
folderId?: string | undefined;
|
|
160
|
+
mimeType?: string | undefined;
|
|
161
|
+
query?: string | undefined;
|
|
162
|
+
offset?: unknown;
|
|
163
|
+
limit?: unknown;
|
|
164
|
+
}>;
|
|
165
|
+
}, AssetListResult>;
|
|
166
|
+
readonly createAsset: better_call.StrictEndpoint<"/media/assets", {} & {
|
|
167
|
+
method: "POST";
|
|
168
|
+
body: better_call.StandardSchemaV1<{
|
|
169
|
+
filename: string;
|
|
170
|
+
originalName: string;
|
|
171
|
+
mimeType: string;
|
|
172
|
+
size: number;
|
|
173
|
+
url: string;
|
|
174
|
+
folderId?: string | undefined;
|
|
175
|
+
alt?: string | undefined;
|
|
176
|
+
}, {
|
|
177
|
+
filename: string;
|
|
178
|
+
originalName: string;
|
|
179
|
+
mimeType: string;
|
|
180
|
+
size: number;
|
|
181
|
+
url: string;
|
|
182
|
+
folderId?: string | undefined;
|
|
183
|
+
alt?: string | undefined;
|
|
184
|
+
}>;
|
|
185
|
+
}, Asset>;
|
|
186
|
+
readonly updateAsset: better_call.StrictEndpoint<"/media/assets/:id", {} & {
|
|
187
|
+
method: "PATCH";
|
|
188
|
+
body: better_call.StandardSchemaV1<{
|
|
189
|
+
alt?: string | undefined;
|
|
190
|
+
folderId?: string | null | undefined;
|
|
191
|
+
}, {
|
|
192
|
+
alt?: string | undefined;
|
|
193
|
+
folderId?: string | null | undefined;
|
|
194
|
+
}>;
|
|
195
|
+
}, Asset>;
|
|
196
|
+
readonly deleteAsset: better_call.StrictEndpoint<"/media/assets/:id", {} & {
|
|
197
|
+
method: "DELETE";
|
|
198
|
+
body: better_call.StandardSchemaV1<unknown, unknown>;
|
|
199
|
+
}, {
|
|
200
|
+
success: boolean;
|
|
201
|
+
}>;
|
|
202
|
+
readonly listFolders: better_call.StrictEndpoint<"/media/folders", {} & {
|
|
203
|
+
method: "GET";
|
|
204
|
+
} & {
|
|
205
|
+
query: better_call.StandardSchemaV1<{
|
|
206
|
+
parentId?: string | undefined;
|
|
207
|
+
}, {
|
|
208
|
+
parentId?: string | undefined;
|
|
209
|
+
}>;
|
|
210
|
+
}, Folder[]>;
|
|
211
|
+
readonly createFolder: better_call.StrictEndpoint<"/media/folders", {} & {
|
|
212
|
+
method: "POST";
|
|
213
|
+
body: better_call.StandardSchemaV1<{
|
|
214
|
+
name: string;
|
|
215
|
+
parentId?: string | undefined;
|
|
216
|
+
}, {
|
|
217
|
+
name: string;
|
|
218
|
+
parentId?: string | undefined;
|
|
219
|
+
}>;
|
|
220
|
+
}, Folder>;
|
|
221
|
+
readonly deleteFolder: better_call.StrictEndpoint<"/media/folders/:id", {} & {
|
|
222
|
+
method: "DELETE";
|
|
223
|
+
body: better_call.StandardSchemaV1<unknown, unknown>;
|
|
224
|
+
}, {
|
|
225
|
+
success: boolean;
|
|
226
|
+
}>;
|
|
227
|
+
readonly uploadDirect: better_call.StrictEndpoint<"/media/upload", {
|
|
228
|
+
metadata: {
|
|
229
|
+
allowedMediaTypes: string[];
|
|
230
|
+
};
|
|
231
|
+
} & {
|
|
232
|
+
method: "POST";
|
|
233
|
+
body: better_call.StandardSchemaV1<unknown, unknown>;
|
|
234
|
+
}, Asset>;
|
|
235
|
+
readonly uploadToken: better_call.StrictEndpoint<"/media/upload/token", {} & {
|
|
236
|
+
method: "POST";
|
|
237
|
+
body: better_call.StandardSchemaV1<{
|
|
238
|
+
filename: string;
|
|
239
|
+
mimeType: string;
|
|
240
|
+
size: number;
|
|
241
|
+
folderId?: string | undefined;
|
|
242
|
+
}, {
|
|
243
|
+
filename: string;
|
|
244
|
+
mimeType: string;
|
|
245
|
+
size: number;
|
|
246
|
+
folderId?: string | undefined;
|
|
247
|
+
}>;
|
|
248
|
+
}, S3UploadToken>;
|
|
249
|
+
readonly uploadVercelBlob: better_call.StrictEndpoint<"/media/upload/vercel-blob", {} & {
|
|
250
|
+
method: "POST";
|
|
251
|
+
body: better_call.StandardSchemaV1<unknown, unknown>;
|
|
252
|
+
}, unknown>;
|
|
253
|
+
}, {
|
|
254
|
+
listAssets: (params?: Parameters<typeof listAssets>[1]) => Promise<AssetListResult>;
|
|
255
|
+
getAssetById: (id: string) => Promise<Asset | null>;
|
|
256
|
+
listFolders: (params?: Parameters<typeof listFolders>[1]) => Promise<Folder[]>;
|
|
257
|
+
getFolderById: (id: string) => Promise<Folder | null>;
|
|
258
|
+
}>;
|
|
259
|
+
type MediaApiRouter = ReturnType<ReturnType<typeof mediaBackendPlugin>["routes"]>;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Internal query key constants for the media plugin.
|
|
263
|
+
* Shared between query-keys.ts (HTTP path) and any SSR/SSG prefetching
|
|
264
|
+
* to prevent key drift between client and server.
|
|
265
|
+
*/
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Discriminator for the asset list cache key.
|
|
269
|
+
*/
|
|
270
|
+
interface AssetListDiscriminator {
|
|
271
|
+
folderId: string | undefined;
|
|
272
|
+
mimeType: string | undefined;
|
|
273
|
+
query: string | undefined;
|
|
274
|
+
limit: number | undefined;
|
|
275
|
+
offset: number | undefined;
|
|
276
|
+
}
|
|
277
|
+
declare function assetListDiscriminator(params?: AssetListParams): AssetListDiscriminator;
|
|
278
|
+
/** Full query key builders — use these with `queryClient.setQueryData()`. */
|
|
279
|
+
declare const MEDIA_QUERY_KEYS: {
|
|
280
|
+
assetsList: (params?: AssetListParams) => readonly ["media", "assets", "list", AssetListDiscriminator];
|
|
281
|
+
assetDetail: (id: string) => readonly ["media", "assets", "detail", string];
|
|
282
|
+
foldersList: (parentId?: string | null) => readonly ["media", "folders", "list", string];
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export { MEDIA_QUERY_KEYS as a, assetListDiscriminator as b, mediaBackendPlugin as m };
|
|
286
|
+
export type { AssetListDiscriminator as A, MediaApiRouter as M, MediaApiContext as c, MediaBackendHooks as d, MediaBackendConfig as e };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btst/stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "A composable, plugin-based library for building full-stack applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -414,6 +414,77 @@
|
|
|
414
414
|
}
|
|
415
415
|
},
|
|
416
416
|
"./plugins/comments/css": "./dist/plugins/comments/style.css",
|
|
417
|
+
"./plugins/media/api": {
|
|
418
|
+
"import": {
|
|
419
|
+
"types": "./dist/plugins/media/api/index.d.ts",
|
|
420
|
+
"default": "./dist/plugins/media/api/index.mjs"
|
|
421
|
+
},
|
|
422
|
+
"require": {
|
|
423
|
+
"types": "./dist/plugins/media/api/index.d.cts",
|
|
424
|
+
"default": "./dist/plugins/media/api/index.cjs"
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
"./plugins/media/api/adapters/s3": {
|
|
428
|
+
"import": {
|
|
429
|
+
"types": "./dist/plugins/media/api/adapters/s3.d.ts",
|
|
430
|
+
"default": "./dist/plugins/media/api/adapters/s3.mjs"
|
|
431
|
+
},
|
|
432
|
+
"require": {
|
|
433
|
+
"types": "./dist/plugins/media/api/adapters/s3.d.cts",
|
|
434
|
+
"default": "./dist/plugins/media/api/adapters/s3.cjs"
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
"./plugins/media/api/adapters/vercel-blob": {
|
|
438
|
+
"import": {
|
|
439
|
+
"types": "./dist/plugins/media/api/adapters/vercel-blob.d.ts",
|
|
440
|
+
"default": "./dist/plugins/media/api/adapters/vercel-blob.mjs"
|
|
441
|
+
},
|
|
442
|
+
"require": {
|
|
443
|
+
"types": "./dist/plugins/media/api/adapters/vercel-blob.d.cts",
|
|
444
|
+
"default": "./dist/plugins/media/api/adapters/vercel-blob.cjs"
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
"./plugins/media/client": {
|
|
448
|
+
"import": {
|
|
449
|
+
"types": "./dist/plugins/media/client/index.d.ts",
|
|
450
|
+
"default": "./dist/plugins/media/client/index.mjs"
|
|
451
|
+
},
|
|
452
|
+
"require": {
|
|
453
|
+
"types": "./dist/plugins/media/client/index.d.cts",
|
|
454
|
+
"default": "./dist/plugins/media/client/index.cjs"
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
"./plugins/media/client/components": {
|
|
458
|
+
"import": {
|
|
459
|
+
"types": "./dist/plugins/media/client/components/index.d.ts",
|
|
460
|
+
"default": "./dist/plugins/media/client/components/index.mjs"
|
|
461
|
+
},
|
|
462
|
+
"require": {
|
|
463
|
+
"types": "./dist/plugins/media/client/components/index.d.cts",
|
|
464
|
+
"default": "./dist/plugins/media/client/components/index.cjs"
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
"./plugins/media/client/hooks": {
|
|
468
|
+
"import": {
|
|
469
|
+
"types": "./dist/plugins/media/client/hooks/index.d.ts",
|
|
470
|
+
"default": "./dist/plugins/media/client/hooks/index.mjs"
|
|
471
|
+
},
|
|
472
|
+
"require": {
|
|
473
|
+
"types": "./dist/plugins/media/client/hooks/index.d.cts",
|
|
474
|
+
"default": "./dist/plugins/media/client/hooks/index.cjs"
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
"./plugins/media/query-keys": {
|
|
478
|
+
"import": {
|
|
479
|
+
"types": "./dist/plugins/media/query-keys.d.ts",
|
|
480
|
+
"default": "./dist/plugins/media/query-keys.mjs"
|
|
481
|
+
},
|
|
482
|
+
"require": {
|
|
483
|
+
"types": "./dist/plugins/media/query-keys.d.cts",
|
|
484
|
+
"default": "./dist/plugins/media/query-keys.cjs"
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
"./plugins/media/css": "./dist/plugins/media/client.css",
|
|
417
488
|
"./plugins/route-docs/client": {
|
|
418
489
|
"import": {
|
|
419
490
|
"types": "./dist/plugins/route-docs/client/index.d.ts",
|
|
@@ -610,6 +681,27 @@
|
|
|
610
681
|
"plugins/comments/query-keys": [
|
|
611
682
|
"./dist/plugins/comments/query-keys.d.ts"
|
|
612
683
|
],
|
|
684
|
+
"plugins/media/api": [
|
|
685
|
+
"./dist/plugins/media/api/index.d.ts"
|
|
686
|
+
],
|
|
687
|
+
"plugins/media/api/adapters/s3": [
|
|
688
|
+
"./dist/plugins/media/api/adapters/s3.d.ts"
|
|
689
|
+
],
|
|
690
|
+
"plugins/media/api/adapters/vercel-blob": [
|
|
691
|
+
"./dist/plugins/media/api/adapters/vercel-blob.d.ts"
|
|
692
|
+
],
|
|
693
|
+
"plugins/media/client": [
|
|
694
|
+
"./dist/plugins/media/client/index.d.ts"
|
|
695
|
+
],
|
|
696
|
+
"plugins/media/client/components": [
|
|
697
|
+
"./dist/plugins/media/client/components/index.d.ts"
|
|
698
|
+
],
|
|
699
|
+
"plugins/media/client/hooks": [
|
|
700
|
+
"./dist/plugins/media/client/hooks/index.d.ts"
|
|
701
|
+
],
|
|
702
|
+
"plugins/media/query-keys": [
|
|
703
|
+
"./dist/plugins/media/query-keys.d.ts"
|
|
704
|
+
],
|
|
613
705
|
"plugins/route-docs/client": [
|
|
614
706
|
"./dist/plugins/route-docs/client/index.d.ts"
|
|
615
707
|
],
|
|
@@ -637,7 +729,7 @@
|
|
|
637
729
|
}
|
|
638
730
|
},
|
|
639
731
|
"dependencies": {
|
|
640
|
-
"@btst/db": "2.1.
|
|
732
|
+
"@btst/db": "2.1.1",
|
|
641
733
|
"@lukemorales/query-key-factory": "^1.3.4",
|
|
642
734
|
"@milkdown/crepe": "^7.17.1",
|
|
643
735
|
"@milkdown/kit": "^7.17.1",
|
|
@@ -646,13 +738,17 @@
|
|
|
646
738
|
},
|
|
647
739
|
"peerDependencies": {
|
|
648
740
|
"@ai-sdk/react": ">=2.0.0",
|
|
741
|
+
"@aws-sdk/client-s3": ">=3.0.0",
|
|
742
|
+
"@aws-sdk/s3-request-presigner": ">=3.0.0",
|
|
649
743
|
"@btst/yar": ">=1.2.0",
|
|
650
744
|
"@hookform/resolvers": ">=5.0.0",
|
|
651
745
|
"@radix-ui/react-dialog": ">=1.1.0",
|
|
652
746
|
"@radix-ui/react-label": ">=2.1.0",
|
|
653
747
|
"@radix-ui/react-slot": ">=1.1.0",
|
|
654
748
|
"@radix-ui/react-switch": ">=1.1.0",
|
|
749
|
+
"@tailwindcss/typography": ">=0.5.0",
|
|
655
750
|
"@tanstack/react-query": "^5.0.0",
|
|
751
|
+
"@vercel/blob": ">=0.14.0",
|
|
656
752
|
"ai": ">=5.0.0",
|
|
657
753
|
"better-call": ">=1.3.2",
|
|
658
754
|
"class-variance-authority": ">=0.7.0",
|
|
@@ -675,25 +771,38 @@
|
|
|
675
771
|
"sonner": ">=2.0.0",
|
|
676
772
|
"tailwind-merge": ">=2.6.0",
|
|
677
773
|
"tailwindcss": ">=3.0.0",
|
|
678
|
-
"@tailwindcss/typography": ">=0.5.0",
|
|
679
774
|
"zod": ">=4.2.0"
|
|
680
775
|
},
|
|
776
|
+
"peerDependenciesMeta": {
|
|
777
|
+
"@vercel/blob": {
|
|
778
|
+
"optional": true
|
|
779
|
+
},
|
|
780
|
+
"@aws-sdk/client-s3": {
|
|
781
|
+
"optional": true
|
|
782
|
+
},
|
|
783
|
+
"@aws-sdk/s3-request-presigner": {
|
|
784
|
+
"optional": true
|
|
785
|
+
}
|
|
786
|
+
},
|
|
681
787
|
"devDependencies": {
|
|
682
|
-
"tsx": "catalog:",
|
|
683
788
|
"@ai-sdk/react": "^2.0.94",
|
|
684
|
-
"@
|
|
789
|
+
"@aws-sdk/client-s3": "^3.1011.0",
|
|
790
|
+
"@aws-sdk/s3-request-presigner": "^3.1011.0",
|
|
791
|
+
"@btst/adapter-memory": "2.1.1",
|
|
685
792
|
"@btst/yar": "1.2.0",
|
|
686
793
|
"@types/react": "^19.0.0",
|
|
687
794
|
"@types/slug": "^5.0.9",
|
|
795
|
+
"@vercel/blob": "^0.27.3",
|
|
688
796
|
"@workspace/ui": "workspace:*",
|
|
689
797
|
"ai": "^5.0.94",
|
|
690
798
|
"better-call": "catalog:",
|
|
799
|
+
"knip": "^5.61.2",
|
|
691
800
|
"react": "^19.1.1",
|
|
692
801
|
"react-dom": "^19.1.1",
|
|
693
802
|
"react-error-boundary": "^4.1.2",
|
|
694
|
-
"knip": "^5.61.2",
|
|
695
803
|
"rollup-plugin-preserve-directives": "0.4.0",
|
|
696
804
|
"rollup-plugin-visualizer": "^5.12.0",
|
|
805
|
+
"tsx": "catalog:",
|
|
697
806
|
"typescript": "catalog:",
|
|
698
807
|
"unbuild": "catalog:",
|
|
699
808
|
"vitest": "catalog:",
|
|
@@ -28,13 +28,44 @@ export function FeaturedImageField({
|
|
|
28
28
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
29
29
|
const [isUploading, setIsUploading] = useState(false);
|
|
30
30
|
|
|
31
|
-
const {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const {
|
|
32
|
+
uploadImage,
|
|
33
|
+
Image,
|
|
34
|
+
localization,
|
|
35
|
+
imageInputField: ImageInput,
|
|
36
|
+
} = usePluginOverrides<BlogPluginOverrides, Partial<BlogPluginOverrides>>(
|
|
37
|
+
"blog",
|
|
38
|
+
{ localization: BLOG_LOCALIZATION },
|
|
39
|
+
);
|
|
35
40
|
|
|
36
41
|
const ImageComponent = Image ? Image : DefaultImage;
|
|
37
42
|
|
|
43
|
+
// When a custom imageInput component is provided via overrides, delegate to it.
|
|
44
|
+
if (ImageInput) {
|
|
45
|
+
return (
|
|
46
|
+
<FormItem className="flex flex-col">
|
|
47
|
+
<FormLabel>
|
|
48
|
+
{localization.BLOG_FORMS_FEATURED_IMAGE_LABEL}
|
|
49
|
+
{isRequired && (
|
|
50
|
+
<span className="text-destructive">
|
|
51
|
+
{" "}
|
|
52
|
+
{localization.BLOG_FORMS_FEATURED_IMAGE_REQUIRED_ASTERISK}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
</FormLabel>
|
|
56
|
+
<FormControl>
|
|
57
|
+
<ImageInput
|
|
58
|
+
value={value || ""}
|
|
59
|
+
onChange={onChange}
|
|
60
|
+
isRequired={isRequired}
|
|
61
|
+
/>
|
|
62
|
+
</FormControl>
|
|
63
|
+
<FormDescription />
|
|
64
|
+
<FormMessage />
|
|
65
|
+
</FormItem>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
38
69
|
const handleImageUpload = async (
|
|
39
70
|
event: React.ChangeEvent<HTMLInputElement>,
|
|
40
71
|
) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
2
3
|
import { usePluginOverrides } from "@btst/stack/context";
|
|
3
4
|
import type { BlogPluginOverrides } from "../../overrides";
|
|
4
5
|
import { BLOG_LOCALIZATION } from "../../localization";
|
|
@@ -6,24 +7,78 @@ import { MarkdownEditor, type MarkdownEditorProps } from "./markdown-editor";
|
|
|
6
7
|
|
|
7
8
|
type MarkdownEditorWithOverridesProps = Omit<
|
|
8
9
|
MarkdownEditorProps,
|
|
9
|
-
|
|
10
|
+
| "uploadImage"
|
|
11
|
+
| "placeholder"
|
|
12
|
+
| "insertImageRef"
|
|
13
|
+
| "openMediaPickerForImageBlock"
|
|
10
14
|
>;
|
|
11
15
|
|
|
12
16
|
export function MarkdownEditorWithOverrides(
|
|
13
17
|
props: MarkdownEditorWithOverridesProps,
|
|
14
18
|
) {
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const {
|
|
20
|
+
uploadImage,
|
|
21
|
+
imagePicker: ImagePickerTrigger,
|
|
22
|
+
localization,
|
|
23
|
+
} = usePluginOverrides<BlogPluginOverrides, Partial<BlogPluginOverrides>>(
|
|
24
|
+
"blog",
|
|
25
|
+
{ localization: BLOG_LOCALIZATION },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const insertImageRef = useRef<((url: string) => void) | null>(null);
|
|
29
|
+
// Holds the Crepe-image-block `setUrl` callback while the picker is open.
|
|
30
|
+
const pendingInsertUrlRef = useRef<((url: string) => void) | null>(null);
|
|
31
|
+
// Ref to the trigger wrapper so we can programmatically click the picker button.
|
|
32
|
+
const triggerContainerRef = useRef<HTMLDivElement | null>(null);
|
|
33
|
+
|
|
34
|
+
// Single onSelect handler for ImagePickerTrigger.
|
|
35
|
+
// URLs returned by the media plugin are already percent-encoded at the
|
|
36
|
+
// source (storage adapter), so no additional encoding is applied here.
|
|
37
|
+
const handleSelect = useCallback((url: string) => {
|
|
38
|
+
if (pendingInsertUrlRef.current) {
|
|
39
|
+
// Crepe image block flow: set the URL into the block's link input.
|
|
40
|
+
pendingInsertUrlRef.current(url);
|
|
41
|
+
pendingInsertUrlRef.current = null;
|
|
42
|
+
} else {
|
|
43
|
+
// Normal flow: insert image at end of markdown content.
|
|
44
|
+
insertImageRef.current?.(url);
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Called by MarkdownEditor's click interceptor when the user clicks a Crepe
|
|
49
|
+
// image-block upload placeholder.
|
|
50
|
+
const openMediaPickerForImageBlock = useCallback(
|
|
51
|
+
(setUrl: (url: string) => void) => {
|
|
52
|
+
pendingInsertUrlRef.current = setUrl;
|
|
53
|
+
// Programmatically click the visible picker trigger button.
|
|
54
|
+
const btn = triggerContainerRef.current?.querySelector(
|
|
55
|
+
'[data-testid="open-media-picker"]',
|
|
56
|
+
) as HTMLButtonElement | null;
|
|
57
|
+
btn?.click();
|
|
58
|
+
},
|
|
59
|
+
[],
|
|
60
|
+
);
|
|
21
61
|
|
|
22
62
|
return (
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
63
|
+
<div className="flex flex-col">
|
|
64
|
+
<MarkdownEditor
|
|
65
|
+
{...props}
|
|
66
|
+
uploadImage={uploadImage}
|
|
67
|
+
placeholder={localization?.BLOG_FORMS_EDITOR_PLACEHOLDER}
|
|
68
|
+
insertImageRef={insertImageRef}
|
|
69
|
+
openMediaPickerForImageBlock={
|
|
70
|
+
ImagePickerTrigger ? openMediaPickerForImageBlock : undefined
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
73
|
+
{ImagePickerTrigger && (
|
|
74
|
+
<div
|
|
75
|
+
ref={triggerContainerRef}
|
|
76
|
+
className="flex justify-end mt-1"
|
|
77
|
+
data-testid="image-picker-trigger"
|
|
78
|
+
>
|
|
79
|
+
<ImagePickerTrigger onSelect={handleSelect} />
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
28
83
|
);
|
|
29
84
|
}
|