From 3bf677af6ca5d0afcc9595e2bf1076cb282bb78b Mon Sep 17 00:00:00 2001 From: Anso Date: Thu, 25 Jun 2026 18:25:53 -0400 Subject: [PATCH] feat: add syntax highlighting to .env editor tab (#1459) Apply Monaco built-in ini language mode to the .env editor tab, matching the syntax highlighting already used by the FileViewer, diff previews, and Git source diffs for env content. - EditorView.tsx: change env tab language from plaintext to ini - Add EditorView.test.tsx: 3 cases asserting ini/yaml/files tabs - Docs: update editor.mdx to reflect env tab has highlighting --- docs/features/editor.mdx | 2 +- .../components/EditorLayout/EditorView.tsx | 2 +- .../__tests__/EditorView.test.tsx | 139 ++++++++++++++++++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/EditorLayout/__tests__/EditorView.test.tsx diff --git a/docs/features/editor.mdx b/docs/features/editor.mdx index 79566715..afa0cfc8 100644 --- a/docs/features/editor.mdx +++ b/docs/features/editor.mdx @@ -100,7 +100,7 @@ The editor card has three tabs: | Tab | Purpose | |-----|---------| | `compose.yaml` | YAML editor with syntax highlighting. Always available. | -| `.env` | Plaintext editor for the discovered env file. Disabled when the stack has no env file. | +| `.env` | Environment file editor with syntax highlighting. Disabled when the stack has no env file. | | **Files** | Browseable directory tree for the stack folder. See the dedicated [Stack File Explorer](/features/stack-file-explorer) page for permissions, upload caps, and protected-file behavior. | Above the editor area, the toolbar carries: diff --git a/frontend/src/components/EditorLayout/EditorView.tsx b/frontend/src/components/EditorLayout/EditorView.tsx index 45e08ea8..c18a4439 100644 --- a/frontend/src/components/EditorLayout/EditorView.tsx +++ b/frontend/src/components/EditorLayout/EditorView.tsx @@ -524,7 +524,7 @@ export function EditorView(props: EditorViewProps) { }> { monacoEditorRef.current = editor; }} diff --git a/frontend/src/components/EditorLayout/__tests__/EditorView.test.tsx b/frontend/src/components/EditorLayout/__tests__/EditorView.test.tsx new file mode 100644 index 00000000..c1e34141 --- /dev/null +++ b/frontend/src/components/EditorLayout/__tests__/EditorView.test.tsx @@ -0,0 +1,139 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import { EditorView } from '../EditorView'; +import type { EditorViewProps } from '../EditorView'; + +// Capture Monaco language/value props to assert env tab gets 'ini', compose gets 'yaml'. +let lastLanguage: string | undefined; +let lastValue: string | undefined; +vi.mock('@/lib/monacoLoader', () => ({ + Editor: ({ language, value }: { language?: string; value?: string }) => { + lastLanguage = language; + lastValue = value; + return
; + }, +})); + +// Stub heavy children; this test only asserts the Monaco language prop. +vi.mock('../editor-view-blocks', () => ({ + StackIdentityHeader: () =>
identity-header
, + ContainersHealth: () =>
health-pane
, + StackLogsSection: () =>
logs-pane
, +})); +vi.mock('../../StackAnatomyPanel', () => ({ + default: () =>
anatomy-pane
, +})); +vi.mock('../StackOperationBanner', () => ({ StackOperationBanner: () => null })); +vi.mock('../../ErrorBoundary', () => ({ default: ({ children }: { children: ReactNode }) => <>{children} })); +vi.mock('@/hooks/use-is-mobile', () => ({ useIsMobile: () => false })); + +function makeProps(over: Partial = {}): EditorViewProps { + return { + stackName: 'web', + isDarkMode: false, + activeNode: null, + containers: [], + containerStats: {}, + containerStatsError: null, + content: '', + envContent: '', + envExists: false, + envFiles: [], + selectedEnvFile: '', + isFileLoading: false, + backupInfo: { exists: false, timestamp: null }, + gitSourcePendingMap: {}, + notifications: [], + copiedDigest: null, + loadingAction: null, + stackMisconfigScanning: false, + activeTab: 'compose', + isEditing: false, + editingCompose: false, + logsMode: 'structured', + can: () => true, + isAdmin: false, + trivy: { available: false }, + copiedDigestTimerRef: { current: null }, + deployStack: vi.fn(), + restartStack: vi.fn(), + stopStack: vi.fn(), + updateStack: vi.fn(), + rollbackStack: vi.fn(), + scanStackConfig: vi.fn(), + enterEditMode: vi.fn(), + requestSave: vi.fn(), + requestSaveAndDeploy: vi.fn(), + discardChanges: vi.fn(), + setContent: vi.fn(), + setEnvContent: vi.fn(), + changeEnvFile: vi.fn(), + openLogViewer: vi.fn(), + openBashModal: vi.fn(), + serviceAction: vi.fn(), + setActiveTab: vi.fn(), + setLogsMode: vi.fn(), + setEditingCompose: vi.fn(), + setGitSourceOpen: vi.fn(), + setCopiedDigest: vi.fn(), + requestDeleteStack: vi.fn(), + onRefreshState: vi.fn(), + onDismissRecovery: vi.fn(), + panelStartedAt: null, + onMobileBack: vi.fn(), + onCloseEditor: vi.fn(), + hasUnsavedChanges: () => false, + ...over, + }; +} + +describe('EditorView Monaco language prop', () => { + afterEach(() => { + lastLanguage = undefined; + lastValue = undefined; + }); + + it('passes language="ini" when the env tab is active', () => { + render( + , + ); + expect(lastLanguage).toBe('ini'); + expect(lastValue).toBe('KEY=val\n# a comment'); + }); + + it('passes language="yaml" when the compose tab is active', () => { + render( + , + ); + expect(lastLanguage).toBe('yaml'); + expect(lastValue).toBe('services:\n web:\n image: nginx'); + }); + + it('does not mount Monaco when the files tab is active', () => { + render( + , + ); + // StackFileExplorer replaces Monaco in the files tab path. + expect(lastLanguage).toBeUndefined(); + expect(lastValue).toBeUndefined(); + }); +});