8
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
9
import type {editor} from 'monaco-editor';
10
import * as monaco from 'monaco-editor';
11
-import {useState} from 'react';
11
+import React, {useState, useCallback} from 'react';
12
import {Resizable} from 're-resizable';
13
+import {useSnackbar} from 'notistack';
14
import {useStore, useStoreDispatch} from '../StoreContext';
15
import {monacoOptions} from './monacoOptions';
16
import {
17
+ ConfigError,
18
generateOverridePragmaFromConfig,
19
updateSourceWithOverridePragma,
20
} from '../../lib/configUtils';
21
22
+// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
23
+import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
24
+
25
loader.config({monaco});
26
22
-export default function ConfigEditor(): JSX.Element {
23
- const [, setMonaco] = useState<Monaco | null>(null);
27
+export default function ConfigEditor(): React.ReactElement {
28
+ const [isExpanded, setIsExpanded] = useState(false);
29
const store = useStore();
30
const dispatchStore = useStoreDispatch();
31
+ const {enqueueSnackbar} = useSnackbar();
32
27
- const handleChange: (value: string | undefined) => void = async value => {
28
- if (value === undefined) return;
33
+ const toggleExpanded = useCallback(() => {
34
+ setIsExpanded(prev => !prev);
35
+ }, []);
36
37
+ const handleApplyConfig: () => Promise<void> = async () => {
38
try {
31
- const newPragma = await generateOverridePragmaFromConfig(value);
39
+ const config = store.config || '';
40
+
41
+ if (!config.trim()) {
42
+ enqueueSnackbar(
43
+ 'Config is empty. Please add configuration options first.',
44
+ {
45
+ variant: 'warning',
46
+ },
47
+ );
48
+ return;
49
+ }
50
+ const newPragma = await generateOverridePragmaFromConfig(config);
51
const updatedSource = updateSourceWithOverridePragma(
52
store.source,
53
newPragma,
54
);
55
37
- // Update the store with both the new config and updated source
56
dispatchStore({
57
type: 'updateFile',
58
payload: {
59
source: updatedSource,
42
- config: value,
43
- },
44
- });
45
- } catch (_) {
46
- dispatchStore({
47
- type: 'updateFile',
48
- payload: {
49
- source: store.source,
50
- config: value,
60
+ config: config,
61
},
62
});
63
+ } catch (error) {
64
+ console.error('Failed to apply config:', error);
65
+
66
+ if (error instanceof ConfigError && error.message.trim()) {
67
+ enqueueSnackbar(error.message, {
68
+ variant: 'error',
69
+ });
70
+ } else {
71
+ enqueueSnackbar('Unexpected error: failed to apply config.', {
72
+ variant: 'error',
73
+ });
74
+ }
75
}
76
};
77
78
+ const handleChange: (value: string | undefined) => void = value => {
79
+ if (value === undefined) return;
80
+
81
+ // Only update the config
82
+ dispatchStore({
83
+ type: 'updateFile',
84
+ payload: {
85
+ source: store.source,
86
+ config: value,
87
+ },
88
+ });
89
+ };
90
+
91
const handleMount: (
92
_: editor.IStandaloneCodeEditor,
93
monaco: Monaco,
94
) => void = (_, monaco) => {
60
- setMonaco(monaco);
95
+ // Add the babel-plugin-react-compiler type definitions to Monaco
96
+ monaco.languages.typescript.typescriptDefaults.addExtraLib(
97
+ //@ts-expect-error - compilerTypeDefs is a string
98
+ compilerTypeDefs,
99
+ 'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
100
+ );
101
+ monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
102
+ target: monaco.languages.typescript.ScriptTarget.Latest,
103
+ allowNonTsExtensions: true,
104
+ moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
105
+ module: monaco.languages.typescript.ModuleKind.ESNext,
106
+ noEmit: true,
107
+ strict: false,
108
+ esModuleInterop: true,
109
+ allowSyntheticDefaultImports: true,
110
+ jsx: monaco.languages.typescript.JsxEmit.React,
111
+ });
112
62
- const uri = monaco.Uri.parse(`file:///config.js`);
113
+ const uri = monaco.Uri.parse(`file:///config.ts`);
114
const model = monaco.editor.getModel(uri);
115
if (model) {
116
model.updateOptions({tabSize: 2});
118
};
119
120
return (
70
- <div className="relative flex flex-col flex-none border-r border-gray-200">
71
- <h2 className="p-4 duration-150 ease-in border-b cursor-default border-grey-200 font-light text-secondary">
72
- Config Overrides
73
- </h2>
74
- <Resizable
75
- minWidth={300}
76
- maxWidth={600}
77
- defaultSize={{width: 350, height: 'auto'}}
78
- enable={{right: true}}
79
- className="!h-[calc(100vh_-_3.5rem_-_4rem)]">
80
- <MonacoEditor
81
- path={'config.js'}
82
- language={'javascript'}
83
- value={store.config}
84
- onMount={handleMount}
85
- onChange={handleChange}
86
- options={{
87
- ...monacoOptions,
88
- lineNumbers: 'off',
89
- folding: false,
90
- renderLineHighlight: 'none',
91
- scrollBeyondLastLine: false,
92
- hideCursorInOverviewRuler: true,
93
- overviewRulerBorder: false,
94
- overviewRulerLanes: 0,
95
- fontSize: 12,
96
- }}
97
- />
98
- </Resizable>
121
+ <div className="flex flex-row relative">
122
+ {isExpanded ? (
123
+ <>
124
+ <Resizable
125
+ className="border-r"
126
+ minWidth={300}
127
+ maxWidth={600}
128
+ defaultSize={{width: 350, height: 'auto'}}
129
+ enable={{right: true}}>
130
+ <h2
131
+ title="Minimize config editor"
132
+ aria-label="Minimize config editor"
133
+ onClick={toggleExpanded}
134
+ className="p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 font-light text-secondary hover:text-link">
135
+ - Config Overrides
136
+ </h2>
137
+ <div className="h-[calc(100vh_-_3.5rem_-_4rem)]">
138
+ <MonacoEditor
139
+ path={'config.ts'}
140
+ language={'typescript'}
141
+ value={store.config}
142
+ onMount={handleMount}
143
+ onChange={handleChange}
144
+ options={{
145
+ ...monacoOptions,
146
+ lineNumbers: 'off',
147
+ folding: false,
148
+ renderLineHighlight: 'none',
149
+ scrollBeyondLastLine: false,
150
+ hideCursorInOverviewRuler: true,
151
+ overviewRulerBorder: false,
152
+ overviewRulerLanes: 0,
153
+ fontSize: 12,
154
+ }}
155
+ />
156
+ </div>
157
+ </Resizable>
158
+ <button
159
+ onClick={handleApplyConfig}
160
+ title="Apply config overrides to input"
161
+ aria-label="Apply config overrides to input"
162
+ className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10 w-8 h-8 bg-blue-500 hover:bg-blue-600 text-white rounded-full border-2 border-white shadow-lg flex items-center justify-center text-sm font-medium transition-colors duration-150">
163
+ →
164
+ </button>
165
+ </>
166
+ ) : (
167
+ <div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
168
+ <button
169
+ title="Expand config editor"
170
+ aria-label="Expand config editor"
171
+ style={{
172
+ transform: 'rotate(90deg) translate(-50%)',
173
+ whiteSpace: 'nowrap',
174
+ }}
175
+ onClick={toggleExpanded}
176
+ className="flex-grow-0 w-5 transition-colors duration-150 ease-in font-light text-secondary hover:text-link">
177
+ Config Overrides
178
+ </button>
179
+ </div>
180
+ )}
181
</div>
182
);
183
}