feat(ui): replace native confirm() dialogs with reusable modal
Convert the remaining window.confirm() call sites to the in-app MessageDialogHost, extended with a new confirm mode (Cancel + Confirm buttons, optional danger styling) via showConfirmDialog(). Sites converted: - New workspace (EditorPage) - Load project / delete file (FileExplorer) - Overwrite SPIFFS file (BoardOptionsModal) - Delete VFS node (VirtualFileSystem) All dialog strings are internationalized across the 9 supported locales (en, es, pt-br, it, fr, zh-cn, de, ja, ru); the two previously English-only modals now pull from i18n too.
This commit is contained in:
parent
d1d06a44d3
commit
e72413a13f
|
|
@ -11,7 +11,7 @@ import {
|
|||
import type { BoardKind } from '../../types/board';
|
||||
import { boardDisplayName } from '../../types/board';
|
||||
import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject';
|
||||
import { showMessageDialog } from '../../store/useMessageDialogStore';
|
||||
import { showMessageDialog, showConfirmDialog } from '../../store/useMessageDialogStore';
|
||||
import './FileExplorer.css';
|
||||
|
||||
// SVG icons — same style as EditorToolbar (stroke-based, 16x16)
|
||||
|
|
@ -237,6 +237,7 @@ interface FileExplorerProps {
|
|||
}
|
||||
|
||||
export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewClick }) => {
|
||||
const { t } = useTranslation();
|
||||
// Hidden <input type="file"> we trigger via ref when the user clicks
|
||||
// the Open project button. Accepts both .vlx (Velxio native) and .zip
|
||||
// (Wokwi bundle); the dispatcher in utils/importProject.ts decides which
|
||||
|
|
@ -252,14 +253,17 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
e.target.value = '';
|
||||
if (!file) return;
|
||||
const friendlyName = file.name.toLowerCase().endsWith('.zip') ? 'Wokwi .zip' : '.vlx';
|
||||
if (
|
||||
!window.confirm(
|
||||
`Load this ${friendlyName} project? Your current workspace will be replaced. ` +
|
||||
`This cannot be undone.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await showConfirmDialog(
|
||||
t('editor.fileExplorer.confirmLoad.message', { type: friendlyName }),
|
||||
{
|
||||
kind: 'error',
|
||||
title: t('editor.fileExplorer.confirmLoad.title'),
|
||||
confirmLabel: t('editor.fileExplorer.confirmLoad.confirm'),
|
||||
cancelLabel: t('editor.fileTabs.cancel'),
|
||||
danger: true,
|
||||
},
|
||||
);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const result = await importProjectFile(file);
|
||||
// .zip needs the caller to apply the payload to the stores (we keep
|
||||
|
|
@ -287,9 +291,8 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
} catch (err) {
|
||||
showMessageDialog((err as Error).message, { kind: 'error' });
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
fileGroups,
|
||||
activeFileId,
|
||||
|
|
@ -503,11 +506,18 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
setRenamingId(null);
|
||||
}, [renamingId, renameValue, renameFile]);
|
||||
|
||||
const handleDelete = (fileId: string, groupId: string) => {
|
||||
const handleDelete = async (fileId: string, groupId: string) => {
|
||||
setContextMenu(null);
|
||||
const files = fileGroups[groupId] ?? [];
|
||||
if (files.length <= 1) return;
|
||||
if (!window.confirm(t('editor.fileExplorer.confirmDelete'))) return;
|
||||
const confirmed = await showConfirmDialog(t('editor.fileExplorer.confirmDelete'), {
|
||||
kind: 'error',
|
||||
title: t('editor.fileExplorer.contextMenu.delete'),
|
||||
confirmLabel: t('editor.fileExplorer.contextMenu.delete'),
|
||||
cancelLabel: t('editor.fileTabs.cancel'),
|
||||
danger: true,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
deleteFile(fileId);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useVfsStore } from '../../store/useVfsStore';
|
||||
import type { VfsNode } from '../../store/useVfsStore';
|
||||
import { getBoardBridge, useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { showConfirmDialog } from '../../store/useMessageDialogStore';
|
||||
|
||||
/** Resolve true once the board's guest Linux has booted to a shell
|
||||
* (board.piBooted), or false after timeoutMs. Polls the store. */
|
||||
|
|
@ -244,6 +246,7 @@ interface VirtualFileSystemProps {
|
|||
}
|
||||
|
||||
export const VirtualFileSystem: React.FC<VirtualFileSystemProps> = ({ boardId, onFileSelect }) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
initBoardVfs,
|
||||
getRootId,
|
||||
|
|
@ -335,11 +338,21 @@ export const VirtualFileSystem: React.FC<VirtualFileSystemProps> = ({ boardId, o
|
|||
setNewNodeName('');
|
||||
}, [boardId, creatingIn, newNodeName, newNodeType, createNode]);
|
||||
|
||||
const handleDelete = (nodeId: string) => {
|
||||
const handleDelete = async (nodeId: string) => {
|
||||
setCtxMenu(null);
|
||||
const node = getNode(boardId, nodeId);
|
||||
if (!node || node.parentId === null) return;
|
||||
if (!window.confirm(`Delete "${node.name}"?`)) return;
|
||||
const confirmed = await showConfirmDialog(
|
||||
t('editor.pi.confirmDelete.message', { name: node.name }),
|
||||
{
|
||||
kind: 'error',
|
||||
title: t('editor.pi.confirmDelete.title'),
|
||||
confirmLabel: t('editor.pi.confirmDelete.confirm'),
|
||||
cancelLabel: t('editor.pi.confirmDelete.cancel'),
|
||||
danger: true,
|
||||
},
|
||||
);
|
||||
if (!confirmed) return;
|
||||
if (selectedNodeId === nodeId) setSelectedNode(boardId, null);
|
||||
deleteNode(boardId, nodeId);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import { BOARD_KIND_LABELS } from '../../types/board';
|
||||
import type {
|
||||
|
|
@ -19,6 +20,7 @@ import {
|
|||
boardSupportsPsram,
|
||||
getDefaultOptionsForKind,
|
||||
} from '../../types/boardOptions';
|
||||
import { showConfirmDialog } from '../../store/useMessageDialogStore';
|
||||
import './BoardOptionsModal.css';
|
||||
|
||||
interface BoardOptionsModalProps {
|
||||
|
|
@ -80,6 +82,7 @@ export const BoardOptionsModal = ({
|
|||
onApply,
|
||||
onSpiffsChange,
|
||||
}: BoardOptionsModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const seed = useMemo(
|
||||
() => currentOptions ?? getDefaultOptionsForKind(boardKind),
|
||||
[currentOptions, boardKind],
|
||||
|
|
@ -121,8 +124,15 @@ export const BoardOptionsModal = ({
|
|||
const next: SpiffsFile[] = [...spiffsFiles];
|
||||
for (const f of Array.from(files)) {
|
||||
if (next.some((existing) => existing.name === f.name)) {
|
||||
const overwrite = window.confirm(
|
||||
`A file named "${f.name}" already exists. Overwrite?`,
|
||||
const overwrite = await showConfirmDialog(
|
||||
t('editor.boardOptions.confirmOverwrite.message', { name: f.name }),
|
||||
{
|
||||
kind: 'error',
|
||||
title: t('editor.boardOptions.confirmOverwrite.title'),
|
||||
confirmLabel: t('editor.boardOptions.confirmOverwrite.confirm'),
|
||||
cancelLabel: t('editor.boardOptions.confirmOverwrite.cancel'),
|
||||
danger: true,
|
||||
},
|
||||
);
|
||||
if (!overwrite) continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,15 +18,18 @@ const ACCENTS: Record<MessageDialogKind, { bg: string; fg: string; icon: string
|
|||
};
|
||||
|
||||
export const MessageDialogHost = () => {
|
||||
const { open, kind, title, message, close } = useMessageDialogStore();
|
||||
const { open, mode, kind, title, message, confirmLabel, cancelLabel, danger, close } =
|
||||
useMessageDialogStore();
|
||||
const okRef = useRef<HTMLButtonElement | null>(null);
|
||||
const isConfirm = mode === 'confirm';
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
// Focus OK so Enter dismisses, matching the native alert() flow.
|
||||
// Focus the primary button so Enter confirms/dismisses, matching the
|
||||
// native alert()/confirm() flow.
|
||||
okRef.current?.focus();
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') close();
|
||||
if (e.key === 'Escape') close(false);
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
|
|
@ -40,7 +43,7 @@ export const MessageDialogHost = () => {
|
|||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
onClick={close}
|
||||
onClick={() => close(false)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
|
|
@ -92,24 +95,45 @@ export const MessageDialogHost = () => {
|
|||
<span style={{ whiteSpace: 'pre-wrap', overflowY: 'auto' }}>{message}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
|
||||
{isConfirm && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => close(false)}
|
||||
style={{
|
||||
padding: '7px 20px',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: '#e6e6e9',
|
||||
background: '#2a2d35',
|
||||
border: '1px solid #3a3d45',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
ref={okRef}
|
||||
type="button"
|
||||
onClick={close}
|
||||
onClick={() => close(true)}
|
||||
style={{
|
||||
padding: '7px 20px',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: 'white',
|
||||
background: 'linear-gradient(135deg, #007acc 0%, #005ea1 100%)',
|
||||
border: '1px solid #005ea1',
|
||||
background: danger
|
||||
? 'linear-gradient(135deg, #d64545 0%, #a12727 100%)'
|
||||
: 'linear-gradient(135deg, #007acc 0%, #005ea1 100%)',
|
||||
border: danger ? '1px solid #a12727' : '1px solid #005ea1',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
OK
|
||||
{isConfirm ? confirmLabel : 'OK'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "ARBEITSBEREICH",
|
||||
"newWorkspace": "Neuer Arbeitsbereich (löscht Boards, Komponenten, Leitungen und Dateien)",
|
||||
"confirmNew": {
|
||||
"title": "Neuen Arbeitsbereich starten?",
|
||||
"message": "Dies löscht alle Boards, Komponenten, Leitungen und Dateien. Dies kann nicht rückgängig gemacht werden.",
|
||||
"confirm": "Neu starten",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "Projekt laden?",
|
||||
"message": "Dieses {{type}}-Projekt laden? Dein aktueller Arbeitsbereich wird ersetzt. Dies kann nicht rückgängig gemacht werden.",
|
||||
"confirm": "Laden"
|
||||
},
|
||||
"saveProject": "Projekt speichern (Strg+S)",
|
||||
"clickToEdit": "zum Bearbeiten klicken",
|
||||
"collapse": "Einklappen",
|
||||
|
|
@ -267,6 +278,14 @@
|
|||
"rotate": "Drehen",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "Datei überschreiben?",
|
||||
"message": "Eine Datei namens \"{{name}}\" existiert bereits. Überschreiben?",
|
||||
"confirm": "Überschreiben",
|
||||
"cancel": "Überspringen"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "Verbunden",
|
||||
"starting": "Starte…",
|
||||
|
|
@ -286,6 +305,12 @@
|
|||
"offlineNote1": "Hinweis: Der Dateiexplorer zeigt einen Staging-Bereich an.",
|
||||
"offlineNote2": "„Auf den Pi hochladen“ überträgt diese Dateien auf das laufende System und startet den Pi bei Bedarf automatisch.",
|
||||
"loadingTerminal": "Terminal wird geladen…",
|
||||
"confirmDelete": {
|
||||
"title": "Löschen",
|
||||
"message": "\"{{name}}\" löschen?",
|
||||
"confirm": "Löschen",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"selectFile": "Wählen Sie eine Datei aus dem Explorer aus, um sie zu bearbeiten."
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "WORKSPACE",
|
||||
"newWorkspace": "New workspace (clears boards, components, wires and files)",
|
||||
"confirmNew": {
|
||||
"title": "Start a new workspace?",
|
||||
"message": "This clears every board, component, wire and file. This cannot be undone.",
|
||||
"confirm": "Start new",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "Load project?",
|
||||
"message": "Load this {{type}} project? Your current workspace will be replaced. This cannot be undone.",
|
||||
"confirm": "Load"
|
||||
},
|
||||
"saveProject": "Save project (Ctrl+S)",
|
||||
"clickToEdit": "click to edit",
|
||||
"collapse": "Collapse",
|
||||
|
|
@ -269,6 +280,14 @@
|
|||
"rotate": "Rotate",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "Overwrite file?",
|
||||
"message": "A file named \"{{name}}\" already exists. Overwrite?",
|
||||
"confirm": "Overwrite",
|
||||
"cancel": "Skip"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "Connected",
|
||||
"starting": "Starting…",
|
||||
|
|
@ -288,6 +307,12 @@
|
|||
"offlineNote1": "Note: The file explorer shows a staging area.",
|
||||
"offlineNote2": "\"Upload to Pi\" sends these files to the running system, starting the Pi automatically if needed.",
|
||||
"loadingTerminal": "Loading terminal…",
|
||||
"confirmDelete": {
|
||||
"title": "Delete",
|
||||
"message": "Delete \"{{name}}\"?",
|
||||
"confirm": "Delete",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"selectFile": "Select a file from the explorer to edit it."
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "ESPACIO DE TRABAJO",
|
||||
"newWorkspace": "Nuevo espacio de trabajo (borra placas, componentes, cables y archivos)",
|
||||
"confirmNew": {
|
||||
"title": "¿Iniciar un nuevo espacio de trabajo?",
|
||||
"message": "Esto borra todas las placas, componentes, cables y archivos. No se puede deshacer.",
|
||||
"confirm": "Empezar de nuevo",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "¿Cargar proyecto?",
|
||||
"message": "¿Cargar este proyecto {{type}}? Tu espacio de trabajo actual será reemplazado. No se puede deshacer.",
|
||||
"confirm": "Cargar"
|
||||
},
|
||||
"saveProject": "Guardar proyecto (Ctrl+S)",
|
||||
"clickToEdit": "clic para editar",
|
||||
"collapse": "Contraer",
|
||||
|
|
@ -269,6 +280,14 @@
|
|||
"rotate": "Rotar",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "¿Sobrescribir archivo?",
|
||||
"message": "Ya existe un archivo llamado \"{{name}}\". ¿Sobrescribir?",
|
||||
"confirm": "Sobrescribir",
|
||||
"cancel": "Omitir"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "Conectado",
|
||||
"starting": "Iniciando…",
|
||||
|
|
@ -288,6 +307,12 @@
|
|||
"offlineNote1": "Nota: El explorador de archivos muestra un área de preparación.",
|
||||
"offlineNote2": "\"Subir al Pi\" envía estos archivos al sistema en marcha; arranca el Pi automáticamente si hace falta.",
|
||||
"loadingTerminal": "Cargando terminal…",
|
||||
"confirmDelete": {
|
||||
"title": "Eliminar",
|
||||
"message": "¿Eliminar \"{{name}}\"?",
|
||||
"confirm": "Eliminar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"selectFile": "Selecciona un archivo del explorador para editarlo."
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "ESPACE DE TRAVAIL",
|
||||
"newWorkspace": "Nouvel espace de travail (efface cartes, composants, fils et fichiers)",
|
||||
"confirmNew": {
|
||||
"title": "Démarrer un nouvel espace de travail ?",
|
||||
"message": "Ceci efface toutes les cartes, composants, fils et fichiers. Cette action est irréversible.",
|
||||
"confirm": "Recommencer",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "Charger le projet ?",
|
||||
"message": "Charger ce projet {{type}} ? Votre espace de travail actuel sera remplacé. Cette action est irréversible.",
|
||||
"confirm": "Charger"
|
||||
},
|
||||
"saveProject": "Enregistrer le projet (Ctrl+S)",
|
||||
"clickToEdit": "cliquer pour éditer",
|
||||
"collapse": "Réduire",
|
||||
|
|
@ -267,6 +278,14 @@
|
|||
"rotate": "Pivoter",
|
||||
"delete": "Supprimer"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "Écraser le fichier ?",
|
||||
"message": "Un fichier nommé « {{name}} » existe déjà. L'écraser ?",
|
||||
"confirm": "Écraser",
|
||||
"cancel": "Ignorer"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "Connecté",
|
||||
"starting": "Démarrage…",
|
||||
|
|
@ -286,6 +305,12 @@
|
|||
"offlineNote1": "Remarque : L'explorateur de fichiers affiche une zone de préparation.",
|
||||
"offlineNote2": "« Envoyer vers le Pi » transfère ces fichiers vers le système en cours d'exécution et démarre le Pi automatiquement si nécessaire.",
|
||||
"loadingTerminal": "Chargement du terminal…",
|
||||
"confirmDelete": {
|
||||
"title": "Supprimer",
|
||||
"message": "Supprimer « {{name}} » ?",
|
||||
"confirm": "Supprimer",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"selectFile": "Sélectionnez un fichier dans l'explorateur pour l'éditer."
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "WORKSPACE",
|
||||
"newWorkspace": "Nuovo workspace (cancella schede, componenti, cavi e file)",
|
||||
"confirmNew": {
|
||||
"title": "Avviare un nuovo workspace?",
|
||||
"message": "Questo cancella tutte le schede, i componenti, i cavi e i file. Non può essere annullato.",
|
||||
"confirm": "Ricomincia",
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "Caricare il progetto?",
|
||||
"message": "Caricare questo progetto {{type}}? Il tuo workspace attuale sarà sostituito. Non può essere annullato.",
|
||||
"confirm": "Carica"
|
||||
},
|
||||
"saveProject": "Salva progetto (Ctrl+S)",
|
||||
"clickToEdit": "clicca per modificare",
|
||||
"collapse": "Comprimi",
|
||||
|
|
@ -267,6 +278,14 @@
|
|||
"rotate": "Ruota",
|
||||
"delete": "Elimina"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "Sovrascrivere il file?",
|
||||
"message": "Esiste già un file di nome \"{{name}}\". Sovrascrivere?",
|
||||
"confirm": "Sovrascrivi",
|
||||
"cancel": "Salta"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "Connesso",
|
||||
"starting": "Avvio…",
|
||||
|
|
@ -286,6 +305,12 @@
|
|||
"offlineNote1": "Nota: L'esplora file mostra un'area di staging.",
|
||||
"offlineNote2": "\"Carica sul Pi\" invia questi file al sistema in esecuzione e avvia il Pi automaticamente se necessario.",
|
||||
"loadingTerminal": "Caricamento terminale…",
|
||||
"confirmDelete": {
|
||||
"title": "Elimina",
|
||||
"message": "Eliminare \"{{name}}\"?",
|
||||
"confirm": "Elimina",
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"selectFile": "Seleziona un file dall'esplora per modificarlo."
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "ワークスペース",
|
||||
"newWorkspace": "新規ワークスペース (ボード、コンポーネント、配線、ファイルをクリア)",
|
||||
"confirmNew": {
|
||||
"title": "新しいワークスペースを開始しますか?",
|
||||
"message": "すべてのボード、コンポーネント、配線、ファイルが消去されます。この操作は元に戻せません。",
|
||||
"confirm": "新規開始",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "プロジェクトを読み込みますか?",
|
||||
"message": "この {{type}} プロジェクトを読み込みますか?現在のワークスペースが置き換えられます。この操作は元に戻せません。",
|
||||
"confirm": "読み込む"
|
||||
},
|
||||
"saveProject": "プロジェクトを保存 (Ctrl+S)",
|
||||
"clickToEdit": "クリックして編集",
|
||||
"collapse": "折りたたむ",
|
||||
|
|
@ -267,6 +278,14 @@
|
|||
"rotate": "回転",
|
||||
"delete": "削除"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "ファイルを上書きしますか?",
|
||||
"message": "\"{{name}}\" という名前のファイルは既に存在します。上書きしますか?",
|
||||
"confirm": "上書き",
|
||||
"cancel": "スキップ"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "接続済み",
|
||||
"starting": "起動中…",
|
||||
|
|
@ -286,6 +305,12 @@
|
|||
"offlineNote1": "注: ファイルエクスプローラはステージング領域を表示します。",
|
||||
"offlineNote2": "「Pi にアップロード」は実行中のシステムにこれらのファイルを転送します。必要に応じて Pi を自動的に起動します。",
|
||||
"loadingTerminal": "端末を読み込み中…",
|
||||
"confirmDelete": {
|
||||
"title": "削除",
|
||||
"message": "\"{{name}}\" を削除しますか?",
|
||||
"confirm": "削除",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"selectFile": "エクスプローラからファイルを選択して編集します。"
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "WORKSPACE",
|
||||
"newWorkspace": "Novo workspace (limpa placas, componentes, fios e arquivos)",
|
||||
"confirmNew": {
|
||||
"title": "Iniciar um novo workspace?",
|
||||
"message": "Isso limpa todas as placas, componentes, fios e arquivos. Não pode ser desfeito.",
|
||||
"confirm": "Começar novo",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "Carregar projeto?",
|
||||
"message": "Carregar este projeto {{type}}? Seu workspace atual será substituído. Não pode ser desfeito.",
|
||||
"confirm": "Carregar"
|
||||
},
|
||||
"saveProject": "Salvar projeto (Ctrl+S)",
|
||||
"clickToEdit": "clique para editar",
|
||||
"collapse": "Recolher",
|
||||
|
|
@ -267,6 +278,14 @@
|
|||
"rotate": "Girar",
|
||||
"delete": "Excluir"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "Substituir arquivo?",
|
||||
"message": "Já existe um arquivo chamado \"{{name}}\". Substituir?",
|
||||
"confirm": "Substituir",
|
||||
"cancel": "Ignorar"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "Conectado",
|
||||
"starting": "Iniciando…",
|
||||
|
|
@ -286,6 +305,12 @@
|
|||
"offlineNote1": "Nota: O explorador de arquivos mostra uma área de preparação.",
|
||||
"offlineNote2": "\"Enviar para o Pi\" transfere esses arquivos para o sistema em execução e inicia o Pi automaticamente se necessário.",
|
||||
"loadingTerminal": "Carregando terminal…",
|
||||
"confirmDelete": {
|
||||
"title": "Excluir",
|
||||
"message": "Excluir \"{{name}}\"?",
|
||||
"confirm": "Excluir",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"selectFile": "Selecione um arquivo no explorador para editá-lo."
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "РАБОЧАЯ ОБЛАСТЬ",
|
||||
"newWorkspace": "Новая рабочая область (очищает платы, компоненты, провода и файлы)",
|
||||
"confirmNew": {
|
||||
"title": "Начать новую рабочую область?",
|
||||
"message": "Это очистит все платы, компоненты, провода и файлы. Это действие нельзя отменить.",
|
||||
"confirm": "Начать заново",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "Загрузить проект?",
|
||||
"message": "Загрузить этот проект {{type}}? Ваша текущая рабочая область будет заменена. Это действие нельзя отменить.",
|
||||
"confirm": "Загрузить"
|
||||
},
|
||||
"saveProject": "Сохранить проект (Ctrl+S)",
|
||||
"clickToEdit": "нажмите для редактирования",
|
||||
"collapse": "Свернуть",
|
||||
|
|
@ -267,6 +278,14 @@
|
|||
"rotate": "Повернуть",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "Перезаписать файл?",
|
||||
"message": "Файл с именем \"{{name}}\" уже существует. Перезаписать?",
|
||||
"confirm": "Перезаписать",
|
||||
"cancel": "Пропустить"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "Подключено",
|
||||
"starting": "Запуск…",
|
||||
|
|
@ -286,6 +305,12 @@
|
|||
"offlineNote1": "Примечание: Проводник файлов показывает область подготовки.",
|
||||
"offlineNote2": "«Загрузить на Pi» отправляет эти файлы в работающую систему и при необходимости запускает Pi автоматически.",
|
||||
"loadingTerminal": "Загрузка терминала…",
|
||||
"confirmDelete": {
|
||||
"title": "Удалить",
|
||||
"message": "Удалить \"{{name}}\"?",
|
||||
"confirm": "Удалить",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"selectFile": "Выберите файл в проводнике для редактирования."
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@
|
|||
"fileExplorer": {
|
||||
"workspace": "工作区",
|
||||
"newWorkspace": "新建工作区 (清除板、元件、导线和文件)",
|
||||
"confirmNew": {
|
||||
"title": "开始新的工作区?",
|
||||
"message": "这将清除所有板、元件、导线和文件。此操作无法撤销。",
|
||||
"confirm": "新建",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"confirmLoad": {
|
||||
"title": "加载项目?",
|
||||
"message": "加载此 {{type}} 项目?您当前的工作区将被替换。此操作无法撤销。",
|
||||
"confirm": "加载"
|
||||
},
|
||||
"saveProject": "保存项目 (Ctrl+S)",
|
||||
"clickToEdit": "点击编辑",
|
||||
"collapse": "折叠",
|
||||
|
|
@ -267,6 +278,14 @@
|
|||
"rotate": "旋转",
|
||||
"delete": "删除"
|
||||
},
|
||||
"boardOptions": {
|
||||
"confirmOverwrite": {
|
||||
"title": "覆盖文件?",
|
||||
"message": "已存在名为 \"{{name}}\" 的文件。是否覆盖?",
|
||||
"confirm": "覆盖",
|
||||
"cancel": "跳过"
|
||||
}
|
||||
},
|
||||
"pi": {
|
||||
"connected": "已连接",
|
||||
"starting": "启动中…",
|
||||
|
|
@ -286,6 +305,12 @@
|
|||
"offlineNote1": "注意:文件浏览器显示暂存区域。",
|
||||
"offlineNote2": "“上传到 Pi”会将这些文件传输到正在运行的系统,并在需要时自动启动 Pi。",
|
||||
"loadingTerminal": "加载终端中…",
|
||||
"confirmDelete": {
|
||||
"title": "删除",
|
||||
"message": "删除 \"{{name}}\"?",
|
||||
"confirm": "删除",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"selectFile": "从浏览器中选择一个文件进行编辑。"
|
||||
},
|
||||
"sensorPanel": {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { useEditorStore } from '../store/useEditorStore';
|
|||
import { useCompileLogsStore } from '../store/useCompileLogsStore';
|
||||
import { useOscilloscopeStore } from '../store/useOscilloscopeStore';
|
||||
import { useProjectStore } from '../store/useProjectStore';
|
||||
import { showConfirmDialog } from '../store/useMessageDialogStore';
|
||||
import { useAutoSaveProject } from '../hooks/useAutoSaveProject';
|
||||
import type { CompilationLog } from '../utils/compilationLogger';
|
||||
import { isPiBoardKind } from '../types/board';
|
||||
|
|
@ -195,14 +196,18 @@ export const EditorPage: React.FC = () => {
|
|||
triggerSaveAction();
|
||||
}, []);
|
||||
|
||||
const handleNewClick = useCallback(() => {
|
||||
if (
|
||||
!window.confirm(
|
||||
'Start a new workspace? This clears every board, component, wire and file. This cannot be undone.',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const handleNewClick = useCallback(async () => {
|
||||
const confirmed = await showConfirmDialog(
|
||||
t('editor.fileExplorer.confirmNew.message'),
|
||||
{
|
||||
kind: 'error',
|
||||
title: t('editor.fileExplorer.confirmNew.title'),
|
||||
confirmLabel: t('editor.fileExplorer.confirmNew.confirm'),
|
||||
cancelLabel: t('editor.fileExplorer.confirmNew.cancel'),
|
||||
danger: true,
|
||||
},
|
||||
);
|
||||
if (!confirmed) return;
|
||||
const sim = useSimulatorStore.getState();
|
||||
sim.boards.forEach((b) => sim.stopBoard(b.id));
|
||||
const ids = sim.boards.map((b) => b.id);
|
||||
|
|
@ -214,7 +219,7 @@ export const EditorPage: React.FC = () => {
|
|||
.getState()
|
||||
.addBoard('arduino-uno', DEFAULT_BOARD_POSITION.x, DEFAULT_BOARD_POSITION.y);
|
||||
useSimulatorStore.getState().setActiveBoardId(newId);
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
// Track mobile breakpoint
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -14,37 +14,97 @@ import { create } from 'zustand';
|
|||
|
||||
export type MessageDialogKind = 'info' | 'success' | 'error';
|
||||
|
||||
/** 'alert' shows a single OK button; 'confirm' shows Cancel + Confirm. */
|
||||
export type MessageDialogMode = 'alert' | 'confirm';
|
||||
|
||||
export interface MessageDialogOptions {
|
||||
kind?: MessageDialogKind;
|
||||
/** Optional header line. Callers pass an already-translated string. */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ConfirmDialogOptions extends MessageDialogOptions {
|
||||
/** Label for the confirming button. Callers pass an already-translated string. */
|
||||
confirmLabel?: string;
|
||||
/** Label for the dismissing button. Callers pass an already-translated string. */
|
||||
cancelLabel?: string;
|
||||
/** Style the confirm button as a destructive action (red). */
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
interface MessageDialogState {
|
||||
open: boolean;
|
||||
mode: MessageDialogMode;
|
||||
kind: MessageDialogKind;
|
||||
title: string | null;
|
||||
message: string;
|
||||
confirmLabel: string;
|
||||
cancelLabel: string;
|
||||
danger: boolean;
|
||||
/** Set while a confirm dialog is open; called with the user's choice. */
|
||||
resolve: ((confirmed: boolean) => void) | null;
|
||||
show: (message: string, opts?: MessageDialogOptions) => void;
|
||||
close: () => void;
|
||||
confirm: (message: string, opts?: ConfirmDialogOptions) => Promise<boolean>;
|
||||
/** Resolve/close. `result` is only meaningful for confirm dialogs. */
|
||||
close: (result?: boolean) => void;
|
||||
}
|
||||
|
||||
export const useMessageDialogStore = create<MessageDialogState>((set) => ({
|
||||
export const useMessageDialogStore = create<MessageDialogState>((set, get) => ({
|
||||
open: false,
|
||||
mode: 'alert',
|
||||
kind: 'info',
|
||||
title: null,
|
||||
message: '',
|
||||
show: (message, opts) =>
|
||||
confirmLabel: 'OK',
|
||||
cancelLabel: 'Cancel',
|
||||
danger: false,
|
||||
resolve: null,
|
||||
show: (message, opts) => {
|
||||
// Reject any in-flight confirm before replacing it with an alert.
|
||||
get().resolve?.(false);
|
||||
set({
|
||||
open: true,
|
||||
mode: 'alert',
|
||||
message,
|
||||
kind: opts?.kind ?? 'info',
|
||||
title: opts?.title ?? null,
|
||||
resolve: null,
|
||||
});
|
||||
},
|
||||
confirm: (message, opts) =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
// Reject any in-flight confirm before replacing it.
|
||||
get().resolve?.(false);
|
||||
set({
|
||||
open: true,
|
||||
mode: 'confirm',
|
||||
message,
|
||||
kind: opts?.kind ?? 'info',
|
||||
title: opts?.title ?? null,
|
||||
confirmLabel: opts?.confirmLabel ?? 'OK',
|
||||
cancelLabel: opts?.cancelLabel ?? 'Cancel',
|
||||
danger: opts?.danger ?? false,
|
||||
resolve,
|
||||
});
|
||||
}),
|
||||
close: () => set({ open: false }),
|
||||
close: (result = false) => {
|
||||
get().resolve?.(result);
|
||||
set({ open: false, resolve: null });
|
||||
},
|
||||
}));
|
||||
|
||||
/** Imperative helper so non-React callers don't need to know zustand. */
|
||||
export function showMessageDialog(message: string, opts?: MessageDialogOptions): void {
|
||||
useMessageDialogStore.getState().show(message, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* In-app replacement for window.confirm(). Returns a promise resolving to
|
||||
* true when the user confirms, false when they cancel/dismiss.
|
||||
*/
|
||||
export function showConfirmDialog(
|
||||
message: string,
|
||||
opts?: ConfirmDialogOptions,
|
||||
): Promise<boolean> {
|
||||
return useMessageDialogStore.getState().confirm(message, opts);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue