mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
chore: use isFileURL
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user