Skip to content

feat: Add file action button group#118

Merged
fengmk2 merged 2 commits intocnpm:masterfrom
Wxh16144-forks:wuxh/file-actions
Dec 19, 2025
Merged

feat: Add file action button group#118
fengmk2 merged 2 commits intocnpm:masterfrom
Wxh16144-forks:wuxh/file-actions

Conversation

@Wxh16144
Copy link
Copy Markdown
Contributor

@Wxh16144 Wxh16144 commented Dec 19, 2025

背景 or 需求

在 npmmirror 已经开启包白名单的前提下,我理解能在 npmmirror web 的产物预览看到的文件都默许开发者以 cdn 的方式调用。就像这样:
image

但是要得到这个地址是比较繁琐的:

  1. 打开 npmmirror.com
  2. 然后搜索包,
  3. 点击产物预览(通过这个步骤可以知道包是否允许链接的方式使用)
  4. 通过左侧的目录,找到构建产物 如 min.css index.js
  5. 再通过 F12 打开控制台找到地址信息

所以,参考 GitHub 的文件页,将这个功能搬到 npmmirror web
image

效果截图

image

实现细节

因为 header 中组件需要得到文件信息,但是当前代码实现没有统一一个 global store。 所以临时用 zustand 方式,存储在内存中。并且最终通过 <CodeViewer /> 注入这些信息 (CodeViewer 代表用户当前查看到文件,看起来写的比较乱,但是这个可以最小化实现。


另外我在实现的时候发现当前的 dark / light 主题等操作是有点问题的。所以后面再开一个新的 PR 修复。就不和当前放在一起了

Summary by CodeRabbit

  • New Features

    • File action controls added to the code viewer: view raw, copy to clipboard, and download files.
    • File actions are now shown contextually in the tabs header when viewing files.
  • Improvements

    • File content retrieval now exposes the file source URL alongside fetched content.
    • Minor layout adjustments to the version selector and tabs header.
  • Chores

    • Added a state-management dependency to support the new file actions.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 19, 2025 13:10
@vercel
Copy link
Copy Markdown

vercel bot commented Dec 19, 2025

@Wxh16144 is attempting to deploy a commit to the cnpmweb Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a FileActions feature using a zustand store plus UI (Raw/Copy/Download), integrates store updates from CodeViewer, conditionally renders FileActions in CustomTabs, and adjusts useFileContent to also return the resolved file URI.

Changes

Cohort / File(s) Change Summary
Dependency Addition
package.json
Added zustand ^5.0.9 to dependencies.
File Actions Feature
src/components/FileActions.tsx
New component providing Raw, Copy, and Download actions and a zustand store (IState with rawUrl and fileContent). Exports FileActions plus setStore and restoreStore helpers.
Hook Signature Change
src/hooks/useFile.ts
useFileContent now returns a tuple [useSwr(...), { fileUri }] (exposes computed fileUri separately); swr key and fetcher adjusted to use fileUri.
State Integration
src/components/CodeViewer.tsx
Added useEffect to sync fileUri and fetched code into FileActions store via setStore. Destructures useFileContent return to access both SWR data and fileUri.
Conditional Rendering
src/components/CustomTabs.tsx
Tab bar extra content wrapped in a Flex; FileActions is conditionally rendered when active tab is "files" alongside NPMVersionSelect.
Layout Refactor
src/components/NPMVersionSelect.tsx
Removed Space wrapper and ternary; added early guard (if targetOptions.length === 0 return null) and renders controls inside a fragment when options exist.

Sequence Diagram

sequenceDiagram
    participant User
    participant CodeViewer
    participant Store as FileActionsStore
    participant CustomTabs
    participant FileActionsComp as FileActions

    User->>CodeViewer: open file / navigate
    CodeViewer->>CodeViewer: useFileContent fetches code and fileUri
    Note right of CodeViewer: useEffect triggers on code/fileUri
    CodeViewer->>Store: setStore({ rawUrl: fileUri, fileContent: code })
    Store->>CustomTabs: store state available
    CustomTabs->>CustomTabs: activeTab === "files" ?
    CustomTabs->>FileActionsComp: render FileActions (conditional)
    User->>FileActionsComp: click Copy / Download / Raw
    alt Copy
        FileActionsComp->>User: copy fileContent to clipboard
    else Download
        FileActionsComp->>User: create blob, trigger download
    else View Raw
        FileActionsComp->>User: open rawUrl in new tab
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–25 minutes

Areas requiring extra attention:

  • The changed return shape of useFileContent and all consumers expecting the previous API.
  • CodeViewer useEffect dependencies and correctness of setStore payload timing (race with SWR).
  • FileActions export pattern (component with attached setStore/restoreStore) and zustand devtools usage.
  • Blob creation, filename extraction from rawUrl, and URL revocation in handleDownloadRaw.
  • Conditional rendering placement in CustomTabs and potential layout regressions from removing Space in NPMVersionSelect.

Poem

🐰 I found a file with flair,
A raw link, copy, download there.
Zustand stores it neat and sweet,
Click and the actions hop to meet.
Tiny paws, big dev delight 🌿

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add file action button group' directly and specifically describes the main feature introduced in the PR: a new file action button group UI component with Raw, Copy, and Download actions for files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @Wxh16144, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求为 npmmirror 网页界面引入了一项新功能,即在文件预览页面添加了一组文件操作按钮(原始文件、复制、下载)。这一增强功能显著简化了开发人员访问和使用预览包文件 CDN 链接的过程,将多步手动操作转变为直接的一键式操作。实现上利用 zustand 进行高效的状态管理,以便将文件数据传递给新的操作组件。

Highlights

  • 新功能: 在文件预览页面添加了文件操作按钮组(原始文件、复制、下载)。
  • 用户体验改进: 通过提供直接操作,简化了获取文件 CDN 链接的过程,消除了繁琐的手动步骤。
  • 状态管理: 引入 zustand 作为临时全局状态管理方案,用于向操作按钮传递文件信息。
  • 代码查看器集成: CodeViewer 组件现在将文件 URI 和内容注入到 FileActions 存储中。

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Dec 19, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedzustand@​5.0.910010010091100

View full report

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

你好,感谢你对 npmmirror web 的贡献!这个 PR 增加的文件操作按钮组是一个非常实用的功能,极大地改善了用户获取文件 CDN 地址的体验。代码实现上,使用 zustand 来进行跨组件状态共享是一个聪明的临时解决方案。整体代码风格清晰,改动也比较合理。我发现了一些可以改进的地方,主要集中在状态管理的健壮性和边缘情况处理上,例如组件卸载时清理全局状态,以及正确处理空文件的情况。请查看我提出的具体建议,希望能帮助你进一步完善这个功能。

Comment on lines +91 to +96
useEffect(() => {
FileActions.setStore({
rawUrl: fileUri,
fileContent: code
})
}, [fileUri, code])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

这个 useEffect 在组件挂载和更新时向全局 store 注入了文件信息,但缺少了在组件卸载时进行清理的逻辑。这可能会导致在用户离开文件预览页面后,全局 store 中仍然残留着旧的文件数据,造成状态污染。建议添加一个 effect 的清理函数,在组件卸载时调用 FileActions.restoreStore() 来重置状态。

Suggested change
useEffect(() => {
FileActions.setStore({
rawUrl: fileUri,
fileContent: code
})
}, [fileUri, code])
useEffect(() => {
FileActions.setStore({
rawUrl: fileUri,
fileContent: code
})
return () => {
FileActions.restoreStore();
}
}, [fileUri, code])

}

// ========== render =========
if (Object.values(state).some(isEmpty)) return null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

当前的渲染条件 Object.values(state).some(isEmpty) 存在一个问题:lodashisEmpty('') 会返回 true。这意味着当一个文件内容为空字符串时,fileContent'',此条件会判断为真,导致操作按钮组不被渲染。用户应该能够对空文件进行“查看原文”或“下载”等操作。建议修改此条件,仅在数据真正加载中(例如 fileContentundefined)时才隐藏按钮。

Suggested change
if (Object.values(state).some(isEmpty)) return null
if (!state.rawUrl || state.fileContent === undefined) return null

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我知道,故意的,别管

Comment on lines +75 to +78
export const FileActions = Object.assign(_FileActions, {
setStore,
restoreStore,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

通过 Object.assign 将 store 的 action (setStore, restoreStore) 作为静态属性附加到 React 组件上的模式不太常见。这种做法将状态管理逻辑和 UI 组件耦合在了一起,可能会降低代码的可维护性,也不符合社区中常见的 React 和 Zustand 的使用模式。为了更好的关注点分离,建议将 Zustand store 及其相关的 actions 移至一个单独的文件中(例如 src/store/fileActions.ts),或者通过自定义 hook 导出。考虑到你提到这是临时方案,这可以作为一个未来的改进方向。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

故意的,否则别人不知道这个 setStore 是哪里来的

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a file action button group (Raw, Copy, Download) to the package file viewer, inspired by GitHub's file interface. The implementation uses Zustand for state management to share file information between components, making it easier for users to access file URLs without navigating through browser DevTools.

Key changes:

  • New FileActions component with three action buttons for viewing raw files, copying content, and downloading files
  • Zustand state management integration to pass file information from CodeViewer to FileActions
  • Modified useFileContent hook to return both SWR data and file URI

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/components/FileActions.tsx New component implementing Raw/Copy/Download buttons with Zustand store for state management
src/components/CodeViewer.tsx Injects file content and URI into FileActions store via useEffect hook
src/components/CustomTabs.tsx Integrates FileActions component into the tabs' extra content area for the 'files' tab
src/hooks/useFile.ts Modified to return file URI alongside SWR data for use by FileActions component
src/components/NPMVersionSelect.tsx Minor formatting changes removing Space wrapper and adjusting returns
package.json Adds zustand@^5.0.9 dependency for state management

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

</Space>
) : null;
</>
)
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
})
}, [fileUri, code])


Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line should be removed for consistency with the rest of the codebase formatting.

