-
Notifications
You must be signed in to change notification settings - Fork 29
feat: Add file action button group #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -4,8 +4,9 @@ import loader from '@monaco-editor/loader'; | |||
| import { File, useFileContent } from '@/hooks/useFile'; | ||||
| import useHighlightHash, { parseHash } from '@/hooks/useHighlightHash'; | ||||
| import { useThemeMode } from 'antd-style'; | ||||
| import { useRef } from 'react'; | ||||
| import { useEffect, useRef } from 'react'; | ||||
| import { REGISTRY } from '@/config'; | ||||
| import { FileActions } from './FileActions'; | ||||
|
|
||||
| const LinkField = ['devDependencies', 'dependencies', 'peerDependencies', 'optionalDependencies', 'bundleDependencies']; | ||||
|
|
||||
|
|
@@ -25,11 +26,11 @@ function highlightEditor(editor: any) { | |||
| function registerLinkProvider(monaco: any) { | ||||
| monaco.languages.registerLinkProvider('json', { | ||||
| provideLinks: (model: any) => { | ||||
| const links:any= []; | ||||
| const links: any = []; | ||||
| const lines = model.getLinesContent(); | ||||
| let startCatch = false; | ||||
| lines.forEach((line: string, lineIndex: number) => { | ||||
| if (LinkField.some( _ => line === ` "${_}": {`)) { | ||||
| if (LinkField.some(_ => line === ` "${_}": {`)) { | ||||
| startCatch = true; | ||||
| return; | ||||
| } | ||||
|
|
@@ -69,7 +70,7 @@ export const CodeViewer = ({ | |||
| const editorRef = useRef<any>(null); | ||||
| const { themeMode: theme } = useThemeMode(); | ||||
|
|
||||
| const { data: code } = useFileContent({ fullname: pkgName, spec }, selectedFile?.path || ''); | ||||
| const [{ data: code }, { fileUri }] = useFileContent({ fullname: pkgName, spec }, selectedFile?.path || ''); | ||||
|
|
||||
| let language = selectedFile?.path.split('.').pop(); | ||||
| if (language === 'js' || language === 'jsx' || language === 'map') language = 'javascript'; | ||||
|
|
@@ -86,6 +87,19 @@ export const CodeViewer = ({ | |||
| } | ||||
| }; | ||||
|
|
||||
| // inject file info to FileActions store | ||||
| useEffect(() => { | ||||
| FileActions.setStore({ | ||||
| rawUrl: fileUri, | ||||
| fileContent: code | ||||
| }); | ||||
|
|
||||
| return function cleanup() { | ||||
| FileActions.restoreStore(); | ||||
| } | ||||
| }, [fileUri, code]) | ||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
|
|
||||
|
|
||||
|
||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,9 +1,10 @@ | ||||||
| 'use client'; | ||||||
| import { Tabs } from 'antd'; | ||||||
| import { Flex, Space, Tabs } from 'antd'; | ||||||
|
||||||
| import { Flex, Space, Tabs } from 'antd'; | |
| import { Flex, Tabs } from 'antd'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if Space from antd is used in CustomTabs.tsx or NPMVersionSelect.tsx
echo "=== Checking Space usage in CustomTabs.tsx ==="
rg -n "Space" src/components/CustomTabs.tsx
echo -e "\n=== Checking Space usage in NPMVersionSelect.tsx ==="
rg -n "Space" src/components/NPMVersionSelect.tsxRepository: cnpm/cnpmweb
Length of output: 290
🏁 Script executed:
#!/bin/bash
# View CustomTabs.tsx to see if Space or NPMVersionSelect is used in the component
echo "=== CustomTabs.tsx full content ==="
cat -n src/components/CustomTabs.tsx
echo -e "\n=== Check for JSX usage of Space or NPMVersionSelect ==="
rg -n "<Space|<NPMVersionSelect" src/components/CustomTabs.tsxRepository: cnpm/cnpmweb
Length of output: 2852
Remove unused Space import from line 2.
The Space component from 'antd' is imported in CustomTabs.tsx but never used in the component. While NPMVersionSelect.tsx does use Space internally, each component should manage its own imports—parent components don't need to import what child components use.
import { Flex, Space, Tabs } from 'antd';
Change to:
import { Flex, Tabs } from 'antd';
🤖 Prompt for AI Agents
In src/components/CustomTabs.tsx around line 2, the import statement includes an
unused Space import from 'antd'; remove Space from the import list so it only
imports the symbols actually used (e.g., Flex and Tabs) to eliminate the unused
import and satisfy linting.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,80 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { CopyOutlined, DownloadOutlined } from "@ant-design/icons"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { Button, Space, Tooltip, message } from "antd"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { isEmpty } from "lodash"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { create } from 'zustand' | ||||||||||||||||||||||||||||||||||||||||||||
| import { devtools } from 'zustand/middleware' | ||||||||||||||||||||||||||||||||||||||||||||
| import { useShallow } from 'zustand/react/shallow' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // #region Store | ||||||||||||||||||||||||||||||||||||||||||||
| interface IState { | ||||||||||||||||||||||||||||||||||||||||||||
| rawUrl: string; | ||||||||||||||||||||||||||||||||||||||||||||
| fileContent: string; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const initialState: IState = { | ||||||||||||||||||||||||||||||||||||||||||||
| rawUrl: '', | ||||||||||||||||||||||||||||||||||||||||||||
| fileContent: '', | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
Wxh16144 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const setStore = (state: Partial<IState>) => | ||||||||||||||||||||||||||||||||||||||||||||
| useFileActionsStore.setState(state, false, 'FileActions/setStore') | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const restoreStore = () => | ||||||||||||||||||||||||||||||||||||||||||||
| useFileActionsStore.setState(initialState, false, 'FileActions/restoreStore') | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const useFileActionsStore = create<IState>()( | ||||||||||||||||||||||||||||||||||||||||||||
| devtools(() => initialState, { name: 'FileActions' }) | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| // #endregion Store | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // =========================================================================== | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // #region Component | ||||||||||||||||||||||||||||||||||||||||||||
| function InnerFileActions() { | ||||||||||||||||||||||||||||||||||||||||||||
| const state = useFileActionsStore(useShallow(s => s)); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| function handleCopyRaw() { | ||||||||||||||||||||||||||||||||||||||||||||
| if (!state.fileContent) return; | ||||||||||||||||||||||||||||||||||||||||||||
| navigator.clipboard.writeText(state.fileContent); | ||||||||||||||||||||||||||||||||||||||||||||
| message.success('Copied to clipboard'); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+39
|
||||||||||||||||||||||||||||||||||||||||||||
| function handleCopyRaw() { | |
| if (!state.fileContent) return; | |
| navigator.clipboard.writeText(state.fileContent); | |
| message.success('Copied to clipboard'); | |
| async function handleCopyRaw() { | |
| if (!state.fileContent) return; | |
| if (!navigator || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') { | |
| message.error('Clipboard is not available in this browser or context.'); | |
| return; | |
| } | |
| try { | |
| await navigator.clipboard.writeText(state.fileContent); | |
| message.success('Copied to clipboard'); | |
| } catch (error) { | |
| // Clipboard write can fail if permissions are denied or in insecure contexts | |
| // eslint-disable-next-line no-console | |
| console.error('Failed to write to clipboard:', error); | |
| message.error('Failed to copy to clipboard. Please check your browser permissions or try again.'); | |
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MIME type is hardcoded as 'text/plain' for all files. This may cause issues for binary files or files with specific content types (e.g., JSON, CSS, JavaScript). Consider detecting the appropriate MIME type based on the file extension or using a more generic type like 'application/octet-stream' for downloads.
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The filename extraction logic using split('/').pop() will fail for URLs that don't end with a filename (e.g., if the URL ends with a slash). Additionally, if the path contains query parameters or fragments, they will be included in the filename. Consider extracting the filename from the full path including handling edge cases like trailing slashes and URL parameters.
| link.download = state.rawUrl.split('/').pop() || 'file'; | |
| let filename = 'file'; | |
| try { | |
| const parsedUrl = new URL(state.rawUrl, window.location.href); | |
| const segments = parsedUrl.pathname.split('/').filter(Boolean); | |
| const lastSegment = segments[segments.length - 1]; | |
| if (lastSegment) { | |
| filename = decodeURIComponent(lastSegment); | |
| } | |
| } catch { | |
| // If URL parsing fails, keep the default filename | |
| } | |
| link.download = filename; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
当前的渲染条件 Object.values(state).some(isEmpty) 存在一个问题:lodash 的 isEmpty('') 会返回 true。这意味着当一个文件内容为空字符串时,fileContent 为 '',此条件会判断为真,导致操作按钮组不被渲染。用户应该能够对空文件进行“查看原文”或“下载”等操作。建议修改此条件,仅在数据真正加载中(例如 fileContent 为 undefined)时才隐藏按钮。
| if (Object.values(state).some(isEmpty)) return null | |
| if (!state.rawUrl || state.fileContent === undefined) return null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
我知道,故意的,别管
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The visibility check using Object.values(state).some(isEmpty) is fragile and could lead to unexpected behavior. If any state value is legitimately an empty string (like an empty file), the component would hide. Consider explicitly checking for null/undefined states or using a more specific condition like !state.rawUrl || state.fileContent === undefined.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -137,9 +137,11 @@ export default function NPMVersionSelect({ | |||||
| return null; | ||||||
| } | ||||||
|
|
||||||
| // 版本选择器 | ||||||
| return !isEmpty(targetOptions) ? ( | ||||||
| <Space style={{ paddingRight: 32 }}> | ||||||
| // ========== render ========== | ||||||
| if (isEmpty(targetOptions)) return null | ||||||
|
||||||
| if (isEmpty(targetOptions)) return null | |
| if (isEmpty(targetOptions)) return null; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing semicolon at the end of the return statement. For consistency with the rest of the codebase, add a semicolon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个
useEffect在组件挂载和更新时向全局 store 注入了文件信息,但缺少了在组件卸载时进行清理的逻辑。这可能会导致在用户离开文件预览页面后,全局 store 中仍然残留着旧的文件数据,造成状态污染。建议添加一个 effect 的清理函数,在组件卸载时调用FileActions.restoreStore()来重置状态。