From 7cb47e0aa5edfb6b19c2287bbebc5ab668430f79 Mon Sep 17 00:00:00 2001 From: hugo Date: Sun, 22 Feb 2026 22:39:26 +0100 Subject: [PATCH] feat: more phase 1 implementation - proper parity now --- src/main/engine/ScriptEngine.ts | 14 +- .../components/ScriptsView/ScriptsView.css | 60 ++-- .../components/ScriptsView/ScriptsView.tsx | 299 +++++++++++++++++- src/renderer/components/Sidebar/Sidebar.tsx | 89 +++++- src/renderer/i18n/locales/de.json | 10 + src/renderer/i18n/locales/en.json | 10 + src/renderer/i18n/locales/es.json | 10 + src/renderer/i18n/locales/fr.json | 10 + src/renderer/i18n/locales/it.json | 10 + tests/engine/ScriptEngine.test.ts | 31 +- .../components/ScriptsView.styles.test.ts | 2 +- .../renderer/components/ScriptsView.test.tsx | 120 +++++++ .../components/SidebarScripts.test.tsx | 85 ++++- 13 files changed, 694 insertions(+), 56 deletions(-) diff --git a/src/main/engine/ScriptEngine.ts b/src/main/engine/ScriptEngine.ts index 63d3b65..0be90ab 100644 --- a/src/main/engine/ScriptEngine.ts +++ b/src/main/engine/ScriptEngine.ts @@ -94,7 +94,11 @@ export class ScriptEngine extends EventEmitter { } const allScripts = await this.getAllScriptRows(); - const desiredSlug = this.normalizeSlug(updates.slug || updates.title || existing.slug); + const desiredSlug = typeof updates.slug === 'string' + ? this.normalizeSlug(updates.slug) + : typeof updates.title === 'string' + ? this.normalizeSlug(updates.title) + : existing.slug; const nextSlug = this.ensureUniqueSlug(desiredSlug, allScripts, existing.id); const nextFilePath = this.getScriptFilePath(nextSlug); const now = new Date(); @@ -228,8 +232,8 @@ export class ScriptEngine extends EventEmitter { private normalizeSlug(value: string): string { const normalized = value .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, ''); + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_+|_+$/g, ''); return normalized || 'script'; } @@ -246,11 +250,11 @@ export class ScriptEngine extends EventEmitter { } let suffix = 2; - while (taken.has(`${baseSlug}-${suffix}`)) { + while (taken.has(`${baseSlug}_${suffix}`)) { suffix += 1; } - return `${baseSlug}-${suffix}`; + return `${baseSlug}_${suffix}`; } } diff --git a/src/renderer/components/ScriptsView/ScriptsView.css b/src/renderer/components/ScriptsView/ScriptsView.css index 6abfcf2..5f89456 100644 --- a/src/renderer/components/ScriptsView/ScriptsView.css +++ b/src/renderer/components/ScriptsView/ScriptsView.css @@ -1,32 +1,54 @@ -.scripts-view { +.scripts-view-shell { display: flex; flex: 1; + flex-direction: column; min-height: 0; - width: 100%; } -.scripts-editor { - display: flex; - flex-direction: column; +.scripts-view { flex: 1; min-height: 0; - width: 100%; - padding: 10px; +} + +.scripts-meta-row { + margin-bottom: 8px; +} + +.scripts-enabled-field { + align-self: flex-end; +} + +.scripts-enabled-field label { + display: flex; + align-items: center; gap: 8px; } -.scripts-toolbar { - display: flex; - justify-content: flex-end; -} - -.scripts-label { - font-size: 12px; - color: var(--vscode-descriptionForeground); -} - -.scripts-textarea { - width: 100%; +.scripts-editor { flex: 1; min-height: 0; } + +.scripts-toolbar { + margin-bottom: 8px; +} + +.scripts-monaco { + flex: 1; + min-height: 0; + border-radius: 4px; + overflow: hidden; + background-color: var(--vscode-input-background); +} + +.scripts-run-button { + padding: 4px 12px; + font-size: 12px; + border-radius: 4px; +} + +.scripts-save-button { + padding: 4px 12px; + font-size: 12px; + border-radius: 4px; +} diff --git a/src/renderer/components/ScriptsView/ScriptsView.tsx b/src/renderer/components/ScriptsView/ScriptsView.tsx index 279a594..dd2ec7f 100644 --- a/src/renderer/components/ScriptsView/ScriptsView.tsx +++ b/src/renderer/components/ScriptsView/ScriptsView.tsx @@ -1,20 +1,46 @@ import React, { useEffect, useState } from 'react'; +import MonacoEditor from '@monaco-editor/react'; import type { ScriptData } from '../../../main/shared/electronApi'; import { useAppStore } from '../../store'; import { getPythonRuntimeManager } from '../../python/runtimeManagerInstance'; import { useI18n } from '../../i18n'; +import { showToast } from '../Toast'; import './ScriptsView.css'; +const UI_DATE_LOCALE: Record = { + en: 'en-US', + de: 'de-DE', + fr: 'fr-FR', + it: 'it-IT', + es: 'es-ES', +}; + interface ScriptsViewProps { scriptId: string | null; } export const ScriptsView: React.FC = ({ scriptId }) => { - const { t } = useI18n(); + const { t, language } = useI18n(); const appendPanelOutputEntry = useAppStore((state) => state.appendPanelOutputEntry); + const closeTab = useAppStore((state) => state.closeTab); const [script, setScript] = useState(null); + const [title, setTitle] = useState(''); + const [slug, setSlug] = useState(''); + const [kind, setKind] = useState('utility'); + const [entrypoint, setEntrypoint] = useState('render'); + const [enabled, setEnabled] = useState(true); const [scriptContent, setScriptContent] = useState(''); + const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false); const [isRunning, setIsRunning] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + const toFunctionSlug = (value: string) => { + const normalized = value + .toLowerCase() + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_+|_+$/g, ''); + return normalized || 'script'; + }; useEffect(() => { let cancelled = false; @@ -22,19 +48,38 @@ export const ScriptsView: React.FC = ({ scriptId }) => { const loadScript = async () => { if (!scriptId) { setScript(null); + setTitle(''); + setSlug(''); + setKind('utility'); + setEntrypoint('render'); + setEnabled(true); setScriptContent(''); + setIsSlugManuallyEdited(false); return; } const item = await window.electronAPI?.scripts.get(scriptId); if (cancelled || !item) { setScript(null); + setTitle(''); + setSlug(''); + setKind('utility'); + setEntrypoint('render'); + setEnabled(true); setScriptContent(''); + setIsSlugManuallyEdited(false); return; } setScript(item); + setTitle(item.title || ''); + setSlug(toFunctionSlug(item.slug || item.title || '')); + setKind(item.kind || 'utility'); + setEntrypoint(item.entrypoint || 'render'); + setEnabled(item.enabled ?? true); setScriptContent(item.content || ''); + const normalizedExisting = toFunctionSlug(item.slug || item.title || ''); + setIsSlugManuallyEdited(normalizedExisting !== toFunctionSlug(item.title || '')); }; void loadScript(); @@ -44,6 +89,101 @@ export const ScriptsView: React.FC = ({ scriptId }) => { }; }, [scriptId]); + const hasChanges = !!script && ( + title !== script.title || + slug !== script.slug || + kind !== script.kind || + entrypoint !== script.entrypoint || + enabled !== script.enabled || + scriptContent !== script.content + ); + + const handleTitleChange = (nextTitle: string) => { + setTitle(nextTitle); + if (!isSlugManuallyEdited) { + setSlug(toFunctionSlug(nextTitle)); + } + }; + + const handleSlugChange = (nextSlug: string) => { + setIsSlugManuallyEdited(true); + setSlug(toFunctionSlug(nextSlug)); + }; + + const handleSaveScript = async () => { + if (!script || isSaving || !hasChanges) { + return; + } + + setIsSaving(true); + try { + const updated = await window.electronAPI?.scripts.update(script.id, { + title, + slug, + kind, + entrypoint, + enabled, + content: scriptContent, + }); + + if (!updated) { + return; + } + + setScript(updated); + setTitle(updated.title || ''); + setSlug(toFunctionSlug(updated.slug || updated.title || '')); + setKind(updated.kind || 'utility'); + setEntrypoint(updated.entrypoint || 'render'); + setEnabled(updated.enabled ?? true); + setScriptContent(updated.content || ''); + const normalizedExisting = toFunctionSlug(updated.slug || updated.title || ''); + setIsSlugManuallyEdited(normalizedExisting !== toFunctionSlug(updated.title || '')); + if (typeof window.dispatchEvent === 'function') { + window.dispatchEvent(new CustomEvent('bds:scripts-changed')); + } + } finally { + setIsSaving(false); + } + }; + + const handleDeleteScript = async () => { + if (!script) { + return; + } + + try { + const deleted = await window.electronAPI?.scripts.delete(script.id); + if (!deleted) { + showToast.error(t('sidebar.scripts.deleteFailed')); + return; + } + closeTab(script.id); + if (typeof window.dispatchEvent === 'function') { + window.dispatchEvent(new CustomEvent('bds:scripts-changed')); + } + } catch (error) { + console.error('Failed to delete script:', error); + showToast.error(t('sidebar.scripts.deleteFailed')); + } + }; + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.ctrlKey || event.metaKey) && event.key === 's') { + event.preventDefault(); + void handleSaveScript(); + } + }; + + if (typeof window.addEventListener !== 'function' || typeof window.removeEventListener !== 'function') { + return; + } + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [handleSaveScript]); + const handleRunScript = async () => { if (!script || isRunning) { return; @@ -90,24 +230,153 @@ export const ScriptsView: React.FC = ({ scriptId }) => { }; return ( -
-
-
- + + +
+
+ +
+
+
+
+
+ + handleTitleChange(event.target.value)} + disabled={!script} + /> +
+
+ + handleSlugChange(event.target.value)} + disabled={!script} + /> +
+
+
+
+ + +
+
+ + setEntrypoint(event.target.value)} + disabled={!script} + /> +
+
+ +
+
+
+
+ +
+
+
+ +
+
+
- -