Suggested change

Copilot uses AI. Check for mistakes.

function handleDownloadRaw() {
if (!state.fileContent) return;
const blob = new Blob([state.fileContent], { type: 'text/plain' });
Copy link

Copilot AI Dec 19, 2025

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 uses AI. Check for mistakes.
Comment on lines +92 to +98
FileActions.setStore({
rawUrl: fileUri,
fileContent: code
})
}, [fileUri, code])


Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The store update in the useEffect hook will cause unnecessary updates even when the component is not viewing a file. When a file is deselected (selectedFile becomes undefined), the store still retains the old file information. Consider calling FileActions.restoreStore() in a cleanup function to reset the state when the component unmounts or when there's no selected file.

Suggested change
FileActions.setStore({
rawUrl: fileUri,
fileContent: code
})
}, [fileUri, code])
if (!selectedFile) {
FileActions.restoreStore();
return;
}
FileActions.setStore({
rawUrl: fileUri,
fileContent: code,
});
return () => {
FileActions.restoreStore();
};
}, [fileUri, code, selectedFile]);

Copilot uses AI. Check for mistakes.
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = state.rawUrl.split('/').pop() || 'file';
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +38
function handleCopyRaw() {
if (!state.fileContent) return;
navigator.clipboard.writeText(state.fileContent);
message.success('Copied to clipboard');
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clipboard write operation using navigator.clipboard.writeText() may fail if the user hasn't granted clipboard permissions or if the page is not in a secure context. Add error handling to catch potential failures and inform the user appropriately.

Suggested change
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 uses AI. Check for mistakes.
}

// ========== render =========
if (Object.values(state).some(isEmpty)) return null
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
return !isEmpty(targetOptions) ? (
<Space style={{ paddingRight: 32 }}>
// ========== render ==========
if (isEmpty(targetOptions)) return null
Copy link

Copilot AI Dec 19, 2025

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. While JavaScript has automatic semicolon insertion, it's best practice to maintain consistency with the rest of the codebase which uses semicolons.

Suggested change
if (isEmpty(targetOptions)) return null
if (isEmpty(targetOptions)) return null;

Copilot uses AI. Check for mistakes.
@@ -1,9 +1,10 @@
'use client';
import { Tabs } from 'antd';
import { Flex, Space, Tabs } from 'antd';
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import Space.

Suggested change
import { Flex, Space, Tabs } from 'antd';
import { Flex, Tabs } from 'antd';

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +96
})
}, [fileUri, code])
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid automated semicolon insertion (91% of all statements in the enclosing function have an explicit semicolon).

Suggested change
})
}, [fileUri, code])
});
}, [fileUri, code]);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/NPMVersionSelect.tsx (1)

136-141: Remove duplicate isEmpty check.

Line 141 duplicates the check already performed on line 136. Since line 136 returns early when isEmpty(targetOptions) is true, the check on line 141 is unreachable dead code.

🔎 Proposed fix
 if (isEmpty(targetOptions) || !targetVersion) {
   return null;
 }

 // ========== render ==========
-if (isEmpty(targetOptions)) return null

 return (
🧹 Nitpick comments (2)
src/components/FileActions.tsx (2)

18-26: Reorder store helpers after store creation for clarity.

The setStore and restoreStore functions reference useFileActionsStore before it's defined (line 24). While this works due to function hoisting in JavaScript, it reduces readability and may confuse maintainers.

🔎 Suggested reordering
-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' })
 );
+
+const setStore = (state: Partial<IState>) =>
+  useFileActionsStore.setState(state, false, 'FileActions/setStore')
+
+const restoreStore = () =>
+  useFileActionsStore.setState(initialState, false, 'FileActions/restoreStore')

41-52: Consider using URL parsing for safer filename extraction.

The download handler uses .split('/').pop() to extract the filename (line 47). While this works for typical URLs and has a fallback, using the URL API would be more robust for edge cases like URLs with query parameters or fragments.

🔎 Alternative implementation
 function handleDownloadRaw() {
   if (!state.fileContent) return;
   const blob = new Blob([state.fileContent], { type: 'text/plain' });
   const url = window.URL.createObjectURL(blob);
   const link = document.createElement('a');
   link.href = url;
-  link.download = state.rawUrl.split('/').pop() || 'file';
+  try {
+    const urlObj = new URL(state.rawUrl);
+    link.download = urlObj.pathname.split('/').pop() || 'file';
+  } catch {
+    link.download = state.rawUrl.split('/').pop() || 'file';
+  }
   document.body.appendChild(link);
   link.click();
   document.body.removeChild(link);
   window.URL.revokeObjectURL(url);
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 645e531 and ecde016.

📒 Files selected for processing (6)
  • package.json (1 hunks)
  • src/components/CodeViewer.tsx (4 hunks)
  • src/components/CustomTabs.tsx (2 hunks)
  • src/components/FileActions.tsx (1 hunks)
  • src/components/NPMVersionSelect.tsx (2 hunks)
  • src/hooks/useFile.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/hooks/useFile.ts (1)
src/config.ts (1)
  • REGISTRY (1-1)
src/components/CodeViewer.tsx (2)
src/hooks/useFile.ts (1)
  • useFileContent (65-73)
src/components/FileActions.tsx (1)
  • FileActions (75-78)
src/components/CustomTabs.tsx (2)
src/components/FileActions.tsx (1)
  • FileActions (75-78)
src/components/NPMVersionSelect.tsx (1)
  • NPMVersionSelect (89-171)
🪛 GitHub Actions: CI
src/components/FileActions.tsx

[error] 33-33: React Hook "useFileActionsStore" is called in function "_FileActions" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🔇 Additional comments (6)
src/components/FileActions.tsx (1)

60-72: LGTM! Clean guard and render logic.

The early return when store values are empty (line 60) prevents rendering incomplete UI. The action buttons with tooltips provide good UX for copying, downloading, and viewing raw files.

src/components/CodeViewer.tsx (1)

73-73: LGTM! Correct usage of updated useFileContent API.

The destructuring correctly handles the new tuple return type from useFileContent, extracting both the code content and fileUri.

src/components/CustomTabs.tsx (1)

48-64: LGTM! Clean integration of FileActions.

The conditional rendering of FileActions (line 49) correctly limits it to the "files" tab, and the Flex layout provides a clean horizontal arrangement with NPMVersionSelect. The implementation aligns well with the PR objectives.

src/components/NPMVersionSelect.tsx (1)

143-170: LGTM! Clean layout refactor.

The change from Space wrapper to fragment is appropriate since the layout responsibility has moved to the parent component (CustomTabs uses Flex). The rendering logic remains functionally equivalent.

package.json (1)

37-37: No changes needed. Zustand 5.0.9 is the latest version and was published 19 days ago. The dependency is current with no known security advisories.

src/hooks/useFile.ts (1)

66-72: The refactor is properly implemented with all call sites updated.

The separation of the SWR response and fileUri into a clean tuple is an idiomatic pattern that provides consumers access to the raw file URL. The single call site in CodeViewer.tsx correctly destructures the tuple: const [{ data: code }, { fileUri }] = useFileContent(...), confirming the breaking API change has been properly handled throughout the codebase.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/NPMVersionSelect.tsx (1)

136-141: Remove duplicate early return check.

Lines 136-138 already check isEmpty(targetOptions) and return null if true. The additional check at line 141 is unreachable dead code and should be removed.

🔎 Proposed fix
  if (isEmpty(targetOptions) || !targetVersion) {
    return null;
  }

- // ========== render ==========
- if (isEmpty(targetOptions)) return null
-
  return (
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecde016 and 05cc153.

📒 Files selected for processing (6)
  • package.json (1 hunks)
  • src/components/CodeViewer.tsx (4 hunks)
  • src/components/CustomTabs.tsx (2 hunks)
  • src/components/FileActions.tsx (1 hunks)
  • src/components/NPMVersionSelect.tsx (2 hunks)
  • src/hooks/useFile.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/FileActions.tsx
  • src/components/CodeViewer.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/hooks/useFile.ts (1)
src/config.ts (1)
  • REGISTRY (1-1)
src/components/CustomTabs.tsx (2)
src/components/FileActions.tsx (1)
  • FileActions (76-79)
src/components/NPMVersionSelect.tsx (1)
  • NPMVersionSelect (89-171)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Node.js / Test (ubuntu-latest, 24)
  • GitHub Check: Node.js / Test (ubuntu-latest, 22)
🔇 Additional comments (4)
src/components/NPMVersionSelect.tsx (1)

143-170: Refactoring looks good.

The change from Space wrapper to a React fragment is appropriate since the parent component (CustomTabs.tsx) now handles the layout with a Flex container.

src/components/CustomTabs.tsx (1)

48-64: Layout integration looks good.

The Flex wrapper with appropriate gap and conditional rendering of FileActions when activateKey === 'files' aligns well with the PR objectives to add file action buttons.

package.json (1)

37-37: Zustand package version is valid and secure.

Version ^5.0.9 is the current latest stable release with no known vulnerabilities.

src/hooks/useFile.ts (1)

65-72: All call sites properly updated for the tuple return type.

The function signature change is correctly implemented in the single call site found at src/components/CodeViewer.tsx:73, where the destructuring pattern const [{ data: code }, { fileUri }] = useFileContent(...) properly handles the new tuple return type [SWRResponse, { fileUri }].

@@ -1,9 +1,10 @@
'use client';
import { Tabs } from 'antd';
import { Flex, Space, Tabs } from 'antd';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.tsx

Repository: 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.tsx

Repository: 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.

@fengmk2 fengmk2 merged commit 2937035 into cnpm:master Dec 19, 2025
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants