-
Notifications
You must be signed in to change notification settings - Fork 78
feat(FR-1014): implement text file editor for NEO File Explorer #4943
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
Conversation
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
|
1 similar comment
|
|
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has required the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Coverage report for
|
St.❔ |
Category | Percentage | Covered / Total |
|---|---|---|---|
| 🟡 | Statements | 63.5% | 254/400 |
| 🔴 | Branches | 42.3% | 151/357 |
| 🔴 | Functions | 50% | 46/92 |
| 🟡 | Lines | 65.16% | 230/353 |
Show new covered files 🐣
St.❔ |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| 🔴 | ... / BAIDomainSelector.tsx |
50% | 0% | 0% | 50% |
Test suite run success
186 tests passing in 9 suites.
Report generated by 🧪jest coverage report action from 75a47b8
Coverage report for
|
St.❔ |
Category | Percentage | Covered / Total |
|---|---|---|---|
| 🔴 | Statements | 1.13% (-3.78% 🔻) |
136/12039 |
| 🔴 | Branches | 1.22% (-3.26% 🔻) |
103/8464 |
| 🔴 | Functions | 0.92% (-1.82% 🔻) |
34/3685 |
| 🔴 | Lines | 1.14% (-3.6% 🔻) |
134/11756 |
Show new covered files 🐣
St.❔ |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| 🔴 | ... / KeypairResourcePolicySelector.tsx |
0% | 100% | 0% | 0% |
| 🔴 | ... / TextFileEditorModal.tsx |
0% | 0% | 0% | 0% |
| 🔴 | ... / UserResourcePolicySelector.tsx |
0% | 100% | 0% | 0% |
| 🔴 | ... / UserSelector.tsx |
0% | 0% | 0% | 0% |
Show files with reduced coverage 🔻
St.❔ |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| 🔴 | ... / big-number.ts |
0% (-85.71% 🔻) |
0% (-75.32% 🔻) |
0% (-91.67% 🔻) |
0% (-85.71% 🔻) |
| 🔴 | ... / const-vars.ts |
0% (-100% 🔻) |
100% | 100% | 0% (-100% 🔻) |
| 🔴 | hooks/backendai.tsx | 0% (-11.67% 🔻) |
0% | 0% | 0% (-12.07% 🔻) |
| 🔴 | ... / hooksUsingRelay.tsx |
0% (-14.29% 🔻) |
0% | 0% | 0% (-14.29% 🔻) |
| 🔴 | ... / reactPaginationQueryOptions.tsx |
0% (-10.13% 🔻) |
0% | 0% | 0% (-11.11% 🔻) |
| 🔴 | hooks/index.tsx | 0% (-51.68% 🔻) |
0% (-36.59% 🔻) |
0% (-33.33% 🔻) |
0% (-51.37% 🔻) |
| 🔴 | helper/index.tsx | 0% (-61.46% 🔻) |
0% (-57.71% 🔻) |
0% (-38.6% 🔻) |
0% (-61.32% 🔻) |
| 🔴 | ... / reactQueryAlias.tsx |
0% (-45% 🔻) |
0% (-37.5% 🔻) |
0% (-33.33% 🔻) |
0% (-45% 🔻) |
| 🔴 | ... / useCurrentProject.tsx |
0% (-22.22% 🔻) |
0% | 0% | 0% (-22.22% 🔻) |
| 🔴 | ... / useResourceLimitAndRemaining.tsx |
0% (-15.52% 🔻) |
0% (-4.23% 🔻) |
0% (-5.26% 🔻) |
0% (-16.07% 🔻) |
| 🔴 | ... / AgentSelectQuery.graphql.ts |
0% (-100% 🔻) |
100% | 0% (-100% 🔻) |
0% (-100% 🔻) |
| 🔴 | ... / ResourceAllocationFormItemsQuery.graphql.ts |
0% (-100% 🔻) |
100% | 0% (-100% 🔻) |
0% (-100% 🔻) |
| 🔴 | ... / ResourcePresetSelectQuery.graphql.ts |
0% (-100% 🔻) |
100% | 0% (-100% 🔻) |
0% (-100% 🔻) |
| 🔴 | ... / hooksUsingRelay_KeyPairQuery.graphql.ts |
0% (-100% 🔻) |
100% | 0% (-100% 🔻) |
0% (-100% 🔻) |
| 🔴 | ... / hooksUsingRelay_KeyPairResourcePolicyQuery.graphql.ts |
0% (-100% 🔻) |
100% | 0% (-100% 🔻) |
0% (-100% 🔻) |
| 🔴 | ... / useResourceLimitAndRemainingFragment.graphql.ts |
0% (-100% 🔻) |
100% | 100% | 0% (-100% 🔻) |
| 🔴 | ... / AgentSelect.tsx |
0% (-2.56% 🔻) |
0% | 0% | 0% (-2.63% 🔻) |
| 🔴 | ... / EnvVarFormList.tsx |
0% (-14.55% 🔻) |
0% (-6.9% 🔻) |
0% (-13.33% 🔻) |
0% (-13.46% 🔻) |
| 🔴 | ... / InputNumberWithSlider.tsx |
0% (-4.55% 🔻) |
0% | 0% | 0% (-4.55% 🔻) |
| 🔴 | ... / QuestionIconWithTooltip.tsx |
0% (-33.33% 🔻) |
100% | 0% | 0% (-33.33% 🔻) |
| 🔴 | ... / ResourcePresetSelect.tsx |
0% (-4% 🔻) |
0% | 0% | 0% (-4% 🔻) |
| 🔴 | ... / SharedMemoryFormItems.tsx |
0% (-5% 🔻) |
0% | 0% | 0% (-5% 🔻) |
| 🔴 | ... / ResourceAllocationFormItems.tsx |
0% (-13.31% 🔻) |
0% (-9.01% 🔻) |
0% (-11.84% 🔻) |
0% (-13.06% 🔻) |
Test suite run failed
Failed tests: 0/56. Failed suites: 8/13.
● Test suite failed to run
Cannot find module '../../__generated__/BAIDomainSelectorQuery.graphql' from '../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx'
Require stack:
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/index.ts
src/hooks/index.tsx
src/helper/index.tsx
src/helper/index.test.tsx
5 | import React from 'react';
6 | import { useTranslation } from 'react-i18next';
> 7 | import { graphql, useLazyLoadQuery } from 'react-relay';
| ^
8 |
9 | interface Props extends SelectProps {
10 | activeOnly?: boolean;
at Resolver._throwModNotFoundError (../node_modules/.pnpm/[email protected]/node_modules/jest-resolve/build/index.js:863:11)
at Object.<anonymous> (../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx:7:55)
at Object.require (../packages/backend.ai-ui/src/components/fragments/index.ts:58:1)
at Object.require (../packages/backend.ai-ui/src/components/index.ts:86:1)
at Object.require (../packages/backend.ai-ui/src/index.ts:3:1)
at Object.require (src/hooks/index.tsx:4:1)
at Object.require (src/helper/index.tsx:4:1)
at Object.require (src/helper/index.test.tsx:1:1)
● Test suite failed to run
Cannot find module '../../__generated__/BAIDomainSelectorQuery.graphql' from '../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx'
Require stack:
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/index.ts
src/hooks/index.tsx
src/helper/index.tsx
src/helper/big-number.ts
src/helper/big-number.test.ts
5 | import React from 'react';
6 | import { useTranslation } from 'react-i18next';
> 7 | import { graphql, useLazyLoadQuery } from 'react-relay';
| ^
8 |
9 | interface Props extends SelectProps {
10 | activeOnly?: boolean;
at Resolver._throwModNotFoundError (../node_modules/.pnpm/[email protected]/node_modules/jest-resolve/build/index.js:863:11)
at Object.<anonymous> (../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx:7:55)
at Object.require (../packages/backend.ai-ui/src/components/fragments/index.ts:58:1)
at Object.require (../packages/backend.ai-ui/src/components/index.ts:86:1)
at Object.require (../packages/backend.ai-ui/src/index.ts:3:1)
at Object.require (src/hooks/index.tsx:4:1)
at Object.require (src/helper/index.tsx:4:1)
at Object.require (src/helper/big-number.ts:1:1)
at Object.require (src/helper/big-number.test.ts:1:1)
● Test suite failed to run
Cannot find module '../../__generated__/BAIDomainSelectorQuery.graphql' from '../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx'
Require stack:
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/index.ts
src/hooks/index.tsx
src/hooks/useBackendAIImageMetaData.test.tsx
5 | import React from 'react';
6 | import { useTranslation } from 'react-i18next';
> 7 | import { graphql, useLazyLoadQuery } from 'react-relay';
| ^
8 |
9 | interface Props extends SelectProps {
10 | activeOnly?: boolean;
at Resolver._throwModNotFoundError (../node_modules/.pnpm/[email protected]/node_modules/jest-resolve/build/index.js:863:11)
at Object.<anonymous> (../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx:7:55)
at Object.require (../packages/backend.ai-ui/src/components/fragments/index.ts:58:1)
at Object.require (../packages/backend.ai-ui/src/components/index.ts:86:1)
at Object.require (../packages/backend.ai-ui/src/index.ts:3:1)
at Object.require (src/hooks/index.tsx:4:1)
at Object.require (src/hooks/useBackendAIImageMetaData.test.tsx:1:1)
● Test suite failed to run
Cannot find module '../../__generated__/BAIDomainSelectorQuery.graphql' from '../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx'
Require stack:
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/index.ts
src/hooks/index.tsx
src/hooks/index.test.tsx
5 | import React from 'react';
6 | import { useTranslation } from 'react-i18next';
> 7 | import { graphql, useLazyLoadQuery } from 'react-relay';
| ^
8 |
9 | interface Props extends SelectProps {
10 | activeOnly?: boolean;
at Resolver._throwModNotFoundError (../node_modules/.pnpm/[email protected]/node_modules/jest-resolve/build/index.js:863:11)
at Object.<anonymous> (../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx:7:55)
at Object.require (../packages/backend.ai-ui/src/components/fragments/index.ts:58:1)
at Object.require (../packages/backend.ai-ui/src/components/index.ts:86:1)
at Object.require (../packages/backend.ai-ui/src/index.ts:3:1)
at Object.require (src/hooks/index.tsx:4:1)
at Object.require (src/hooks/index.test.tsx:1:1)
● Test suite failed to run
Cannot find module '../../__generated__/BAIDomainSelectorQuery.graphql' from '../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx'
Require stack:
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/index.ts
src/hooks/index.tsx
src/helper/index.tsx
src/components/SessionFormItems/ResourceAllocationFormItems.tsx
src/components/SessionFormItems/ResourceAllocationFormItems.test.ts
5 | import React from 'react';
6 | import { useTranslation } from 'react-i18next';
> 7 | import { graphql, useLazyLoadQuery } from 'react-relay';
| ^
8 |
9 | interface Props extends SelectProps {
10 | activeOnly?: boolean;
at Resolver._throwModNotFoundError (../node_modules/.pnpm/[email protected]/node_modules/jest-resolve/build/index.js:863:11)
at Object.<anonymous> (../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx:7:55)
at Object.require (../packages/backend.ai-ui/src/components/fragments/index.ts:58:1)
at Object.require (../packages/backend.ai-ui/src/components/index.ts:86:1)
at Object.require (../packages/backend.ai-ui/src/index.ts:3:1)
at Object.require (src/hooks/index.tsx:4:1)
at Object.require (src/helper/index.tsx:4:1)
at Object.require (src/components/SessionFormItems/ResourceAllocationFormItems.tsx:2:1)
at Object.require (src/components/SessionFormItems/ResourceAllocationFormItems.test.ts:7:1)
● Test suite failed to run
Cannot find module '../../__generated__/BAIDomainSelectorQuery.graphql' from '../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx'
Require stack:
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/index.ts
src/hooks/index.tsx
src/hooks/useResourceLimitAndRemaining.tsx
src/hooks/useResourceLimitAndRemaining.test.ts
5 | import React from 'react';
6 | import { useTranslation } from 'react-i18next';
> 7 | import { graphql, useLazyLoadQuery } from 'react-relay';
| ^
8 |
9 | interface Props extends SelectProps {
10 | activeOnly?: boolean;
at Resolver._throwModNotFoundError (../node_modules/.pnpm/[email protected]/node_modules/jest-resolve/build/index.js:863:11)
at Object.<anonymous> (../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx:7:55)
at Object.require (../packages/backend.ai-ui/src/components/fragments/index.ts:58:1)
at Object.require (../packages/backend.ai-ui/src/components/index.ts:86:1)
at Object.require (../packages/backend.ai-ui/src/index.ts:3:1)
at Object.require (src/hooks/index.tsx:4:1)
at Object.require (src/hooks/useResourceLimitAndRemaining.tsx:1:1)
at Object.require (src/hooks/useResourceLimitAndRemaining.test.ts:2:1)
● Test suite failed to run
Cannot find module '../../__generated__/BAIDomainSelectorQuery.graphql' from '../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx'
Require stack:
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/index.ts
src/components/EnvVarFormList.tsx
src/components/EnvVarFormList.test.tsx
5 | import React from 'react';
6 | import { useTranslation } from 'react-i18next';
> 7 | import { graphql, useLazyLoadQuery } from 'react-relay';
| ^
8 |
9 | interface Props extends SelectProps {
10 | activeOnly?: boolean;
at Resolver._throwModNotFoundError (../node_modules/.pnpm/[email protected]/node_modules/jest-resolve/build/index.js:863:11)
at Object.<anonymous> (../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx:7:55)
at Object.require (../packages/backend.ai-ui/src/components/fragments/index.ts:58:1)
at Object.require (../packages/backend.ai-ui/src/components/index.ts:86:1)
at Object.require (../packages/backend.ai-ui/src/index.ts:3:1)
at Object.require (src/components/EnvVarFormList.tsx:11:1)
at Object.require (src/components/EnvVarFormList.test.tsx:3:1)
● Test suite failed to run
Cannot find module '../../__generated__/BAIDomainSelectorQuery.graphql' from '../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx'
Require stack:
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/fragments/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/components/index.ts
/home/runner/work/backend.ai-webui/backend.ai-webui/packages/backend.ai-ui/src/index.ts
src/hooks/index.tsx
src/hooks/backendai.tsx
src/hooks/backendai.test.tsx
5 | import React from 'react';
6 | import { useTranslation } from 'react-i18next';
> 7 | import { graphql, useLazyLoadQuery } from 'react-relay';
| ^
8 |
9 | interface Props extends SelectProps {
10 | activeOnly?: boolean;
at Resolver._throwModNotFoundError (../node_modules/.pnpm/[email protected]/node_modules/jest-resolve/build/index.js:863:11)
at Object.<anonymous> (../packages/backend.ai-ui/src/components/fragments/BAIDomainSelector.tsx:7:55)
at Object.require (../packages/backend.ai-ui/src/components/fragments/index.ts:58:1)
at Object.require (../packages/backend.ai-ui/src/components/index.ts:86:1)
at Object.require (../packages/backend.ai-ui/src/index.ts:3:1)
at Object.require (src/hooks/index.tsx:4:1)
at Object.require (src/hooks/backendai.tsx:1:1)
at Object.require (src/hooks/backendai.test.tsx:5:1)
Report generated by 🧪jest coverage report action from 75a47b8
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.
Pull request overview
This PR implements a text file editor for the NEO File Explorer, allowing users to directly edit configuration files (YAML, JSON, TOML, etc.) within vfolders without the download-edit-upload workflow. The implementation adds a new TextFileEditorModal component with CodeMirror integration and extends FileItemControls with a More menu containing an Edit option.
Key Changes:
- Added in-browser text editing capability with syntax highlighting for 20+ file types
- Integrated file editor modal into existing File Explorer workflow with permission-based access control
- Comprehensive internationalization support across all 21 supported languages
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| react/src/components/TextFileEditorModal.tsx | New modal component implementing CodeMirror-based text editor with file loading and saving logic |
| react/src/components/FolderExplorerModal.tsx | Integration of TextFileEditorModal with state management and refresh callback |
| packages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx | Added enableEdit prop and onClickEditFile callback to support edit functionality |
| packages/backend.ai-ui/src/components/baiClient/FileExplorer/FileItemControls.tsx | Added More menu dropdown with Edit option, text file detection logic, and edit button handling |
| packages/backend.ai-ui/src/components/provider/BAIClientProvider/index.ts | Exported VFolderFile type for use in React components |
| resources/i18n/*.json (21 files) | Added "EditFile" and "FailedToLoadFile" translations for all supported languages |
| packages/backend.ai-ui/src/locale/*.json (21 files) | Added "EditFile" translation to backend.ai-ui package locale files |
| const blob = await response.blob(); | ||
| const text = await blob.text(); |
Copilot
AI
Jan 7, 2026
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 encoding detection for text files. The implementation assumes all files are UTF-8 encoded, but files could have different encodings (e.g., Latin-1, Windows-1252). When reading via blob.text(), the browser attempts to decode using UTF-8, which could result in corrupted text for non-UTF-8 files. Consider adding encoding detection or at least documenting this limitation.
| const blob = await response.blob(); | |
| const text = await blob.text(); | |
| const contentType = response.headers.get('Content-Type') || ''; | |
| const charsetMatch = contentType.match(/charset=([^;]+)/i); | |
| const charset = charsetMatch?.[1]?.trim().toLowerCase() || 'utf-8'; | |
| const arrayBuffer = await response.arrayBuffer(); | |
| const decoder = new TextDecoder(charset, { fatal: false }); | |
| const text = decoder.decode(new Uint8Array(arrayBuffer)); |
| const TEXT_FILE_EXTENSIONS = [ | ||
| '.txt', | ||
| '.md', | ||
| '.json', | ||
| '.yaml', | ||
| '.yml', | ||
| '.xml', | ||
| '.csv', | ||
| '.js', | ||
| '.ts', | ||
| '.jsx', | ||
| '.tsx', | ||
| '.py', | ||
| '.sh', | ||
| '.bash', | ||
| '.html', | ||
| '.css', | ||
| '.scss', | ||
| '.less', | ||
| '.sql', | ||
| '.log', | ||
| '.env', | ||
| '.conf', | ||
| '.config', | ||
| '.ini', | ||
| '.toml', | ||
| ]; |
Copilot
AI
Jan 7, 2026
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 TEXT_FILE_EXTENSIONS array is defined within the component file but could be shared across the codebase. Consider extracting this to a constants file or utility module, especially since the file type detection logic might be useful elsewhere in the application.
| <BAIFlex align="center" gap="xs"> | ||
| <Typography.Text>{t('data.explorer.EditFile')}</Typography.Text> | ||
| {fileInfo && ( | ||
| <Typography.Text type="secondary" style={{ fontWeight: 'normal' }}> | ||
| - {fileInfo.name} | ||
| {fileInfo.size > 0 && | ||
| ` (${convertToDecimalUnit(fileInfo.size, 'auto')?.displayValue})`} | ||
| </Typography.Text> | ||
| )} |
Copilot
AI
Jan 7, 2026
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 modal title uses Ant Design's Typography.Text directly instead of BAIText from the backend.ai-ui package. According to the React Component Guidelines, BAI components should be preferred over Ant Design equivalents for better theme management and consistency. Consider replacing Typography.Text with BAIText.
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| }} | ||
| icon={<MoreOutlined />} |
Copilot
AI
Jan 7, 2026
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 More menu button (MoreOutlined icon) lacks an accessible label. Screen reader users won't know what this button does. Consider adding an aria-label attribute to the Button component to describe its purpose, such as "File actions" or "More options".
| icon={<MoreOutlined />} | |
| icon={<MoreOutlined />} | |
| aria-label={t('comp:FileExplorer.MoreOptions')} |
| useEffect(() => { | ||
| if (!open || !fileInfo || !targetVFolderId) return; | ||
|
|
||
| const filePath = | ||
| currentPath === '.' | ||
| ? fileInfo.name | ||
| : `${currentPath}/${fileInfo.name}`.replace(/^\.\//, ''); | ||
|
|
||
| const loadFileContent = async () => { | ||
| setIsLoading(true); | ||
| setLoadError(null); | ||
|
|
||
| try { | ||
| const tokenResponse = await baiClient.vfolder.request_download_token( | ||
| filePath, | ||
| targetVFolderId, | ||
| false, | ||
| ); | ||
|
|
||
| const downloadUrl = `${tokenResponse.url}?token=${tokenResponse.token}&archive=false`; | ||
| const response = await fetch(downloadUrl); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`HTTP error! status: ${response.status}`); | ||
| } | ||
|
|
||
| const blob = await response.blob(); | ||
| const text = await blob.text(); | ||
| setContent(text); | ||
| } catch (error) { | ||
| const errorMessage = | ||
| error instanceof Error ? error.message : 'Unknown error'; | ||
| setLoadError(errorMessage); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| loadFileContent(); | ||
| }, [open, fileInfo, targetVFolderId, currentPath, baiClient]); |
Copilot
AI
Jan 7, 2026
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.
Potential race condition in the file loading useEffect. If the file changes while a previous load is still in progress, the old request could complete after the new one, causing the wrong content to be displayed. Consider using an AbortController to cancel pending requests when dependencies change, or add a cleanup function to ignore stale results.
| const handleCancel = () => { | ||
| onRequestClose(false); | ||
| }; |
Copilot
AI
Jan 7, 2026
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 warning when user tries to close the modal with unsaved changes. If the user has modified the file content but hasn't saved, clicking Cancel or the X button will discard all changes without confirmation. Consider tracking whether content has been modified and showing a confirmation dialog before closing if there are unsaved changes.
| <Alert | ||
| type="error" | ||
| message={t('data.explorer.FailedToLoadFile')} | ||
| description={loadError} |
Copilot
AI
Jan 7, 2026
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 error message shown to users displays the raw technical error message from the Error object, which may not be user-friendly or localized. For example, HTTP errors or network failures could show technical details that confuse users. Consider mapping common error scenarios to user-friendly, translated messages, and only showing technical details in a collapsible section or console log for debugging purposes.
| description={loadError} | |
| description={t('data.explorer.FailedToLoadFileDescription')} |
| const loadFileContent = async () => { | ||
| setIsLoading(true); | ||
| setLoadError(null); | ||
|
|
||
| try { | ||
| const tokenResponse = await baiClient.vfolder.request_download_token( | ||
| filePath, | ||
| targetVFolderId, | ||
| false, | ||
| ); | ||
|
|
||
| const downloadUrl = `${tokenResponse.url}?token=${tokenResponse.token}&archive=false`; | ||
| const response = await fetch(downloadUrl); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`HTTP error! status: ${response.status}`); | ||
| } | ||
|
|
||
| const blob = await response.blob(); | ||
| const text = await blob.text(); | ||
| setContent(text); | ||
| } catch (error) { | ||
| const errorMessage = | ||
| error instanceof Error ? error.message : 'Unknown error'; | ||
| setLoadError(errorMessage); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; |
Copilot
AI
Jan 7, 2026
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 file size validation before attempting to load the file. Large files could cause performance issues or browser crashes when loaded into memory. The implementation should check the file size against a reasonable limit (e.g., using baiClient._config.maxFileUploadSize) and show an appropriate error message if the file is too large to edit in the browser.
| } catch (error) { | ||
| const errorMessage = | ||
| error instanceof Error ? error.message : 'Unknown error'; | ||
| setLoadError(errorMessage); | ||
| } finally { | ||
| setIsSaving(false); | ||
| } |
Copilot
AI
Jan 7, 2026
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 error handling in the save operation sets loadError instead of a dedicated save error state. This means if a save fails, the editor content area will be replaced with an error alert, losing any unsaved changes. Consider using a separate state for save errors and displaying them differently (e.g., as a notification or alert banner above the editor) to preserve the user's work.
| useEffect(() => { | ||
| if (!open || !fileInfo || !targetVFolderId) return; | ||
|
|
||
| const filePath = | ||
| currentPath === '.' | ||
| ? fileInfo.name | ||
| : `${currentPath}/${fileInfo.name}`.replace(/^\.\//, ''); | ||
|
|
||
| const loadFileContent = async () => { | ||
| setIsLoading(true); | ||
| setLoadError(null); | ||
|
|
||
| try { | ||
| const tokenResponse = await baiClient.vfolder.request_download_token( | ||
| filePath, | ||
| targetVFolderId, | ||
| false, | ||
| ); | ||
|
|
||
| const downloadUrl = `${tokenResponse.url}?token=${tokenResponse.token}&archive=false`; | ||
| const response = await fetch(downloadUrl); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`HTTP error! status: ${response.status}`); | ||
| } | ||
|
|
||
| const blob = await response.blob(); | ||
| const text = await blob.text(); | ||
| setContent(text); | ||
| } catch (error) { | ||
| const errorMessage = | ||
| error instanceof Error ? error.message : 'Unknown error'; | ||
| setLoadError(errorMessage); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| loadFileContent(); | ||
| }, [open, fileInfo, targetVFolderId, currentPath, baiClient]); |
Copilot
AI
Jan 7, 2026
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 state cleanup when modal closes. If the user closes the modal with unsaved changes and reopens the same file, the content state from the previous session will still be present briefly before the new file content loads. This could be confusing. Consider resetting state (content, loadError, isLoading) when the modal closes or in a useEffect with appropriate dependencies.
nowgnuesLee
left a comment
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 test actions are failing. Could you take a look and fix them?

resolves #TBD (FR-1014)
Summary
Implement the ability to directly edit and save simple text files within the NEO File Explorer.
Background
Users frequently need to modify configuration files such as yaml, toml, and json within vfolders for model serving. Currently, this requires downloading the file → editing locally → re-uploading, which results in a poor user experience.
Implementation
Key Features
Components Added
TextFileEditorModal.tsx: Main editor modal with BAICodeEditorFileItemControls.tsx: Added More menu with Edit optionBAIFileExplorer.tsx: AddedenableEditandonClickEditFilepropsFolderExplorerModal.tsx: Integration with TextFileEditorModalTechnical Details
request_download_token→fetch→blob.text()new File()→FileUploadManager.uploadFiles()(overwrites)write_contentpermissionbaiClient._config.maxFileUploadSizei18n Support
comp:FileExplorer.EditFile: "Edit"data.explorer.EditFile: "Edit File"Checklist:
write_contentpermission on vfoldertest
2026-01-07.4.13.59.mov