mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
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
This commit is contained in:
@@ -100,7 +100,7 @@ The editor card has three tabs:
|
|||||||
| Tab | Purpose |
|
| Tab | Purpose |
|
||||||
|-----|---------|
|
|-----|---------|
|
||||||
| `compose.yaml` | YAML editor with syntax highlighting. Always available. |
|
| `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. |
|
| **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:
|
Above the editor area, the toolbar carries:
|
||||||
|
|||||||
@@ -524,7 +524,7 @@ export function EditorView(props: EditorViewProps) {
|
|||||||
<Suspense fallback={<div className="w-full h-full" aria-busy="true" />}>
|
<Suspense fallback={<div className="w-full h-full" aria-busy="true" />}>
|
||||||
<Editor
|
<Editor
|
||||||
height="100%"
|
height="100%"
|
||||||
language={activeTab === 'compose' ? 'yaml' : 'plaintext'}
|
language={activeTab === 'compose' ? 'yaml' : 'ini'}
|
||||||
theme={isDarkMode ? 'vs-dark' : 'vs'}
|
theme={isDarkMode ? 'vs-dark' : 'vs'}
|
||||||
value={activeTab === 'compose' ? safeContent : safeEnvContent}
|
value={activeTab === 'compose' ? safeContent : safeEnvContent}
|
||||||
onMount={(editor) => { monacoEditorRef.current = editor; }}
|
onMount={(editor) => { monacoEditorRef.current = editor; }}
|
||||||
|
|||||||
@@ -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 <div data-testid="monaco-editor" />;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Stub heavy children; this test only asserts the Monaco language prop.
|
||||||
|
vi.mock('../editor-view-blocks', () => ({
|
||||||
|
StackIdentityHeader: () => <div>identity-header</div>,
|
||||||
|
ContainersHealth: () => <div>health-pane</div>,
|
||||||
|
StackLogsSection: () => <div>logs-pane</div>,
|
||||||
|
}));
|
||||||
|
vi.mock('../../StackAnatomyPanel', () => ({
|
||||||
|
default: () => <div>anatomy-pane</div>,
|
||||||
|
}));
|
||||||
|
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> = {}): 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(
|
||||||
|
<EditorView
|
||||||
|
{...makeProps({
|
||||||
|
editingCompose: true,
|
||||||
|
activeTab: 'env',
|
||||||
|
envExists: true,
|
||||||
|
envContent: 'KEY=val\n# a comment',
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastLanguage).toBe('ini');
|
||||||
|
expect(lastValue).toBe('KEY=val\n# a comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes language="yaml" when the compose tab is active', () => {
|
||||||
|
render(
|
||||||
|
<EditorView
|
||||||
|
{...makeProps({
|
||||||
|
editingCompose: true,
|
||||||
|
activeTab: 'compose',
|
||||||
|
content: 'services:\n web:\n image: nginx',
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<EditorView
|
||||||
|
{...makeProps({
|
||||||
|
editingCompose: true,
|
||||||
|
activeTab: 'files',
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// StackFileExplorer replaces Monaco in the files tab path.
|
||||||
|
expect(lastLanguage).toBeUndefined();
|
||||||
|
expect(lastValue).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user