Skip to content

Conversation

@limshyun
Copy link
Contributor

@limshyun limshyun commented Jan 6, 2026

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

  • Edit button in More menu: Added dropdown menu with edit option in File Explorer controls
  • Text file detection: Supports .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
  • CodeMirror editor: Full-featured text editor with syntax highlighting
  • File operations: Download → Edit → Upload workflow using existing APIs

Components Added

  • TextFileEditorModal.tsx: Main editor modal with BAICodeEditor
  • Enhanced FileItemControls.tsx: Added More menu with Edit option
  • Enhanced BAIFileExplorer.tsx: Added enableEdit and onClickEditFile props
  • Updated FolderExplorerModal.tsx: Integration with TextFileEditorModal

Technical Details

  • File Reading: request_download_tokenfetchblob.text()
  • File Saving: Modified content → new File()FileUploadManager.uploadFiles() (overwrites)
  • Permissions: Requires write_content permission
  • Size Limit: baiClient._config.maxFileUploadSize

i18n Support

  • comp:FileExplorer.EditFile: "Edit"
  • data.explorer.EditFile: "Edit File"
  • `data.explorer.FailedToLoadFile": "Failed to load file"

Checklist:

  • Documentation (PR description)
  • Minimum required manager version (N/A - client-side feature)
  • Specific setting for review: Test with various text files in File Explorer
  • Minimum requirements: write_content permission on vfolder
  • Test cases: Edit .txt, .yaml, .json files; verify save/cancel functionality

test

2026-01-07.4.13.59.mov

@github-actions github-actions bot added area:ux UI / UX issue. area:i18n Localization labels Jan 6, 2026
@github-actions github-actions bot added the size:L 100~500 LoC label Jan 6, 2026
@cla-assistant
Copy link

cla-assistant bot commented Jan 6, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

1 similar comment
@cla-assistant
Copy link

cla-assistant bot commented Jan 6, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor Author

limshyun commented Jan 6, 2026


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • flow:merge-queue - adds this PR to the back of the merge queue
  • flow:hotfix - for urgent hot fixes, skip the queue and merge this PR next

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.

@limshyun limshyun changed the title feat(i18n): add EditFile translation to all locale files feat(FR-1869): implement text file editor for NEO File Explorer Jan 6, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

Coverage report for ./packages/backend.ai-ui

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

@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

Coverage report for ./react

Caution

Test run failed

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

@limshyun limshyun marked this pull request as ready for review January 6, 2026 04:18
@limshyun limshyun marked this pull request as draft January 7, 2026 05:16
@limshyun limshyun marked this pull request as ready for review January 7, 2026 07:15
@nowgnuesLee nowgnuesLee requested a review from Copilot January 7, 2026 07:32
Copy link
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 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

Comment on lines +79 to +80
const blob = await response.blob();
const text = await blob.text();
Copy link

Copilot AI Jan 7, 2026

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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +51
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',
];
Copy link

Copilot AI Jan 7, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +131
<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>
)}
Copy link

Copilot AI Jan 7, 2026

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.

Copilot generated this review using guidance from repository custom instructions.
e.preventDefault();
e.stopPropagation();
}}
icon={<MoreOutlined />}
Copy link

Copilot AI Jan 7, 2026

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".

Suggested change
icon={<MoreOutlined />}
icon={<MoreOutlined />}
aria-label={t('comp:FileExplorer.MoreOptions')}

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +92
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]);
Copy link

Copilot AI Jan 7, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +117
const handleCancel = () => {
onRequestClose(false);
};
Copy link

Copilot AI Jan 7, 2026

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.

Copilot uses AI. Check for mistakes.
<Alert
type="error"
message={t('data.explorer.FailedToLoadFile')}
description={loadError}
Copy link

Copilot AI Jan 7, 2026

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.

Suggested change
description={loadError}
description={t('data.explorer.FailedToLoadFileDescription')}

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +89
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);
}
};
Copy link

Copilot AI Jan 7, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +112
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
setLoadError(errorMessage);
} finally {
setIsSaving(false);
}
Copy link

Copilot AI Jan 7, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +92
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]);
Copy link

Copilot AI Jan 7, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@nowgnuesLee nowgnuesLee left a 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?

@limshyun limshyun closed this Jan 7, 2026
@limshyun limshyun deleted the feat/FR-1869/text-file-editor branch January 7, 2026 08:47
@limshyun limshyun changed the title feat(FR-1869): implement text file editor for NEO File Explorer feat(FR-1014): implement text file editor for NEO File Explorer Jan 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n Localization area:ux UI / UX issue. size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants