0.2 在线版本打通
This commit is contained in:
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { useSessionContext } from "@supabase/auth-helpers-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
|
||||
|
||||
interface TaskResponse {
|
||||
task_id: string;
|
||||
@@ -20,7 +21,7 @@ export function DocumentTaskPanel({ documentId }: Props) {
|
||||
const { session } = useSessionContext();
|
||||
const [task, setTask] = useState<TaskResponse | null>(null);
|
||||
const [pending, setPending] = useState(false);
|
||||
const backendUrl = useMemo(() => process.env.NEXT_PUBLIC_BACKEND_URL, []);
|
||||
const backendUrl = useMemo(() => getMnoteRuntimeConfig().backendUrl, []);
|
||||
|
||||
const triggerTask = async () => {
|
||||
if (!backendUrl || !session?.access_token) return;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useImagePicker } from "@/components/media/image-picker-context";
|
||||
import type { MediaKind } from "@/types/media";
|
||||
import { emitAssetsChanged } from "@/lib/events";
|
||||
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -97,7 +98,7 @@ const MediaBlockContent = ({ block, editor }: any) => {
|
||||
const captionRef = useRef<HTMLInputElement | null>(null);
|
||||
const [captionEditing, setCaptionEditing] = useState(false);
|
||||
const shouldShowCaption = captionEditing || Boolean(block.props.caption);
|
||||
const officeBase = process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL;
|
||||
const officeBase = getMnoteRuntimeConfig().onlyofficeBaseUrl;
|
||||
const resolveDocumentId = useCallback(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const [, tail] = window.location.pathname.split("/documents/");
|
||||
@@ -240,11 +241,14 @@ const MediaBlockContent = ({ block, editor }: any) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/media/signed-url?fileUrl=${encodeURIComponent(fileUrl)}&fileName=${encodeURIComponent(
|
||||
displayFileName,
|
||||
)}&for=onlyoffice`,
|
||||
);
|
||||
const assetId = (block.props as { assetId?: string })?.assetId;
|
||||
const res = assetId
|
||||
? await fetch(`/api/media/sign?assetId=${encodeURIComponent(assetId)}`)
|
||||
: await fetch(
|
||||
`/api/media/signed-url?fileUrl=${encodeURIComponent(fileUrl)}&fileName=${encodeURIComponent(
|
||||
displayFileName,
|
||||
)}&for=onlyoffice`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
const payload = await res.json().catch(() => null);
|
||||
throw new Error(payload?.error ?? "生成签名链接失败");
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { createReactBlockSpec } from "@blocknote/react";
|
||||
import { RiFileTextFill } from "react-icons/ri";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { supabaseBrowser } from "@/lib/supabase/client";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
|
||||
|
||||
const normalizeTitle = (value?: string | null) => {
|
||||
if (!value || !value.trim()) {
|
||||
@@ -15,6 +15,7 @@ const normalizeTitle = (value?: string | null) => {
|
||||
|
||||
const PageReferenceContent = ({ pageId, title }: { pageId: string; title: string }) => {
|
||||
const router = useRouter();
|
||||
const supabaseBrowser = getSupabaseBrowserClient();
|
||||
const fallbackTitle = normalizeTitle(title);
|
||||
const [resolvedTitle, setResolvedTitle] = useState(fallbackTitle);
|
||||
|
||||
@@ -39,7 +40,7 @@ const PageReferenceContent = ({ pageId, title }: { pageId: string; title: string
|
||||
.from("documents")
|
||||
.select("title")
|
||||
.eq("id", pageId)
|
||||
.single();
|
||||
.single<{ title: string | null }>();
|
||||
if (data) {
|
||||
applyTitle(data.title);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoader";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import { extractRowsForPreview } from "@/components/online-table/utils";
|
||||
import { supabaseBrowser } from "@/lib/supabase/client";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
|
||||
|
||||
type LuckysheetSelection =
|
||||
| {
|
||||
@@ -52,6 +52,7 @@ const VIEWER_CONTAINER_PREFIX = "headless-table-viewer-";
|
||||
|
||||
const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embed = false, editable }) => {
|
||||
const containerId = useMemo(() => `${VIEWER_CONTAINER_PREFIX}${tableId}`, [tableId]);
|
||||
const supabaseBrowser = useMemo(() => getSupabaseBrowserClient(), []);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isLuckysheetReady = useLuckysheetLoader();
|
||||
const [table, setTable] = useState<DocumentTable | null>(null);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
interface QueryProviderProps {
|
||||
@@ -25,7 +24,6 @@ export function QueryProvider({ children }: QueryProviderProps) {
|
||||
return (
|
||||
<QueryClientProvider client={client}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect } from "react";
|
||||
import type { Session } from "@supabase/supabase-js";
|
||||
import { SessionContextProvider } from "@supabase/auth-helpers-react";
|
||||
import { supabaseBrowser } from "@/lib/supabase/client";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
|
||||
|
||||
interface SupabaseProviderProps {
|
||||
session: Session | null;
|
||||
@@ -12,6 +12,7 @@ interface SupabaseProviderProps {
|
||||
|
||||
export function SupabaseProvider({ session, children }: SupabaseProviderProps) {
|
||||
useEffect(() => {
|
||||
const supabaseBrowser = getSupabaseBrowserClient();
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabaseBrowser.auth.onAuthStateChange((_event, newSession) => {
|
||||
@@ -40,7 +41,10 @@ export function SupabaseProvider({ session, children }: SupabaseProviderProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SessionContextProvider supabaseClient={supabaseBrowser} initialSession={session}>
|
||||
<SessionContextProvider
|
||||
supabaseClient={getSupabaseBrowserClient()}
|
||||
initialSession={session}
|
||||
>
|
||||
{children}
|
||||
</SessionContextProvider>
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ import type { SidebarInitialData, SidebarSectionId } from "@/components/sidebar/
|
||||
import { useSidebarData } from "@/hooks/use-sidebar-data";
|
||||
import { PrivateTree } from "@/components/sidebar/private-tree";
|
||||
import { buildSidebarSectionsFromTree, flattenDocumentTree } from "@/lib/sidebar-tree";
|
||||
import { supabaseBrowser } from "@/lib/supabase/client";
|
||||
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
|
||||
import { useSearchPaletteStore } from "@/store/search-palette";
|
||||
import { useEditorBridgeStore } from "@/store/editor-bridge";
|
||||
import { FileTree } from "@/components/sidebar/file-tree";
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
import { AssetContextMenu } from "@/components/sidebar/asset-context-menu";
|
||||
import { ASSETS_CHANGED_EVENT, DOCUMENTS_CHANGED_EVENT, emitAssetsChanged, emitDocumentsChanged } from "@/lib/events";
|
||||
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
|
||||
|
||||
const TOP_BUTTONS = [
|
||||
{ id: "search", icon: SearchIcon, label: "搜索" },
|
||||
@@ -78,6 +79,21 @@ const SECTION_ICONS: Record<Exclude<SidebarSectionId, "private">, React.ReactNod
|
||||
|
||||
const normalizeStoragePath = (storagePath: string): string => storagePath.replaceAll("\\", "/");
|
||||
|
||||
const officeFileTypeFromAsset = (fileName: string | null, mimeType: string | null) => {
|
||||
const name = (fileName ?? "").trim().toLowerCase();
|
||||
const mt = (mimeType ?? "").trim().toLowerCase();
|
||||
const ext = name.includes(".") ? name.split(".").pop() : null;
|
||||
if (ext && ["doc", "docx", "odt", "rtf"].includes(ext)) return ext;
|
||||
if (ext && ["ppt", "pptx", "odp"].includes(ext)) return ext;
|
||||
if (ext && ["xls", "xlsx", "ods", "csv"].includes(ext)) return ext;
|
||||
if (ext && ["pdf"].includes(ext)) return ext;
|
||||
if (mt.includes("wordprocessingml")) return "docx";
|
||||
if (mt.includes("presentationml")) return "pptx";
|
||||
if (mt.includes("spreadsheetml")) return "xlsx";
|
||||
if (mt.includes("pdf")) return "pdf";
|
||||
return null;
|
||||
};
|
||||
|
||||
const extractMindmapIdFromStoragePath = (
|
||||
storagePath: string | null | undefined,
|
||||
): string | null => {
|
||||
@@ -112,6 +128,7 @@ interface ContextMenuState {
|
||||
export function Sidebar({ initialData }: SidebarProps) {
|
||||
const { open, setOpen, width, setWidth, collapsedSections, toggleSection, setSectionCollapsed, trashConfirm, setTrashConfirm } =
|
||||
useSidebarStore();
|
||||
const supabaseBrowser = useMemo(() => getSupabaseBrowserClient(), []);
|
||||
const sectionsTrayOpen = useSidebarStore((state) => state.sectionsTrayOpen);
|
||||
const toggleSectionsTray = useSidebarStore((state) => state.toggleSectionsTray);
|
||||
const setSectionsTrayOpen = useSidebarStore((state) => state.setSectionsTrayOpen);
|
||||
@@ -530,6 +547,36 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const officeFileType = officeFileTypeFromAsset(asset.file_name ?? null, asset.mime_type ?? null);
|
||||
if (officeFileType) {
|
||||
const officeBase = getMnoteRuntimeConfig().onlyofficeBaseUrl;
|
||||
if (!officeBase) {
|
||||
window.alert("缺少 ONLYOFFICE 地址,请在 .env.local 配置 NEXT_PUBLIC_ONLYOFFICE_BASE_URL");
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/media/sign?assetId=${encodeURIComponent(asset.id)}`);
|
||||
if (!res.ok) {
|
||||
const payload = await res.json().catch(() => null);
|
||||
throw new Error(payload?.error ?? "生成签名链接失败");
|
||||
}
|
||||
const { signedUrl } = (await res.json()) as { signedUrl: string };
|
||||
const target = new URL("/onlyoffice", window.location.origin);
|
||||
target.searchParams.set("fileUrl", signedUrl);
|
||||
target.searchParams.set("fileName", asset.file_name ?? `未命名.${officeFileType}`);
|
||||
target.searchParams.set("fileType", officeFileType);
|
||||
target.searchParams.set("assetId", asset.id);
|
||||
window.open(target.toString(), "_blank", "noopener,noreferrer");
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
window.alert((error as Error).message);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = asset.signed_url ?? asset.file_url;
|
||||
if (!url) {
|
||||
window.alert("暂无可用的文件链接");
|
||||
|
||||
Reference in New Issue
Block a user