From 3548a298f01c8cd79f6e0547b12053dd2cc66a00 Mon Sep 17 00:00:00 2001 From: Yuriy Demidov Date: Wed, 29 Apr 2026 17:29:05 +0300 Subject: [PATCH 1/2] feat(view): support gallery preview for custom file types beyond images and videos --- .../src/view/hooks/useFilesGallery/README.md | 19 +++ .../src/view/hooks/useFilesGallery/types.ts | 8 +- .../hooks/useFilesGallery/useFilesGallery.tsx | 116 ++++++++++-------- 3 files changed, 89 insertions(+), 54 deletions(-) diff --git a/packages/editor/src/view/hooks/useFilesGallery/README.md b/packages/editor/src/view/hooks/useFilesGallery/README.md index 6b421b0b8..d9bbc4efb 100644 --- a/packages/editor/src/view/hooks/useFilesGallery/README.md +++ b/packages/editor/src/view/hooks/useFilesGallery/README.md @@ -27,6 +27,7 @@ _UseFilesGalleryOptions_ | download | `(url: string, type: FilesGalleryItemType, element: Element) => string or undefined` | | | | The file download link getter (if you want to show the download action) | | copyUrl | `(url: string, type: FilesGalleryItemType, element: Element) => string or undefined` | | | | The file copy link getter (if you want to show the copy link action) | | overrideItemProps | `(url: string, type: FilesGalleryItemType, element: Element, currentProps: GalleryItemProps) => GalleryItemProps` | | | | The custom gallery item props getter (if you want to override the default gallery item props) | +| resolveCustomItem | `(url: string, type: FilesGalleryItemType, element: Element, linkObj: {name?: string or null; mimetype?: string or null}) => GalleryItemProps or undefined` | | | | Resolves base `GalleryItemProps` for elements not handled by the default image/video logic (e.g. arbitrary file links). Return `undefined` to skip the element. The returned props go through the same `download`/`copyUrl`/`overrideItemProps` pipeline with `type: 'file'`. Note: `FilesGalleryItemType` is now `'image' \| 'video' \| 'file'` — callers doing exhaustive `switch` on `type` in other options may need to handle the new `'file'` case. | _useFilesGallery returns function `openFilesGallery` with the following args_: @@ -122,3 +123,21 @@ const {openFilesGallery} = useFilesGallery(undefined, {overrideItemProps:getGall ; ``` + +If you want to handle custom file types (e.g. PDF links) that are not images or videos, provide the `resolveCustomItem` option + +```tsx +import {YfmStaticView, useFilesGallery} from '@gravity-ui/markdown-editor/view'; +import {getGalleryItemImage} from '@gravity-ui/components'; + +function resolveCustomItem(url: string, type: 'image' | 'video' | 'file', element: Element, {name, mimetype}: {name?: string | null; mimetype?: string | null}) { + if (mimetype !== 'application/pdf') return undefined; + return getGalleryItemImage({src: '/icons/pdf.svg', name: name ?? url}); +} + +const {openFilesGallery} = useFilesGallery(undefined, {resolveCustomItem}); + +
+ +
; +``` diff --git a/packages/editor/src/view/hooks/useFilesGallery/types.ts b/packages/editor/src/view/hooks/useFilesGallery/types.ts index d20448adf..c7f7a8844 100644 --- a/packages/editor/src/view/hooks/useFilesGallery/types.ts +++ b/packages/editor/src/view/hooks/useFilesGallery/types.ts @@ -5,7 +5,7 @@ export type GalleryItemPropsWithUrl = GalleryItemProps & { url?: string; }; -export type FilesGalleryItemType = 'image' | 'video'; +export type FilesGalleryItemType = 'image' | 'video' | 'file'; export type UseFilesGalleryOptions = { download?: (url: string, type: FilesGalleryItemType, element: Element) => string | undefined; @@ -16,4 +16,10 @@ export type UseFilesGalleryOptions = { element: Element, currentProps: GalleryItemProps, ) => GalleryItemProps; + resolveCustomItem?: ( + url: string, + type: FilesGalleryItemType, + element: Element, + linkObj: {name?: string | null; mimetype?: string | null}, + ) => GalleryItemProps | undefined; }; diff --git a/packages/editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx b/packages/editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx index 2fa93562b..f1321531b 100644 --- a/packages/editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx +++ b/packages/editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx @@ -22,6 +22,7 @@ export function useFilesGallery( download: getItemDownloladUrl, overrideItemProps, copyUrl: getItemCopyUrl, + resolveCustomItem, }: UseFilesGalleryOptions = {}, ) { const {openGallery} = useGallery(); @@ -42,6 +43,48 @@ export function useFilesGallery( return false; } + const buildItem = ( + link: string, + type: FilesGalleryItemType, + element: Element, + baseProps: GalleryItemPropsWithUrl, + ): GalleryItemPropsWithUrl => { + const galleryItemActions: GalleryItemAction[] = []; + + const itemCopyUrl = getItemCopyUrl?.(link, type, element); + if (itemCopyUrl) { + const handleLinkCopied = () => { + toaster.add({ + theme: 'success', + name: 'g-md-editor-gallery-copy-link', + title: i18n('link_copied'), + }); + }; + galleryItemActions.push( + getGalleryItemCopyLinkAction({ + copyUrl: itemCopyUrl, + onCopy: handleLinkCopied, + }), + ); + } + + const downloadUrl = getItemDownloladUrl?.(link, type, element); + if (downloadUrl) { + galleryItemActions.push(getGalleryItemDownloadAction({downloadUrl})); + } + + const galleryItemProps: GalleryItemPropsWithUrl = { + ...baseProps, + url: link, + actions: galleryItemActions, + }; + + return { + ...galleryItemProps, + ...overrideItemProps?.(link, type, element, galleryItemProps), + }; + }; + const targetFile = buildLinkObject(event.target); if (!targetFile || !targetFile.link) return false; @@ -60,63 +103,29 @@ export function useFilesGallery( if (linkObj.type === 'image' || supportedExtensions.includes(extension)) { const link = linkObj.link; const name = linkObj.name || ''; - - const filesGalleryItemType: FilesGalleryItemType = - supportedVideoExtensions.includes(extension) ? 'video' : 'image'; - const galleryItemActions: GalleryItemAction[] = []; - - const itemCopyUrl = getItemCopyUrl?.( - link, - filesGalleryItemType, - element, - ); - - if (itemCopyUrl) { - const handleLinkCopied = () => { - toaster.add({ - theme: 'success', - name: 'g-md-editor-gallery-copy-link', - title: i18n('link_copied'), - }); - }; - - galleryItemActions.push( - getGalleryItemCopyLinkAction({ - copyUrl: itemCopyUrl, - onCopy: handleLinkCopied, - }), - ); - } - - const downloadUrl = getItemDownloladUrl?.( - link, - filesGalleryItemType, - element, - ); - - if (downloadUrl) { - galleryItemActions.push( - getGalleryItemDownloadAction({downloadUrl}), - ); - } - - const galleryItemProps = { - ...(filesGalleryItemType === 'video' - ? getGalleryItemVideo({src: link, name: name}) - : getGalleryItemImage({src: link, name: name})), + const type: FilesGalleryItemType = supportedVideoExtensions.includes( + extension, + ) + ? 'video' + : 'image'; + const baseProps: GalleryItemPropsWithUrl = { + ...(type === 'video' + ? getGalleryItemVideo({src: link, name}) + : getGalleryItemImage({src: link, name})), url: link, - actions: galleryItemActions, }; - result.push({ - ...galleryItemProps, - ...overrideItemProps?.( - link, - filesGalleryItemType, - element, - galleryItemProps, - ), + result.push(buildItem(link, type, element, baseProps)); + } else if (resolveCustomItem) { + const link = linkObj.link; + const baseProps = resolveCustomItem(link, 'file', element, { + name: linkObj.name, + mimetype: linkObj.mimetype, }); + + if (baseProps) { + result.push(buildItem(link, 'file', element, baseProps)); + } } } @@ -140,6 +149,7 @@ export function useFilesGallery( getItemCopyUrl, getItemDownloladUrl, overrideItemProps, + resolveCustomItem, toaster, openGallery, ], From be0141fb79a4e6b8a5e33e0e7c2f0d1a32fcff26 Mon Sep 17 00:00:00 2001 From: Yuriy Demidov Date: Wed, 29 Apr 2026 17:44:56 +0300 Subject: [PATCH 2/2] fix: changes after review --- packages/editor/src/view/hooks/useFilesGallery/README.md | 4 ++-- packages/editor/src/view/hooks/useFilesGallery/types.ts | 2 +- .../editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/view/hooks/useFilesGallery/README.md b/packages/editor/src/view/hooks/useFilesGallery/README.md index d9bbc4efb..93ce67518 100644 --- a/packages/editor/src/view/hooks/useFilesGallery/README.md +++ b/packages/editor/src/view/hooks/useFilesGallery/README.md @@ -27,7 +27,7 @@ _UseFilesGalleryOptions_ | download | `(url: string, type: FilesGalleryItemType, element: Element) => string or undefined` | | | | The file download link getter (if you want to show the download action) | | copyUrl | `(url: string, type: FilesGalleryItemType, element: Element) => string or undefined` | | | | The file copy link getter (if you want to show the copy link action) | | overrideItemProps | `(url: string, type: FilesGalleryItemType, element: Element, currentProps: GalleryItemProps) => GalleryItemProps` | | | | The custom gallery item props getter (if you want to override the default gallery item props) | -| resolveCustomItem | `(url: string, type: FilesGalleryItemType, element: Element, linkObj: {name?: string or null; mimetype?: string or null}) => GalleryItemProps or undefined` | | | | Resolves base `GalleryItemProps` for elements not handled by the default image/video logic (e.g. arbitrary file links). Return `undefined` to skip the element. The returned props go through the same `download`/`copyUrl`/`overrideItemProps` pipeline with `type: 'file'`. Note: `FilesGalleryItemType` is now `'image' \| 'video' \| 'file'` — callers doing exhaustive `switch` on `type` in other options may need to handle the new `'file'` case. | +| resolveCustomItem | `(url: string, type: 'file', element: Element, linkObj: {name?: string or null; mimetype?: string or null}) => GalleryItemProps or undefined` | | | | Resolves base `GalleryItemProps` for elements not handled by the default image/video logic (e.g. arbitrary file links). Return `undefined` to skip the element. The returned props go through the same `download`/`copyUrl`/`overrideItemProps` pipeline with `type: 'file'`. If the returned props contain `actions`, they are merged with the auto-generated download/copy actions. Note: `FilesGalleryItemType` is now `'image' \| 'video' \| 'file'` — callers doing exhaustive `switch` on `type` in other options may need to handle the new `'file'` case. | _useFilesGallery returns function `openFilesGallery` with the following args_: @@ -130,7 +130,7 @@ If you want to handle custom file types (e.g. PDF links) that are not images or import {YfmStaticView, useFilesGallery} from '@gravity-ui/markdown-editor/view'; import {getGalleryItemImage} from '@gravity-ui/components'; -function resolveCustomItem(url: string, type: 'image' | 'video' | 'file', element: Element, {name, mimetype}: {name?: string | null; mimetype?: string | null}) { +function resolveCustomItem(url: string, type: 'file', element: Element, {name, mimetype}: {name?: string | null; mimetype?: string | null}) { if (mimetype !== 'application/pdf') return undefined; return getGalleryItemImage({src: '/icons/pdf.svg', name: name ?? url}); } diff --git a/packages/editor/src/view/hooks/useFilesGallery/types.ts b/packages/editor/src/view/hooks/useFilesGallery/types.ts index c7f7a8844..cc00d4a71 100644 --- a/packages/editor/src/view/hooks/useFilesGallery/types.ts +++ b/packages/editor/src/view/hooks/useFilesGallery/types.ts @@ -18,7 +18,7 @@ export type UseFilesGalleryOptions = { ) => GalleryItemProps; resolveCustomItem?: ( url: string, - type: FilesGalleryItemType, + type: 'file', element: Element, linkObj: {name?: string | null; mimetype?: string | null}, ) => GalleryItemProps | undefined; diff --git a/packages/editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx b/packages/editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx index f1321531b..6382e4d9e 100644 --- a/packages/editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx +++ b/packages/editor/src/view/hooks/useFilesGallery/useFilesGallery.tsx @@ -49,7 +49,7 @@ export function useFilesGallery( element: Element, baseProps: GalleryItemPropsWithUrl, ): GalleryItemPropsWithUrl => { - const galleryItemActions: GalleryItemAction[] = []; + const galleryItemActions: GalleryItemAction[] = [...(baseProps.actions ?? [])]; const itemCopyUrl = getItemCopyUrl?.(link, type, element); if (itemCopyUrl) { @@ -113,6 +113,7 @@ export function useFilesGallery( ? getGalleryItemVideo({src: link, name}) : getGalleryItemImage({src: link, name})), url: link, + actions: undefined, }; result.push(buildItem(link, type, element, baseProps));