Skip to content

Commit 30be997

Browse files
author
igardev
committed
Replace alert and confirm with custom modals. This is needed as Webview in VS Code doesn't permit alert and confirm for security reasons.
1 parent 6b56a64 commit 30be997

File tree

4 files changed

+246
-141
lines changed

4 files changed

+246
-141
lines changed

tools/server/public/index.html.gz

303 Bytes
Binary file not shown.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export function ConfirmModal({
2+
isOpen,
3+
onClose,
4+
onConfirm,
5+
message,
6+
}: {
7+
isOpen: boolean;
8+
onClose: () => void;
9+
onConfirm: () => void;
10+
message: string;
11+
}) {
12+
if (!isOpen) return null;
13+
14+
return (
15+
<div className="modal modal-open z-[1000]">
16+
<div className="modal-box">
17+
<h3 className="font-bold text-lg">{message}</h3>
18+
<div className="modal-action">
19+
<button className="btn btn-ghost" onClick={onClose}>
20+
Cancel
21+
</button>
22+
<button className="btn btn-error" onClick={onConfirm}>
23+
Confirm
24+
</button>
25+
</div>
26+
</div>
27+
</div>
28+
);
29+
}
30+
31+
export function AlertModal({
32+
isOpen,
33+
onClose,
34+
message,
35+
}: {
36+
isOpen: boolean;
37+
onClose: () => void;
38+
message: string;
39+
}) {
40+
if (!isOpen) return null;
41+
42+
return (
43+
<div className="modal modal-open z-[1000]">
44+
<div className="modal-box">
45+
<h3 className="font-bold text-lg">{message}</h3>
46+
<div className="modal-action">
47+
<button className="btn" onClick={onClose}>
48+
OK
49+
</button>
50+
</div>
51+
</div>
52+
</div>
53+
);
54+
}

tools/server/webui/src/components/SettingDialog.tsx

Lines changed: 158 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
SquaresPlusIcon,
1414
} from '@heroicons/react/24/outline';
1515
import { OpenInNewTab } from '../utils/common';
16+
import { ConfirmModal, AlertModal } from './CustomModals';
1617

1718
type SettKey = keyof typeof CONFIG_DEFAULT;
1819

@@ -277,16 +278,36 @@ export default function SettingDialog({
277278
}) {
278279
const { config, saveConfig } = useAppContext();
279280
const [sectionIdx, setSectionIdx] = useState(0);
281+
const [showResetConfirm, setShowResetConfirm] = useState(false);
282+
const [alertState, setAlertState] = useState({
283+
isOpen: false,
284+
message: '',
285+
});
280286

281287
// clone the config object to prevent direct mutation
282288
const [localConfig, setLocalConfig] = useState<typeof CONFIG_DEFAULT>(
283289
JSON.parse(JSON.stringify(config))
284290
);
285291

286292
const resetConfig = () => {
287-
if (window.confirm('Are you sure you want to reset all settings?')) {
288-
setLocalConfig(CONFIG_DEFAULT);
289-
}
293+
setShowResetConfirm(true);
294+
};
295+
296+
const handleResetConfirm = () => {
297+
setLocalConfig(CONFIG_DEFAULT);
298+
setShowResetConfirm(false);
299+
};
300+
301+
const handleResetCancel = () => {
302+
setShowResetConfirm(false);
303+
};
304+
305+
const showAlert = (message: string) => {
306+
setAlertState({ isOpen: true, message });
307+
};
308+
309+
const closeAlert = () => {
310+
setAlertState({ isOpen: false, message: '' });
290311
};
291312

292313
const handleSave = () => {
@@ -302,22 +323,22 @@ export default function SettingDialog({
302323
const mustBeNumeric = isNumeric(CONFIG_DEFAULT[key as SettKey]);
303324
if (mustBeString) {
304325
if (!isString(value)) {
305-
alert(`Value for ${key} must be string`);
326+
showAlert(`Value for ${key} must be string`);
306327
return;
307328
}
308329
} else if (mustBeNumeric) {
309330
const trimmedValue = value.toString().trim();
310331
const numVal = Number(trimmedValue);
311332
if (isNaN(numVal) || !isNumeric(numVal) || trimmedValue.length === 0) {
312-
alert(`Value for ${key} must be numeric`);
333+
showAlert(`Value for ${key} must be numeric`);
313334
return;
314335
}
315336
// force conversion to number
316337
// @ts-expect-error this is safe
317338
newConfig[key] = numVal;
318339
} else if (mustBeBoolean) {
319340
if (!isBoolean(value)) {
320-
alert(`Value for ${key} must be boolean`);
341+
showAlert(`Value for ${key} must be boolean`);
321342
return;
322343
}
323344
} else {
@@ -335,130 +356,143 @@ export default function SettingDialog({
335356
};
336357

337358
return (
338-
<dialog
339-
className={classNames({ modal: true, 'modal-open': show })}
340-
aria-label="Settings dialog"
341-
>
342-
<div className="modal-box w-11/12 max-w-3xl">
343-
<h3 className="text-lg font-bold mb-6">Settings</h3>
344-
<div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
345-
{/* Left panel, showing sections - Desktop version */}
346-
<div
347-
className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200"
348-
role="complementary"
349-
aria-description="Settings sections"
350-
tabIndex={0}
351-
>
352-
{SETTING_SECTIONS.map((section, idx) => (
353-
<button
354-
key={idx}
355-
className={classNames({
356-
'btn btn-ghost justify-start font-normal w-44 mb-1': true,
357-
'btn-active': sectionIdx === idx,
358-
})}
359-
onClick={() => setSectionIdx(idx)}
360-
dir="auto"
361-
>
362-
{section.title}
363-
</button>
364-
))}
365-
</div>
359+
<>
360+
<ConfirmModal
361+
isOpen={showResetConfirm}
362+
onClose={handleResetCancel}
363+
onConfirm={handleResetConfirm}
364+
message="Are you sure you want to reset all settings?"
365+
/>
366+
<AlertModal
367+
isOpen={alertState.isOpen}
368+
onClose={closeAlert}
369+
message={alertState.message}
370+
/>
371+
<dialog
372+
className={classNames({ modal: true, 'modal-open': show })}
373+
aria-label="Settings dialog"
374+
>
375+
<div className="modal-box w-11/12 max-w-3xl">
376+
<h3 className="text-lg font-bold mb-6">Settings</h3>
377+
<div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
378+
{/* Left panel, showing sections - Desktop version */}
379+
<div
380+
className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200"
381+
role="complementary"
382+
aria-description="Settings sections"
383+
tabIndex={0}
384+
>
385+
{SETTING_SECTIONS.map((section, idx) => (
386+
<button
387+
key={idx}
388+
className={classNames({
389+
'btn btn-ghost justify-start font-normal w-44 mb-1': true,
390+
'btn-active': sectionIdx === idx,
391+
})}
392+
onClick={() => setSectionIdx(idx)}
393+
dir="auto"
394+
>
395+
{section.title}
396+
</button>
397+
))}
398+
</div>
366399

367-
{/* Left panel, showing sections - Mobile version */}
368-
{/* This menu is skipped on a11y, otherwise it's repeated the desktop version */}
369-
<div
370-
className="md:hidden flex flex-row gap-2 mb-4"
371-
aria-disabled={true}
372-
>
373-
<details className="dropdown">
374-
<summary className="btn bt-sm w-full m-1">
375-
{SETTING_SECTIONS[sectionIdx].title}
376-
</summary>
377-
<ul className="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
378-
{SETTING_SECTIONS.map((section, idx) => (
379-
<div
380-
key={idx}
381-
className={classNames({
382-
'btn btn-ghost justify-start font-normal': true,
383-
'btn-active': sectionIdx === idx,
384-
})}
385-
onClick={() => setSectionIdx(idx)}
386-
dir="auto"
387-
>
388-
{section.title}
389-
</div>
390-
))}
391-
</ul>
392-
</details>
393-
</div>
400+
{/* Left panel, showing sections - Mobile version */}
401+
{/* This menu is skipped on a11y, otherwise it's repeated the desktop version */}
402+
<div
403+
className="md:hidden flex flex-row gap-2 mb-4"
404+
aria-disabled={true}
405+
>
406+
<details className="dropdown">
407+
<summary className="btn bt-sm w-full m-1">
408+
{SETTING_SECTIONS[sectionIdx].title}
409+
</summary>
410+
<ul className="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
411+
{SETTING_SECTIONS.map((section, idx) => (
412+
<div
413+
key={idx}
414+
className={classNames({
415+
'btn btn-ghost justify-start font-normal': true,
416+
'btn-active': sectionIdx === idx,
417+
})}
418+
onClick={() => setSectionIdx(idx)}
419+
dir="auto"
420+
>
421+
{section.title}
422+
</div>
423+
))}
424+
</ul>
425+
</details>
426+
</div>
394427

395-
{/* Right panel, showing setting fields */}
396-
<div className="grow overflow-y-auto px-4">
397-
{SETTING_SECTIONS[sectionIdx].fields.map((field, idx) => {
398-
const key = `${sectionIdx}-${idx}`;
399-
if (field.type === SettingInputType.SHORT_INPUT) {
400-
return (
401-
<SettingsModalShortInput
402-
key={key}
403-
configKey={field.key}
404-
value={localConfig[field.key]}
405-
onChange={onChange(field.key)}
406-
label={field.label as string}
407-
/>
408-
);
409-
} else if (field.type === SettingInputType.LONG_INPUT) {
410-
return (
411-
<SettingsModalLongInput
412-
key={key}
413-
configKey={field.key}
414-
value={localConfig[field.key].toString()}
415-
onChange={onChange(field.key)}
416-
label={field.label as string}
417-
/>
418-
);
419-
} else if (field.type === SettingInputType.CHECKBOX) {
420-
return (
421-
<SettingsModalCheckbox
422-
key={key}
423-
configKey={field.key}
424-
value={!!localConfig[field.key]}
425-
onChange={onChange(field.key)}
426-
label={field.label as string}
427-
/>
428-
);
429-
} else if (field.type === SettingInputType.CUSTOM) {
430-
return (
431-
<div key={key} className="mb-2">
432-
{typeof field.component === 'string'
433-
? field.component
434-
: field.component({
435-
value: localConfig[field.key],
436-
onChange: onChange(field.key),
437-
})}
438-
</div>
439-
);
440-
}
441-
})}
428+
{/* Right panel, showing setting fields */}
429+
<div className="grow overflow-y-auto px-4">
430+
{SETTING_SECTIONS[sectionIdx].fields.map((field, idx) => {
431+
const key = `${sectionIdx}-${idx}`;
432+
if (field.type === SettingInputType.SHORT_INPUT) {
433+
return (
434+
<SettingsModalShortInput
435+
key={key}
436+
configKey={field.key}
437+
value={localConfig[field.key]}
438+
onChange={onChange(field.key)}
439+
label={field.label as string}
440+
/>
441+
);
442+
} else if (field.type === SettingInputType.LONG_INPUT) {
443+
return (
444+
<SettingsModalLongInput
445+
key={key}
446+
configKey={field.key}
447+
value={localConfig[field.key].toString()}
448+
onChange={onChange(field.key)}
449+
label={field.label as string}
450+
/>
451+
);
452+
} else if (field.type === SettingInputType.CHECKBOX) {
453+
return (
454+
<SettingsModalCheckbox
455+
key={key}
456+
configKey={field.key}
457+
value={!!localConfig[field.key]}
458+
onChange={onChange(field.key)}
459+
label={field.label as string}
460+
/>
461+
);
462+
} else if (field.type === SettingInputType.CUSTOM) {
463+
return (
464+
<div key={key} className="mb-2">
465+
{typeof field.component === 'string'
466+
? field.component
467+
: field.component({
468+
value: localConfig[field.key],
469+
onChange: onChange(field.key),
470+
})}
471+
</div>
472+
);
473+
}
474+
})}
442475

443-
<p className="opacity-40 mb-6 text-sm mt-8">
444-
Settings are saved in browser's localStorage
445-
</p>
476+
<p className="opacity-40 mb-6 text-sm mt-8">
477+
Settings are saved in browser's localStorage
478+
</p>
479+
</div>
446480
</div>
447-
</div>
448481

449-
<div className="modal-action">
450-
<button className="btn" onClick={resetConfig}>
451-
Reset to default
452-
</button>
453-
<button className="btn" onClick={onClose}>
454-
Close
455-
</button>
456-
<button className="btn btn-primary" onClick={handleSave}>
457-
Save
458-
</button>
482+
<div className="modal-action">
483+
<button className="btn" onClick={resetConfig}>
484+
Reset to default
485+
</button>
486+
<button className="btn" onClick={onClose}>
487+
Close
488+
</button>
489+
<button className="btn btn-primary" onClick={handleSave}>
490+
Save
491+
</button>
492+
</div>
459493
</div>
460-
</div>
461-
</dialog>
494+
</dialog>
495+
</>
462496
);
463497
}
464498

0 commit comments

Comments
 (0)