From 0d0820ddd7e35a4d553f916f3f1a79c3b145f22f Mon Sep 17 00:00:00 2001 From: synasapmob Date: Mon, 6 Apr 2026 17:17:20 +0700 Subject: [PATCH 1/5] Implement preview documents via click file in artifact detail --- frontend/app/components/Markdown/index.tsx | 52 ++--- .../ArtifactFile/ArtifactFileList.tsx | 197 ++++++++---------- .../ArtifactFile/ArtifactFileMarkdown.tsx | 15 +- .../ArtifactFile/ArtifactFilePreview.tsx | 101 +++++++++ .../ArtifactFile/ArtifactFileReadme.tsx | 42 ++++ frontend/app/layout/Artifact/index.tsx | 60 +++--- frontend/app/utils/index.tsx | 22 +- 7 files changed, 317 insertions(+), 172 deletions(-) create mode 100644 frontend/app/layout/Artifact/ArtifactFile/ArtifactFilePreview.tsx create mode 100644 frontend/app/layout/Artifact/ArtifactFile/ArtifactFileReadme.tsx diff --git a/frontend/app/components/Markdown/index.tsx b/frontend/app/components/Markdown/index.tsx index 109860a..de86f68 100644 --- a/frontend/app/components/Markdown/index.tsx +++ b/frontend/app/components/Markdown/index.tsx @@ -6,35 +6,37 @@ interface MarkdownProps { export default ({ content }: MarkdownProps) => { return ( - { - const match = /language-(\w+)/.exec(className || ""); +
+ { + const match = /language-(\w+)/.exec(className || ""); - // when array?, possible
  • inside - const content = Array.isArray(children) - ? children.join("") - : String(children); + // when array?, possible
  • inside + const content = Array.isArray(children) + ? children.join("") + : String(children); - const isSpecialBlock = /[┌┐└┘│─]/.test(content); + const isSpecialBlock = /[┌┐└┘│─]/.test(content); + + if (match || isSpecialBlock) { + return ( +
    + {children} +
    + ); + } - if (match || isSpecialBlock) { return ( -
    - {children} -
    + + {children} + ); - } - - return ( - - {children} - - ); - }, - }} - > - {content} - + }, + }} + > + {content} + +
  • ); }; diff --git a/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileList.tsx b/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileList.tsx index 9fb6c68..2c738e5 100644 --- a/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileList.tsx +++ b/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileList.tsx @@ -10,43 +10,36 @@ import { useIncrementDownloadMutation } from "app/services/graphql-app/generated import graphqlApp from "app/services/graphql-app"; import { downloadFileWithBlob, formatBytesSizes } from "app/utils"; import utilsWalrus from "app/utils/utils.walrus"; -import { useRef, useState } from "react"; -import useClickOutside from "app/hook/useClickOutside"; +import { useState, type HTMLAttributes } from "react"; import Spinner from "app/components/Spinner"; import { extension } from "mime-types"; +import { useSearchParams } from "react-router"; interface ArtifactFileListProps { files: ArtifactFile[]; rootId: string; onRefetch: () => void; + variant?: HTMLAttributes; } -export default ({ files, rootId, onRefetch }: ArtifactFileListProps) => { +export default ({ + files, + rootId, + onRefetch, + variant, +}: ArtifactFileListProps) => { const [loading, setLoading] = useState(); - const incrementDownload = useIncrementDownloadMutation(graphqlApp.client); - - const fileRef = useRef(null); - const containerRef = useRef(null); - - useClickOutside(containerRef, () => { - removeSelectedFile(); - }); - - const addSelectedFile = (currentTarget: EventTarget & HTMLButtonElement) => { - fileRef.current = currentTarget; - fileRef.current?.classList.add("isSelected"); - }; + const [params, setSearchParams] = useSearchParams(); - const removeSelectedFile = () => { - if (fileRef.current?.classList.contains("isSelected")) { - fileRef.current?.classList.remove("isSelected"); - } - }; + const incrementDownload = useIncrementDownloadMutation(graphqlApp.client); return (
    { SIZE
    - {files.map((meta) => { - return ( - - ); - })} + + const blob = await request.blob(); + + downloadFileWithBlob(blob, meta.mimeType, meta.name); + + incrementDownload.mutate( + { + rootId, + }, + { + onSuccess: onRefetch, + }, + ); + } finally { + setLoading(undefined); + } + }} + /> + )} + + + ))}
    ); }; diff --git a/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileMarkdown.tsx b/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileMarkdown.tsx index b991528..c8e34a0 100644 --- a/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileMarkdown.tsx +++ b/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileMarkdown.tsx @@ -1,9 +1,6 @@ -import Hstack from "app/components/Hstack"; -import Typography from "app/components/Typography"; import { Skeleton } from "app/components/ui/skeleton"; import Vstack from "app/components/Vstack"; import type { ArtifactFile } from "app/services/graphql-app/generated"; -import OpenBookLine from "public/assets/line/open-book.svg"; import Markdown from "app/components/Markdown"; import useGetFileByPatchId from "app/hook/useGetFileByPatchId"; @@ -27,16 +24,8 @@ export default ({ file }: ArtifactFileMarkdownProps) => { if (!data?.length || isError) return null; return ( -
    - - - - {file.name} - - -
    - -
    +
    +
    ); }; diff --git a/frontend/app/layout/Artifact/ArtifactFile/ArtifactFilePreview.tsx b/frontend/app/layout/Artifact/ArtifactFile/ArtifactFilePreview.tsx new file mode 100644 index 0000000..b78e00d --- /dev/null +++ b/frontend/app/layout/Artifact/ArtifactFile/ArtifactFilePreview.tsx @@ -0,0 +1,101 @@ +import type { ArtifactFile } from "app/services/graphql-app/generated"; +import ArtifactFileList from "./ArtifactFileList"; +import { Link } from "react-router"; +import ArtifactFilePDF from "./ArtifactFilePDF"; +import ArtifactFileMarkdown from "./ArtifactFileMarkdown"; +import utilsWalrus from "app/utils/utils.walrus"; +import ArtifactFileSVG from "./ArtifactFileSVG"; +import ArtifactFileCSV from "./ArtifactFileCSV"; +import Flex from "app/components/Flex"; +import { renderSectionFile } from "app/utils"; +import Typography from "app/components/Typography"; +import Stack from "app/components/Stack"; + +interface ArtifactFilePreviewProps { + files: ArtifactFile[]; + rootId: string | undefined; + suiObjectId: string; + select: string; + onRefetch: () => void; +} + +export default ({ + files, + rootId, + suiObjectId, + select, + onRefetch, +}: ArtifactFilePreviewProps) => { + const getSelectFile = files.find((file) => file.name === select); + + return ( + +
    + +
    + +
    + {(function () { + if (!getSelectFile) { + return ( + + 404 - page not found + + + the list file does not contain the path  + + {select} + + + + ); + } + + return ( + <> + + + Root + + + / + + + {getSelectFile.name} + + + + {renderSectionFile(getSelectFile.mimeType, { + csv: , + svg: , + image: ( + {getSelectFile.name} + ), + video: ( +
    +
    + ); +}; diff --git a/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileReadme.tsx b/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileReadme.tsx new file mode 100644 index 0000000..b8f505d --- /dev/null +++ b/frontend/app/layout/Artifact/ArtifactFile/ArtifactFileReadme.tsx @@ -0,0 +1,42 @@ +import Hstack from "app/components/Hstack"; +import Typography from "app/components/Typography"; +import { Skeleton } from "app/components/ui/skeleton"; +import Vstack from "app/components/Vstack"; +import type { ArtifactFile } from "app/services/graphql-app/generated"; +import OpenBookLine from "public/assets/line/open-book.svg"; + +import Markdown from "app/components/Markdown"; +import useGetFileByPatchId from "app/hook/useGetFileByPatchId"; + +interface ArtifactFileReadmeProps { + file: ArtifactFile; +} + +export default ({ file }: ArtifactFileReadmeProps) => { + const { data, isError, isLoading } = useGetFileByPatchId(file, "text"); + + if (isLoading) { + return ( + + + + + ); + } + + if (!data?.length || isError) return null; + + return ( +
    + + + + {file.name} + + +
    + +
    +
    + ); +}; diff --git a/frontend/app/layout/Artifact/index.tsx b/frontend/app/layout/Artifact/index.tsx index bb579ad..c9461ee 100644 --- a/frontend/app/layout/Artifact/index.tsx +++ b/frontend/app/layout/Artifact/index.tsx @@ -18,6 +18,9 @@ import useGetConfig, { contributorConfigEnum } from "app/hook/useGetConfig"; import { useCurrentAccount } from "@mysten/dapp-kit-react"; import { useSearchParams } from "react-router"; import ArtifactRelease from "./ArtifactRelease"; +import { renderSectionFile } from "app/utils"; +import ArtifactFileReadme from "./ArtifactFile/ArtifactFileReadme"; +import ArtifactFilePreview from "./ArtifactFile/ArtifactFilePreview"; const ArtifactFileCSV = lazy(() => import("./ArtifactFile/ArtifactFileCSV")); @@ -38,6 +41,8 @@ export default ({ loaderData, params }: Route.ComponentProps) => { }, ); + const getSelectFile = searchParams?.get?.("file"); + const getSingleFile = artifact.data?.artifact?.files?.[0]; const getREADME = artifact.data?.artifact?.files?.find?.( @@ -68,6 +73,18 @@ export default ({ loaderData, params }: Route.ComponentProps) => { ); } + if (getSelectFile?.length) { + return ( + + ); + } + return ( { {(function () { if (!artifact.data.artifact?.files?.length) return null; - if (artifact.data.artifact.files.length === 1) { - if (getSingleFile?.mimeType === "text/csv") { - return ; - } - - if (getSingleFile?.mimeType === "image/svg+xml") { - return ; - } - - if (getSingleFile?.mimeType?.startsWith?.("image")) { - return ( + if (artifact.data.artifact.files.length === 1 && getSingleFile) { + return renderSectionFile(getSingleFile.mimeType, { + csv: , + svg: , + image: ( {getSingleFile.name} - ); - } - - if (getSingleFile?.mimeType?.startsWith?.("video")) { - return ( + ), + video: (