chore: use isFileURL

This commit is contained in:
Nathan
2025-09-23 12:37:49 +08:00
parent 7684b07d48
commit 9759aff227
10 changed files with 63 additions and 63 deletions

View File

@@ -12,8 +12,7 @@ import { ReactComponent as ReloadIcon } from '@/assets/icons/reset.svg';
import { ReactComponent as DownloadIcon } from '@/assets/icons/save_as.svg';
import { notify } from '@/components/_shared/notify';
import { copyTextToClipboard } from '@/utils/copy';
import isURL from 'validator/lib/isURL';
import { getFileLegacyUrl } from '@/utils/file-storage-url';
import { getFileUrl, isFileURL } from '@/utils/file-storage-url';
export interface GalleryImage {
src: string;
@@ -25,11 +24,12 @@ export interface GalleryPreviewProps {
onClose: () => void;
previewIndex: number;
workspaceId: string;
viewId: string;
}
const buttonClassName = 'p-1 hover:bg-transparent text-white hover:text-text-action p-0';
function GalleryPreview({ images, open, onClose, previewIndex, workspaceId }: GalleryPreviewProps) {
function GalleryPreview({ images, open, onClose, previewIndex, workspaceId, viewId }: GalleryPreviewProps) {
const { t } = useTranslation();
const [index, setIndex] = useState(previewIndex);
const transformComponentRef = useRef<ReactZoomPanPinchContentRef>(null);
@@ -96,14 +96,13 @@ function GalleryPreview({ images, open, onClose, previewIndex, workspaceId }: Ga
}, [handleKeydown]);
const imageUrl = useMemo(() => {
if (isURL(images[index].src)) {
if (isFileURL(images[index].src)) {
return images[index].src;
}
const fileId = images[index].src;
return getFileLegacyUrl(workspaceId, fileId);
}, [images, index, workspaceId]);
return getFileUrl(workspaceId, viewId, fileId);
}, [images, index, workspaceId, viewId]);
if (!open) {
return null;

View File

@@ -25,7 +25,7 @@ export function FileMediaCell({
readOnly,
}: CellProps<FileMediaCellType>) {
const value = cell?.data;
const { workspaceId } = useDatabaseContext();
const { workspaceId, viewId } = useDatabaseContext();
const [openPreview, setOpenPreview] = React.useState(false);
const previewIndexRef = React.useRef(0);
const photos = useMemo(() => {
@@ -113,6 +113,7 @@ export function FileMediaCell({
<Suspense>
<GalleryPreview
workspaceId={workspaceId}
viewId={viewId}
images={photos}
previewIndex={previewIndexRef.current}
open={openPreview}

View File

@@ -1,19 +1,18 @@
import { useMemo } from 'react';
import isURL from 'validator/lib/isURL';
import { useDatabaseContext } from '@/application/database-yjs';
import { FileMediaCellDataItem } from '@/application/database-yjs/cell.type';
import { getFileLegacyUrl } from '@/utils/file-storage-url';
import { getFileUrl, isFileURL } from '@/utils/file-storage-url';
function PreviewImage({ file, onClick }: { file: FileMediaCellDataItem; onClick: () => void }) {
const { workspaceId } = useDatabaseContext();
const { workspaceId, viewId } = useDatabaseContext();
const thumb = useMemo(() => {
let fileUrl = file.url;
if (!fileUrl) return '';
if (!isURL(fileUrl)) {
fileUrl = getFileLegacyUrl(workspaceId, file.url);
if (!isFileURL(fileUrl)) {
fileUrl = getFileUrl(workspaceId, viewId, file.url);
}
const url = new URL(fileUrl);
@@ -22,7 +21,7 @@ function PreviewImage({ file, onClick }: { file: FileMediaCellDataItem; onClick:
url.searchParams.set('fit', 'crop');
return url.toString() + '&w=240&q=80';
}, [file.url, workspaceId]);
}, [file.url, workspaceId, viewId]);
return (
<div

View File

@@ -1,15 +1,14 @@
import isURL from 'validator/lib/isURL';
import { useDatabaseContext } from '@/application/database-yjs';
import { FileMediaCellDataItem } from '@/application/database-yjs/cell.type';
import FileIcon from '@/components/database/components/cell/file-media/FileIcon';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { getFileUrl, isFileURL } from '@/utils/file-storage-url';
import { openUrl } from '@/utils/url';
import { getFileLegacyUrl } from '@/utils/file-storage-url';
function UnPreviewFile({ file }: { file: FileMediaCellDataItem }) {
const { workspaceId } = useDatabaseContext();
const { workspaceId, viewId } = useDatabaseContext();
return (
<Tooltip delayDuration={500} disableHoverableContent>
@@ -20,13 +19,13 @@ function UnPreviewFile({ file }: { file: FileMediaCellDataItem }) {
className={'cursor-pointer rounded-[4px] bg-fill-content-hover text-icon-secondary'}
onClick={(e) => {
e.stopPropagation();
if (file.url && isURL(file.url)) {
if (file.url && isFileURL(file.url)) {
void openUrl(file.url, '_blank');
return;
}
const fileId = file.url;
const newUrl = getFileLegacyUrl(workspaceId, fileId);
const newUrl = getFileUrl(workspaceId, viewId, fileId);
void openUrl(newUrl, '_blank');
}}

View File

@@ -31,7 +31,7 @@ export function FileMediaCell ({
const value = cell?.data;
const { t } = useTranslation();
const { field, clock } = useFieldSelector(fieldId);
const { workspaceId } = useDatabaseContext();
const { workspaceId, viewId } = useDatabaseContext();
const typeOption = useMemo(() => {
if (!field) return null;
return parseFileMediaTypeOptions(field);
@@ -169,6 +169,7 @@ export function FileMediaCell ({
/>}
{openPreview && <Suspense><GalleryPreview
workspaceId={workspaceId}
viewId={viewId}
images={photos}
previewIndex={previewIndexRef.current}
open={openPreview}

View File

@@ -1,5 +1,4 @@
import { useMemo, useRef, useState } from 'react';
import isURL from 'validator/lib/isURL';
import { useDatabaseContext, useReadOnly } from '@/application/database-yjs';
import { FileMediaCellDataItem, FileMediaType } from '@/application/database-yjs/cell.type';
@@ -7,8 +6,8 @@ import FileIcon from '@/components/database/components/cell/file-media/FileIcon'
import FileMediaMore from '@/components/database/components/cell/file-media/FileMediaMore';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { getFileUrl, isFileURL } from '@/utils/file-storage-url';
import { openUrl } from '@/utils/url';
import { getFileLegacyUrl } from '@/utils/file-storage-url';
function FileMediaItem({
file,
@@ -26,7 +25,7 @@ function FileMediaItem({
onDelete: () => void;
}) {
const readOnly = useReadOnly();
const { workspaceId } = useDatabaseContext();
const { workspaceId, viewId } = useDatabaseContext();
const isImage = file.file_type === FileMediaType.Image;
const mouseDownStartTimeRef = useRef<number | null>(null);
@@ -48,14 +47,14 @@ function FileMediaItem({
}, [file.file_type]);
const fileUrl = useMemo(() => {
if (file.url && isURL(file.url)) {
if (file.url && isFileURL(file.url)) {
return file.url;
}
const fileId = file.url;
return getFileLegacyUrl(workspaceId, fileId);
}, [file.url, workspaceId]);
return getFileUrl(workspaceId, viewId, fileId);
}, [file.url, workspaceId, viewId]);
const [hover, setHover] = useState(false);
@@ -65,13 +64,13 @@ function FileMediaItem({
e.stopPropagation();
// Open the file in a new tab
if (file.file_type !== FileMediaType.Image) {
if (file.url && isURL(file.url)) {
if (file.url && isFileURL(file.url)) {
void openUrl(file.url, '_blank');
return;
}
const fileId = file.url;
const newUrl = getFileLegacyUrl(workspaceId, fileId);
const newUrl = getFileUrl(workspaceId, viewId, fileId);
void openUrl(newUrl, '_blank');
}

View File

@@ -1,7 +1,6 @@
import React, { forwardRef, memo, Suspense, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useReadOnly } from 'slate-react';
import isURL from 'validator/lib/isURL';
import { GalleryLayout } from '@/application/types';
import { ReactComponent as GalleryIcon } from '@/assets/icons/gallery.svg';
@@ -13,12 +12,12 @@ import ImageGallery from '@/components/editor/components/blocks/gallery/ImageGal
import { EditorElementProps, GalleryBlockNode } from '@/components/editor/editor.type';
import { useEditorContext } from '@/components/editor/EditorContext';
import { copyTextToClipboard } from '@/utils/copy';
import { getFileLegacyUrl } from '@/utils/file-storage-url';
import { getFileUrl, isFileURL } from '@/utils/file-storage-url';
const GalleryBlock = memo(
forwardRef<HTMLDivElement, EditorElementProps<GalleryBlockNode>>(({ node, children, ...attributes }, ref) => {
const { t } = useTranslation();
const { workspaceId } = useEditorContext();
const { workspaceId, viewId } = useEditorContext();
const { images, layout } = useMemo(() => node.data || {}, [node.data]);
const [openPreview, setOpenPreview] = React.useState(false);
const previewIndexRef = React.useRef(0);
@@ -36,8 +35,8 @@ const GalleryBlock = memo(
let imageUrl = image.url;
if (!imageUrl) return null;
if (!isURL(image.url)) {
imageUrl = getFileLegacyUrl(workspaceId, image.url);
if (!isFileURL(image.url)) {
imageUrl = getFileUrl(workspaceId, viewId, image.url);
}
const url = new URL(imageUrl);
@@ -51,11 +50,11 @@ const GalleryBlock = memo(
};
})
.filter(Boolean) as {
src: string;
thumb: string;
responsive: string;
}[];
}, [images, workspaceId]);
src: string;
thumb: string;
responsive: string;
}[];
}, [images, workspaceId, viewId]);
const handleOpenPreview = useCallback(() => {
setOpenPreview(true);
@@ -131,6 +130,7 @@ const GalleryBlock = memo(
<Suspense>
<GalleryPreview
workspaceId={workspaceId}
viewId={viewId}
images={photos}
previewIndex={previewIndexRef.current}
open={openPreview}

View File

@@ -22,7 +22,7 @@ function ImageToolbar({ node }: { node: ImageBlockNode }) {
const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element);
const { t } = useTranslation();
const [openPreview, setOpenPreview] = React.useState(false);
const { workspaceId } = useEditorContext();
const { workspaceId, viewId } = useEditorContext();
const onOpenPreview = () => {
setOpenPreview(true);
};
@@ -63,6 +63,7 @@ function ImageToolbar({ node }: { node: ImageBlockNode }) {
<Suspense>
<GalleryPreview
workspaceId={workspaceId}
viewId={viewId}
images={[{ src: node.data.url || '' }]}
previewIndex={0}
open={openPreview}

View File

@@ -1,5 +1,4 @@
import { getFileLegacyUrl, getFileUrl } from '@/utils/file-storage-url';
import isURL from 'validator/lib/isURL';
import { getFileUrl, isFileURL } from '@/utils/file-storage-url';
/**
* Constructs the appropriate URL for file/image blocks
@@ -20,26 +19,21 @@ export function constructFileUrl(
viewId?: string
): string {
if (!dataUrl) return '';
console.log('dataUrl', dataUrl);
// Case 1: Already a full URL (http/https)
// This is the format returned by uploadFile() function
if (isURL(dataUrl)) {
return dataUrl;
}
if (dataUrl.startsWith('http://') || dataUrl.startsWith('https://')) {
// workaround for this case:http://localhost:8000/api/file_storage/06b1b077-1042-4c5f-8bd4-774c71e27a0c/v1/blob/103df21d-c365-4b2c-87f1-80d64e956603/ea47M1S0xOXE5jIti4H3QSHAAZvoF8BOpFzRHApzc4U=
// when isURL(dataUrl) is false
if (isFileURL(dataUrl)) {
return dataUrl;
}
const fileId = dataUrl;
if (viewId) {
return getFileUrl(workspaceId, viewId, fileId);
}
// Fallback without viewId - this will likely fail to load
console.warn('File URL construction: viewId not available, file may not load correctly', { fileId });
return getFileLegacyUrl(workspaceId, fileId);
// Use empty string as viewId fallback, though this may not work
return getFileUrl(workspaceId, '', fileId);
}

View File

@@ -1,4 +1,5 @@
import { getConfigValue } from '@/utils/runtime-config';
import isURL from 'validator/lib/isURL';
/**
* Constructs file storage URLs for the AppFlowy API
@@ -12,14 +13,19 @@ function getFileStorageBaseUrl(): string {
return getConfigValue('APPFLOWY_BASE_URL', '') + '/api/file_storage';
}
/**
* Constructs URL for file upload endpoint
* @param workspaceId - The workspace ID
* @param viewId - The view ID (used as parent_dir)
* @returns Complete upload URL
*/
export function getFileUploadUrl(workspaceId: string, viewId: string): string {
return `${getFileStorageBaseUrl()}/${workspaceId}/v1/blob/${viewId}`;
export function isFileURL(url: string): boolean {
if (isURL(url)) {
return true;
}
// workaround for this case:http://localhost:8000/api/file_storage/06b1b077-1042-4c5f-8bd4-774c71e27a0c/v1/blob/103df21d-c365-4b2c-87f1-80d64e956603/ea47M1S0xOXE5jIti4H3QSHAAZvoF8BOpFzRHApzc4U=
// when isURL(url) is false
if (url.startsWith('http://localhost')) {
return true;
}
return false;
}
/**
@@ -30,17 +36,18 @@ export function getFileUploadUrl(workspaceId: string, viewId: string): string {
* @returns Complete file URL
*/
export function getFileUrl(workspaceId: string, viewId: string, fileId: string): string {
console.warn("URL should be valid - seeing this indicates a bug")
return `${getFileStorageBaseUrl()}/${workspaceId}/v1/blob/${viewId}/${fileId}`;
}
/**
* Constructs URL for file retrieval (legacy fallback without viewId)
* Constructs URL for file upload endpoint
* @param workspaceId - The workspace ID
* @param fileId - The file ID
* @returns Complete file URL (may not work properly without viewId)
* @param viewId - The view ID (used as parent_dir)
* @returns Complete upload URL
*/
export function getFileLegacyUrl(workspaceId: string, fileId: string): string {
return `${getFileStorageBaseUrl()}/${workspaceId}/v1/blob/${fileId}`;
export function getFileUploadUrl(workspaceId: string, viewId: string): string {
return `${getFileStorageBaseUrl()}/${workspaceId}/v1/blob/${viewId}`;
}
/**