feat: more phase 1 implementation - proper parity now
This commit is contained in:
@@ -94,7 +94,11 @@ export class ScriptEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allScripts = await this.getAllScriptRows();
|
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 nextSlug = this.ensureUniqueSlug(desiredSlug, allScripts, existing.id);
|
||||||
const nextFilePath = this.getScriptFilePath(nextSlug);
|
const nextFilePath = this.getScriptFilePath(nextSlug);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -228,8 +232,8 @@ export class ScriptEngine extends EventEmitter {
|
|||||||
private normalizeSlug(value: string): string {
|
private normalizeSlug(value: string): string {
|
||||||
const normalized = value
|
const normalized = value
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
.replace(/^-|-$/g, '');
|
.replace(/^_+|_+$/g, '');
|
||||||
return normalized || 'script';
|
return normalized || 'script';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,11 +250,11 @@ export class ScriptEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let suffix = 2;
|
let suffix = 2;
|
||||||
while (taken.has(`${baseSlug}-${suffix}`)) {
|
while (taken.has(`${baseSlug}_${suffix}`)) {
|
||||||
suffix += 1;
|
suffix += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${baseSlug}-${suffix}`;
|
return `${baseSlug}_${suffix}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,54 @@
|
|||||||
.scripts-view {
|
.scripts-view-shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scripts-editor {
|
.scripts-view {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
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;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scripts-toolbar {
|
.scripts-editor {
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scripts-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--vscode-descriptionForeground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scripts-textarea {
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,46 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import MonacoEditor from '@monaco-editor/react';
|
||||||
import type { ScriptData } from '../../../main/shared/electronApi';
|
import type { ScriptData } from '../../../main/shared/electronApi';
|
||||||
import { useAppStore } from '../../store';
|
import { useAppStore } from '../../store';
|
||||||
import { getPythonRuntimeManager } from '../../python/runtimeManagerInstance';
|
import { getPythonRuntimeManager } from '../../python/runtimeManagerInstance';
|
||||||
import { useI18n } from '../../i18n';
|
import { useI18n } from '../../i18n';
|
||||||
|
import { showToast } from '../Toast';
|
||||||
import './ScriptsView.css';
|
import './ScriptsView.css';
|
||||||
|
|
||||||
|
const UI_DATE_LOCALE: Record<string, string> = {
|
||||||
|
en: 'en-US',
|
||||||
|
de: 'de-DE',
|
||||||
|
fr: 'fr-FR',
|
||||||
|
it: 'it-IT',
|
||||||
|
es: 'es-ES',
|
||||||
|
};
|
||||||
|
|
||||||
interface ScriptsViewProps {
|
interface ScriptsViewProps {
|
||||||
scriptId: string | null;
|
scriptId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
||||||
const { t } = useI18n();
|
const { t, language } = useI18n();
|
||||||
const appendPanelOutputEntry = useAppStore((state) => state.appendPanelOutputEntry);
|
const appendPanelOutputEntry = useAppStore((state) => state.appendPanelOutputEntry);
|
||||||
|
const closeTab = useAppStore((state) => state.closeTab);
|
||||||
const [script, setScript] = useState<ScriptData | null>(null);
|
const [script, setScript] = useState<ScriptData | null>(null);
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [slug, setSlug] = useState('');
|
||||||
|
const [kind, setKind] = useState<ScriptData['kind']>('utility');
|
||||||
|
const [entrypoint, setEntrypoint] = useState('render');
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
const [scriptContent, setScriptContent] = useState('');
|
const [scriptContent, setScriptContent] = useState('');
|
||||||
|
const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false);
|
||||||
const [isRunning, setIsRunning] = 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(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -22,19 +48,38 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
|||||||
const loadScript = async () => {
|
const loadScript = async () => {
|
||||||
if (!scriptId) {
|
if (!scriptId) {
|
||||||
setScript(null);
|
setScript(null);
|
||||||
|
setTitle('');
|
||||||
|
setSlug('');
|
||||||
|
setKind('utility');
|
||||||
|
setEntrypoint('render');
|
||||||
|
setEnabled(true);
|
||||||
setScriptContent('');
|
setScriptContent('');
|
||||||
|
setIsSlugManuallyEdited(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = await window.electronAPI?.scripts.get(scriptId);
|
const item = await window.electronAPI?.scripts.get(scriptId);
|
||||||
if (cancelled || !item) {
|
if (cancelled || !item) {
|
||||||
setScript(null);
|
setScript(null);
|
||||||
|
setTitle('');
|
||||||
|
setSlug('');
|
||||||
|
setKind('utility');
|
||||||
|
setEntrypoint('render');
|
||||||
|
setEnabled(true);
|
||||||
setScriptContent('');
|
setScriptContent('');
|
||||||
|
setIsSlugManuallyEdited(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setScript(item);
|
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 || '');
|
setScriptContent(item.content || '');
|
||||||
|
const normalizedExisting = toFunctionSlug(item.slug || item.title || '');
|
||||||
|
setIsSlugManuallyEdited(normalizedExisting !== toFunctionSlug(item.title || ''));
|
||||||
};
|
};
|
||||||
|
|
||||||
void loadScript();
|
void loadScript();
|
||||||
@@ -44,6 +89,101 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
|||||||
};
|
};
|
||||||
}, [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 () => {
|
const handleRunScript = async () => {
|
||||||
if (!script || isRunning) {
|
if (!script || isRunning) {
|
||||||
return;
|
return;
|
||||||
@@ -90,24 +230,153 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scripts-view">
|
<div className="scripts-view-shell">
|
||||||
<div className="scripts-editor">
|
<div className="editor-header scripts-header">
|
||||||
<div className="scripts-toolbar">
|
<div className="editor-tabs">
|
||||||
<button type="button" onClick={handleRunScript} disabled={!script || isRunning}>
|
<div className="editor-tab active">
|
||||||
|
<span className="editor-tab-title">{title || t('editor.untitled')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="editor-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="scripts-save-button"
|
||||||
|
onClick={handleSaveScript}
|
||||||
|
disabled={!script || !hasChanges || isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? t('editor.saving') : t('scripts.save')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="scripts-run-button"
|
||||||
|
onClick={handleRunScript}
|
||||||
|
disabled={!script || isRunning}
|
||||||
|
>
|
||||||
{t('scripts.run')}
|
{t('scripts.run')}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary danger"
|
||||||
|
onClick={handleDeleteScript}
|
||||||
|
disabled={!script}
|
||||||
|
title={t('scripts.delete')}
|
||||||
|
>
|
||||||
|
{t('scripts.delete')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-content scripts-view">
|
||||||
|
<div className="editor-header-row scripts-meta-row">
|
||||||
|
<div className="editor-meta">
|
||||||
|
<div className="editor-field-row">
|
||||||
|
<div className="editor-field">
|
||||||
|
<label htmlFor="script-title">{t('editor.field.title')}</label>
|
||||||
|
<input
|
||||||
|
id="script-title"
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(event) => handleTitleChange(event.target.value)}
|
||||||
|
disabled={!script}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="editor-field">
|
||||||
|
<label htmlFor="script-slug">{t('editor.field.slug')}</label>
|
||||||
|
<input
|
||||||
|
id="script-slug"
|
||||||
|
type="text"
|
||||||
|
value={slug}
|
||||||
|
onChange={(event) => handleSlugChange(event.target.value)}
|
||||||
|
disabled={!script}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="editor-field-row">
|
||||||
|
<div className="editor-field">
|
||||||
|
<label htmlFor="script-kind">{t('scripts.field.kind')}</label>
|
||||||
|
<select
|
||||||
|
id="script-kind"
|
||||||
|
value={kind}
|
||||||
|
onChange={(event) => setKind(event.target.value as ScriptData['kind'])}
|
||||||
|
disabled={!script}
|
||||||
|
>
|
||||||
|
<option value="utility">{t('scripts.kind.utility')}</option>
|
||||||
|
<option value="macro">{t('scripts.kind.macro')}</option>
|
||||||
|
<option value="transform">{t('scripts.kind.transform')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="editor-field">
|
||||||
|
<label htmlFor="script-entrypoint">{t('scripts.field.entrypoint')}</label>
|
||||||
|
<input
|
||||||
|
id="script-entrypoint"
|
||||||
|
type="text"
|
||||||
|
value={entrypoint}
|
||||||
|
onChange={(event) => setEntrypoint(event.target.value)}
|
||||||
|
disabled={!script}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="editor-field scripts-enabled-field">
|
||||||
|
<label htmlFor="script-enabled">
|
||||||
|
<input
|
||||||
|
id="script-enabled"
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(event) => setEnabled(event.target.checked)}
|
||||||
|
disabled={!script}
|
||||||
|
/>
|
||||||
|
{t('scripts.field.enabled')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-body scripts-editor">
|
||||||
|
<div className="editor-toolbar scripts-toolbar">
|
||||||
|
<div className="editor-toolbar-left">
|
||||||
|
<label>{t('scripts.content')}</label>
|
||||||
|
</div>
|
||||||
|
<div className="editor-toolbar-center" />
|
||||||
|
<div className="editor-toolbar-right" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="scripts-label" htmlFor="scripts-content">
|
<div className="scripts-monaco">
|
||||||
{t('scripts.content')}
|
<MonacoEditor
|
||||||
</label>
|
height="100%"
|
||||||
<textarea
|
language="python"
|
||||||
id="scripts-content"
|
theme="vs-dark"
|
||||||
className="scripts-textarea"
|
value={scriptContent}
|
||||||
value={scriptContent}
|
onChange={(value) => setScriptContent(value || '')}
|
||||||
onChange={(event) => setScriptContent(event.target.value)}
|
options={{
|
||||||
disabled={!script}
|
minimap: { enabled: false },
|
||||||
/>
|
wordWrap: 'on',
|
||||||
|
lineNumbers: 'on',
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: "'Cascadia Code', 'Consolas', 'Courier New', monospace",
|
||||||
|
padding: { top: 12, bottom: 12 },
|
||||||
|
automaticLayout: true,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
renderLineHighlight: 'line',
|
||||||
|
formatOnPaste: true,
|
||||||
|
cursorStyle: 'line',
|
||||||
|
cursorBlinking: 'smooth',
|
||||||
|
readOnly: !script,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{script && (
|
||||||
|
<div className="editor-footer">
|
||||||
|
<span className="text-muted text-small">
|
||||||
|
{t('editor.footer.created')}: {new Date(script.createdAt).toLocaleString(UI_DATE_LOCALE[language] || UI_DATE_LOCALE.en)}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted text-small">
|
||||||
|
{t('editor.footer.updated')}: {new Date(script.updatedAt).toLocaleString(UI_DATE_LOCALE[language] || UI_DATE_LOCALE.en)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1667,9 +1667,9 @@ const ImportList: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ScriptsList: React.FC = () => {
|
const ScriptsList: React.FC = () => {
|
||||||
const { t } = useI18n();
|
const { t, language } = useI18n();
|
||||||
const { openTab, activeTabId } = useAppStore();
|
const { openTab, activeTabId, closeTab } = useAppStore();
|
||||||
const [scripts, setScripts] = useState<Array<{ id: string; title: string }>>([]);
|
const [scripts, setScripts] = useState<Array<{ id: string; title: string; updatedAt: string }>>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const loadScripts = useCallback(async () => {
|
const loadScripts = useCallback(async () => {
|
||||||
@@ -1678,7 +1678,7 @@ const ScriptsList: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setScripts(items.map((item) => ({ id: item.id, title: item.title })));
|
setScripts(items.map((item) => ({ id: item.id, title: item.title, updatedAt: item.updatedAt })));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1692,7 +1692,7 @@ const ScriptsList: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setScripts((items ?? []).map((item) => ({ id: item.id, title: item.title })));
|
setScripts((items ?? []).map((item) => ({ id: item.id, title: item.title, updatedAt: item.updatedAt })));
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -1702,10 +1702,22 @@ const ScriptsList: React.FC = () => {
|
|||||||
|
|
||||||
void loadInitialScripts();
|
void loadInitialScripts();
|
||||||
|
|
||||||
|
const canListen = typeof window.addEventListener === 'function' && typeof window.removeEventListener === 'function';
|
||||||
|
const handleScriptsChanged = () => {
|
||||||
|
void loadScripts();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (canListen) {
|
||||||
|
window.addEventListener('bds:scripts-changed', handleScriptsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
if (canListen) {
|
||||||
|
window.removeEventListener('bds:scripts-changed', handleScriptsChanged);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [loadScripts]);
|
||||||
|
|
||||||
const handleCreateScript = async () => {
|
const handleCreateScript = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -1722,9 +1734,12 @@ const ScriptsList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setScripts((prev) => [
|
setScripts((prev) => [
|
||||||
{ id: created.id, title: created.title },
|
{ id: created.id, title: created.title, updatedAt: created.updatedAt },
|
||||||
...prev.filter((script) => script.id !== created.id),
|
...prev.filter((script) => script.id !== created.id),
|
||||||
]);
|
]);
|
||||||
|
if (typeof window.dispatchEvent === 'function') {
|
||||||
|
window.dispatchEvent(new CustomEvent('bds:scripts-changed'));
|
||||||
|
}
|
||||||
openScriptTab(openTab, created.id, 'pin');
|
openScriptTab(openTab, created.id, 'pin');
|
||||||
void loadScripts();
|
void loadScripts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1733,6 +1748,40 @@ const ScriptsList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const now = new Date();
|
||||||
|
const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
const uiDateLocale = UI_DATE_LOCALE[language] || UI_DATE_LOCALE.en;
|
||||||
|
if (diffDays === 0) {
|
||||||
|
return date.toLocaleTimeString(uiDateLocale, { hour: 'numeric', minute: '2-digit' });
|
||||||
|
} else if (diffDays === 1) {
|
||||||
|
return t('sidebar.chat.yesterday');
|
||||||
|
} else if (diffDays < 7) {
|
||||||
|
return date.toLocaleDateString(uiDateLocale, { weekday: 'short' });
|
||||||
|
}
|
||||||
|
return date.toLocaleDateString(uiDateLocale, { month: 'short', day: 'numeric' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteScript = async (event: React.MouseEvent, scriptId: string) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
|
const deleted = await window.electronAPI?.scripts.delete(scriptId);
|
||||||
|
if (!deleted) {
|
||||||
|
showToast.error(t('sidebar.scripts.deleteFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setScripts((prev) => prev.filter((script) => script.id !== scriptId));
|
||||||
|
closeTab(scriptId);
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="chat-list">
|
<div className="chat-list">
|
||||||
@@ -1767,17 +1816,37 @@ const ScriptsList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
scripts.map((script) => (
|
scripts.map((script) => (
|
||||||
<button
|
<div
|
||||||
key={script.id}
|
key={script.id}
|
||||||
type="button"
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={script.title}
|
||||||
className={`chat-list-item ${activeTabId === script.id ? 'active' : ''}`}
|
className={`chat-list-item ${activeTabId === script.id ? 'active' : ''}`}
|
||||||
onClick={() => openScriptTab(openTab, script.id, 'preview')}
|
onClick={() => openScriptTab(openTab, script.id, 'preview')}
|
||||||
onDoubleClick={() => openScriptTab(openTab, script.id, 'pin')}
|
onDoubleClick={() => openScriptTab(openTab, script.id, 'pin')}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
openScriptTab(openTab, script.id, 'pin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
openScriptTab(openTab, script.id, 'preview');
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="chat-item-content">
|
<div className="chat-item-content">
|
||||||
<div className="chat-item-title">{script.title}</div>
|
<div className="chat-item-title">{script.title}</div>
|
||||||
|
<div className="chat-item-date">{formatDate(script.updatedAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
<button
|
||||||
|
className="chat-item-delete"
|
||||||
|
onClick={(event) => handleDeleteScript(event, script.id)}
|
||||||
|
title={t('sidebar.scripts.deleteScript')}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -419,7 +419,15 @@
|
|||||||
"sidebar.nav.style": "Stil",
|
"sidebar.nav.style": "Stil",
|
||||||
"sidebar.nav.scripts": "Skripte",
|
"sidebar.nav.scripts": "Skripte",
|
||||||
"scripts.run": "Skript ausführen",
|
"scripts.run": "Skript ausführen",
|
||||||
|
"scripts.save": "Skript speichern",
|
||||||
|
"scripts.delete": "Skript löschen",
|
||||||
"scripts.content": "Skriptinhalt",
|
"scripts.content": "Skriptinhalt",
|
||||||
|
"scripts.field.kind": "Typ",
|
||||||
|
"scripts.field.entrypoint": "Einstiegspunkt",
|
||||||
|
"scripts.field.enabled": "Aktiviert",
|
||||||
|
"scripts.kind.utility": "utility",
|
||||||
|
"scripts.kind.macro": "macro",
|
||||||
|
"scripts.kind.transform": "transform",
|
||||||
"sidebar.tagCloud": "Tag-Wolke",
|
"sidebar.tagCloud": "Tag-Wolke",
|
||||||
"sidebar.createEdit": "Erstellen & Bearbeiten",
|
"sidebar.createEdit": "Erstellen & Bearbeiten",
|
||||||
"sidebar.mergeTags": "Tags zusammenführen",
|
"sidebar.mergeTags": "Tags zusammenführen",
|
||||||
@@ -705,6 +713,8 @@
|
|||||||
"sidebar.scripts.none": "Noch keine Skripte",
|
"sidebar.scripts.none": "Noch keine Skripte",
|
||||||
"sidebar.scripts.createScript": "Ein Skript erstellen",
|
"sidebar.scripts.createScript": "Ein Skript erstellen",
|
||||||
"sidebar.scripts.createFailed": "Skript konnte nicht erstellt werden",
|
"sidebar.scripts.createFailed": "Skript konnte nicht erstellt werden",
|
||||||
|
"sidebar.scripts.deleteScript": "Skript löschen",
|
||||||
|
"sidebar.scripts.deleteFailed": "Skript konnte nicht gelöscht werden",
|
||||||
"sidebar.import.none": "Noch keine Importdefinitionen",
|
"sidebar.import.none": "Noch keine Importdefinitionen",
|
||||||
"sidebar.import.createDefinition": "Eine Importdefinition erstellen",
|
"sidebar.import.createDefinition": "Eine Importdefinition erstellen",
|
||||||
"sidebar.import.deleteDefinition": "Importdefinition löschen",
|
"sidebar.import.deleteDefinition": "Importdefinition löschen",
|
||||||
|
|||||||
@@ -419,7 +419,15 @@
|
|||||||
"sidebar.nav.style": "Style",
|
"sidebar.nav.style": "Style",
|
||||||
"sidebar.nav.scripts": "Scripts",
|
"sidebar.nav.scripts": "Scripts",
|
||||||
"scripts.run": "Run Script",
|
"scripts.run": "Run Script",
|
||||||
|
"scripts.save": "Save Script",
|
||||||
|
"scripts.delete": "Delete Script",
|
||||||
"scripts.content": "Script Content",
|
"scripts.content": "Script Content",
|
||||||
|
"scripts.field.kind": "Kind",
|
||||||
|
"scripts.field.entrypoint": "Entrypoint",
|
||||||
|
"scripts.field.enabled": "Enabled",
|
||||||
|
"scripts.kind.utility": "utility",
|
||||||
|
"scripts.kind.macro": "macro",
|
||||||
|
"scripts.kind.transform": "transform",
|
||||||
"sidebar.tagCloud": "Tag Cloud",
|
"sidebar.tagCloud": "Tag Cloud",
|
||||||
"sidebar.createEdit": "Create & Edit",
|
"sidebar.createEdit": "Create & Edit",
|
||||||
"sidebar.mergeTags": "Merge Tags",
|
"sidebar.mergeTags": "Merge Tags",
|
||||||
@@ -705,6 +713,8 @@
|
|||||||
"sidebar.scripts.none": "No scripts yet",
|
"sidebar.scripts.none": "No scripts yet",
|
||||||
"sidebar.scripts.createScript": "Create a script",
|
"sidebar.scripts.createScript": "Create a script",
|
||||||
"sidebar.scripts.createFailed": "Failed to create script",
|
"sidebar.scripts.createFailed": "Failed to create script",
|
||||||
|
"sidebar.scripts.deleteScript": "Delete script",
|
||||||
|
"sidebar.scripts.deleteFailed": "Failed to delete script",
|
||||||
"sidebar.import.none": "No import definitions yet",
|
"sidebar.import.none": "No import definitions yet",
|
||||||
"sidebar.import.createDefinition": "Create an import definition",
|
"sidebar.import.createDefinition": "Create an import definition",
|
||||||
"sidebar.import.deleteDefinition": "Delete import definition",
|
"sidebar.import.deleteDefinition": "Delete import definition",
|
||||||
|
|||||||
@@ -419,7 +419,15 @@
|
|||||||
"sidebar.nav.style": "Estilo",
|
"sidebar.nav.style": "Estilo",
|
||||||
"sidebar.nav.scripts": "Scripts",
|
"sidebar.nav.scripts": "Scripts",
|
||||||
"scripts.run": "Ejecutar script",
|
"scripts.run": "Ejecutar script",
|
||||||
|
"scripts.save": "Guardar script",
|
||||||
|
"scripts.delete": "Eliminar script",
|
||||||
"scripts.content": "Contenido del script",
|
"scripts.content": "Contenido del script",
|
||||||
|
"scripts.field.kind": "Tipo",
|
||||||
|
"scripts.field.entrypoint": "Punto de entrada",
|
||||||
|
"scripts.field.enabled": "Habilitado",
|
||||||
|
"scripts.kind.utility": "utility",
|
||||||
|
"scripts.kind.macro": "macro",
|
||||||
|
"scripts.kind.transform": "transform",
|
||||||
"sidebar.tagCloud": "Nube de etiquetas",
|
"sidebar.tagCloud": "Nube de etiquetas",
|
||||||
"sidebar.createEdit": "Crear y editar",
|
"sidebar.createEdit": "Crear y editar",
|
||||||
"sidebar.mergeTags": "Combinar etiquetas",
|
"sidebar.mergeTags": "Combinar etiquetas",
|
||||||
@@ -705,6 +713,8 @@
|
|||||||
"sidebar.scripts.none": "Aún no hay scripts",
|
"sidebar.scripts.none": "Aún no hay scripts",
|
||||||
"sidebar.scripts.createScript": "Crear un script",
|
"sidebar.scripts.createScript": "Crear un script",
|
||||||
"sidebar.scripts.createFailed": "No se pudo crear el script",
|
"sidebar.scripts.createFailed": "No se pudo crear el script",
|
||||||
|
"sidebar.scripts.deleteScript": "Eliminar script",
|
||||||
|
"sidebar.scripts.deleteFailed": "No se pudo eliminar el script",
|
||||||
"sidebar.import.none": "Sin definiciones de importación",
|
"sidebar.import.none": "Sin definiciones de importación",
|
||||||
"sidebar.import.createDefinition": "Crear definición",
|
"sidebar.import.createDefinition": "Crear definición",
|
||||||
"sidebar.import.deleteDefinition": "Eliminar definición",
|
"sidebar.import.deleteDefinition": "Eliminar definición",
|
||||||
|
|||||||
@@ -419,7 +419,15 @@
|
|||||||
"sidebar.nav.style": "Style",
|
"sidebar.nav.style": "Style",
|
||||||
"sidebar.nav.scripts": "Scripts",
|
"sidebar.nav.scripts": "Scripts",
|
||||||
"scripts.run": "Exécuter le script",
|
"scripts.run": "Exécuter le script",
|
||||||
|
"scripts.save": "Enregistrer le script",
|
||||||
|
"scripts.delete": "Supprimer le script",
|
||||||
"scripts.content": "Contenu du script",
|
"scripts.content": "Contenu du script",
|
||||||
|
"scripts.field.kind": "Type",
|
||||||
|
"scripts.field.entrypoint": "Point d’entrée",
|
||||||
|
"scripts.field.enabled": "Activé",
|
||||||
|
"scripts.kind.utility": "utility",
|
||||||
|
"scripts.kind.macro": "macro",
|
||||||
|
"scripts.kind.transform": "transform",
|
||||||
"sidebar.tagCloud": "Nuage d’étiquettes",
|
"sidebar.tagCloud": "Nuage d’étiquettes",
|
||||||
"sidebar.createEdit": "Créer & modifier",
|
"sidebar.createEdit": "Créer & modifier",
|
||||||
"sidebar.mergeTags": "Fusionner les étiquettes",
|
"sidebar.mergeTags": "Fusionner les étiquettes",
|
||||||
@@ -705,6 +713,8 @@
|
|||||||
"sidebar.scripts.none": "Aucun script",
|
"sidebar.scripts.none": "Aucun script",
|
||||||
"sidebar.scripts.createScript": "Créer un script",
|
"sidebar.scripts.createScript": "Créer un script",
|
||||||
"sidebar.scripts.createFailed": "Impossible de créer le script",
|
"sidebar.scripts.createFailed": "Impossible de créer le script",
|
||||||
|
"sidebar.scripts.deleteScript": "Supprimer le script",
|
||||||
|
"sidebar.scripts.deleteFailed": "Impossible de supprimer le script",
|
||||||
"sidebar.import.none": "Aucune définition d’import",
|
"sidebar.import.none": "Aucune définition d’import",
|
||||||
"sidebar.import.createDefinition": "Créer une définition",
|
"sidebar.import.createDefinition": "Créer une définition",
|
||||||
"sidebar.import.deleteDefinition": "Supprimer la définition",
|
"sidebar.import.deleteDefinition": "Supprimer la définition",
|
||||||
|
|||||||
@@ -419,7 +419,15 @@
|
|||||||
"sidebar.nav.style": "Stile",
|
"sidebar.nav.style": "Stile",
|
||||||
"sidebar.nav.scripts": "Script",
|
"sidebar.nav.scripts": "Script",
|
||||||
"scripts.run": "Esegui script",
|
"scripts.run": "Esegui script",
|
||||||
|
"scripts.save": "Salva script",
|
||||||
|
"scripts.delete": "Elimina script",
|
||||||
"scripts.content": "Contenuto script",
|
"scripts.content": "Contenuto script",
|
||||||
|
"scripts.field.kind": "Tipo",
|
||||||
|
"scripts.field.entrypoint": "Punto di ingresso",
|
||||||
|
"scripts.field.enabled": "Abilitato",
|
||||||
|
"scripts.kind.utility": "utility",
|
||||||
|
"scripts.kind.macro": "macro",
|
||||||
|
"scripts.kind.transform": "transform",
|
||||||
"sidebar.tagCloud": "Nuvola tag",
|
"sidebar.tagCloud": "Nuvola tag",
|
||||||
"sidebar.createEdit": "Crea e modifica",
|
"sidebar.createEdit": "Crea e modifica",
|
||||||
"sidebar.mergeTags": "Unisci tag",
|
"sidebar.mergeTags": "Unisci tag",
|
||||||
@@ -705,6 +713,8 @@
|
|||||||
"sidebar.scripts.none": "Nessuno script",
|
"sidebar.scripts.none": "Nessuno script",
|
||||||
"sidebar.scripts.createScript": "Crea uno script",
|
"sidebar.scripts.createScript": "Crea uno script",
|
||||||
"sidebar.scripts.createFailed": "Impossibile creare lo script",
|
"sidebar.scripts.createFailed": "Impossibile creare lo script",
|
||||||
|
"sidebar.scripts.deleteScript": "Elimina script",
|
||||||
|
"sidebar.scripts.deleteFailed": "Impossibile eliminare lo script",
|
||||||
"sidebar.import.none": "Nessuna definizione di importazione",
|
"sidebar.import.none": "Nessuna definizione di importazione",
|
||||||
"sidebar.import.createDefinition": "Crea definizione",
|
"sidebar.import.createDefinition": "Crea definizione",
|
||||||
"sidebar.import.deleteDefinition": "Elimina definizione",
|
"sidebar.import.deleteDefinition": "Elimina definizione",
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ describe('ScriptEngine', () => {
|
|||||||
content: 'def render(context):\n return {"html": "<h1>Hi</h1>"}',
|
content: 'def render(context):\n return {"html": "<h1>Hi</h1>"}',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(created.slug).toBe('render-hero');
|
expect(created.slug).toBe('render_hero');
|
||||||
expect(mockScripts.has(created.id)).toBe(true);
|
expect(mockScripts.has(created.id)).toBe(true);
|
||||||
expect(mockFiles.get('/mock/userData/projects/default/scripts/render-hero.py')).toContain('def render');
|
expect(mockFiles.get('/mock/userData/projects/default/scripts/render_hero.py')).toContain('def render');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates script metadata and file content', async () => {
|
it('updates script metadata and file content', async () => {
|
||||||
@@ -117,8 +117,29 @@ describe('ScriptEngine', () => {
|
|||||||
content: 'def render(context):\n return {"html": "<h1>Banner</h1>"}',
|
content: 'def render(context):\n return {"html": "<h1>Banner</h1>"}',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(updated?.slug).toBe('render-hero-banner');
|
expect(updated?.slug).toBe('render_hero_banner');
|
||||||
expect(mockFiles.get('/mock/userData/projects/default/scripts/render-hero-banner.py')).toContain('Banner');
|
expect(mockFiles.get('/mock/userData/projects/default/scripts/render_hero_banner.py')).toContain('Banner');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends underscore numeric suffix for duplicate slugs', async () => {
|
||||||
|
const first = await scriptEngine.createScript({
|
||||||
|
title: 'Render Hero',
|
||||||
|
kind: 'macro',
|
||||||
|
content: 'def render(context):\n return {"html": "<h1>Hi</h1>"}',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked((await import('uuid')).v4)
|
||||||
|
.mockReturnValueOnce('mock-script-id-2');
|
||||||
|
|
||||||
|
const second = await scriptEngine.createScript({
|
||||||
|
title: 'Render Hero',
|
||||||
|
kind: 'macro',
|
||||||
|
content: 'def render(context):\n return {"html": "<h1>Again</h1>"}',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(first.slug).toBe('render_hero');
|
||||||
|
expect(second.slug).toBe('render_hero_2');
|
||||||
|
expect(mockFiles.get('/mock/userData/projects/default/scripts/render_hero_2.py')).toContain('Again');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes script metadata and source file', async () => {
|
it('deletes script metadata and source file', async () => {
|
||||||
@@ -132,6 +153,6 @@ describe('ScriptEngine', () => {
|
|||||||
|
|
||||||
expect(deleted).toBe(true);
|
expect(deleted).toBe(true);
|
||||||
expect(mockScripts.has(created.id)).toBe(false);
|
expect(mockScripts.has(created.id)).toBe(false);
|
||||||
expect(mockFiles.has('/mock/userData/projects/default/scripts/delete-me.py')).toBe(false);
|
expect(mockFiles.has('/mock/userData/projects/default/scripts/delete_me.py')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ describe('ScriptsView styles', () => {
|
|||||||
const css = fs.readFileSync(cssPath, 'utf8');
|
const css = fs.readFileSync(cssPath, 'utf8');
|
||||||
|
|
||||||
expect(css).toMatch(/\.scripts-editor\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0;[^}]*\}/s);
|
expect(css).toMatch(/\.scripts-editor\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0;[^}]*\}/s);
|
||||||
expect(css).toMatch(/\.scripts-textarea\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0;[^}]*\}/s);
|
expect(css).toMatch(/\.scripts-monaco\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0;[^}]*\}/s);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,24 @@ import { ScriptsView } from '../../../src/renderer/components/ScriptsView/Script
|
|||||||
import { useAppStore } from '../../../src/renderer/store';
|
import { useAppStore } from '../../../src/renderer/store';
|
||||||
|
|
||||||
const executeMock = vi.fn();
|
const executeMock = vi.fn();
|
||||||
|
const monacoPropsSpy = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('@monaco-editor/react', () => ({
|
||||||
|
default: (props: {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value?: string) => void;
|
||||||
|
language?: string;
|
||||||
|
}) => {
|
||||||
|
monacoPropsSpy(props);
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
aria-label="Script Content"
|
||||||
|
value={props.value || ''}
|
||||||
|
onChange={(event) => props.onChange?.(event.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../../../src/renderer/python/runtimeManagerInstance', () => ({
|
vi.mock('../../../src/renderer/python/runtimeManagerInstance', () => ({
|
||||||
getPythonRuntimeManager: () => ({
|
getPythonRuntimeManager: () => ({
|
||||||
@@ -59,6 +77,89 @@ describe('ScriptsView', () => {
|
|||||||
|
|
||||||
fireEvent.change(textarea, { target: { value: 'print("updated")' } });
|
fireEvent.change(textarea, { target: { value: 'print("updated")' } });
|
||||||
expect(textarea.value).toContain('updated');
|
expect(textarea.value).toContain('updated');
|
||||||
|
|
||||||
|
expect(monacoPropsSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
language: 'python',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows metadata fields and footer timestamps', async () => {
|
||||||
|
render(<ScriptsView scriptId="script-1" />);
|
||||||
|
|
||||||
|
const titleInput = await screen.findByLabelText('Title') as HTMLInputElement;
|
||||||
|
const slugInput = screen.getByLabelText('Slug') as HTMLInputElement;
|
||||||
|
const kindSelect = screen.getByLabelText('Kind') as HTMLSelectElement;
|
||||||
|
const entrypointInput = screen.getByLabelText('Entrypoint') as HTMLInputElement;
|
||||||
|
const enabledInput = screen.getByLabelText('Enabled') as HTMLInputElement;
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(titleInput.value).toBe('Hello Script');
|
||||||
|
expect(slugInput.value).toBe('hello_script');
|
||||||
|
});
|
||||||
|
expect(kindSelect.value).toBe('utility');
|
||||||
|
expect(entrypointInput.value).toBe('render');
|
||||||
|
expect(enabledInput.checked).toBe(true);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Created:/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Updated:/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves renamed script metadata and content', async () => {
|
||||||
|
const updateMock = vi.fn().mockResolvedValue({
|
||||||
|
id: 'script-1',
|
||||||
|
projectId: 'default',
|
||||||
|
slug: 'my_helper_function',
|
||||||
|
title: 'My Helper Function',
|
||||||
|
kind: 'utility',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 2,
|
||||||
|
filePath: '/tmp/hello-script.py',
|
||||||
|
content: 'print("renamed")',
|
||||||
|
createdAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-02-22T00:01:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
(window as any).electronAPI.scripts.update = updateMock;
|
||||||
|
|
||||||
|
render(<ScriptsView scriptId="script-1" />);
|
||||||
|
|
||||||
|
const titleInput = await screen.findByLabelText('Title');
|
||||||
|
const kindSelect = screen.getByLabelText('Kind');
|
||||||
|
const entrypointInput = screen.getByLabelText('Entrypoint');
|
||||||
|
const enabledInput = screen.getByLabelText('Enabled');
|
||||||
|
const textarea = screen.getByLabelText('Script Content');
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect((titleInput as HTMLInputElement).value).toBe('Hello Script');
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(kindSelect, { target: { value: 'transform' } });
|
||||||
|
fireEvent.click(enabledInput);
|
||||||
|
fireEvent.change(textarea, { target: { value: 'print("renamed")' } });
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect((kindSelect as HTMLSelectElement).value).toBe('transform');
|
||||||
|
expect((enabledInput as HTMLInputElement).checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Save Script' }));
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(updateMock).toHaveBeenCalledWith(
|
||||||
|
'script-1',
|
||||||
|
expect.objectContaining({
|
||||||
|
title: 'Hello Script',
|
||||||
|
slug: 'hello_script',
|
||||||
|
kind: 'transform',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: false,
|
||||||
|
content: 'print("hello")',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('runs selected script and writes output into panel output log', async () => {
|
it('runs selected script and writes output into panel output log', async () => {
|
||||||
@@ -77,4 +178,23 @@ describe('ScriptsView', () => {
|
|||||||
expect(state.panelOutputEntries.length).toBeGreaterThan(0);
|
expect(state.panelOutputEntries.length).toBeGreaterThan(0);
|
||||||
expect(state.panelOutputEntries[state.panelOutputEntries.length - 1].message).toContain('hello');
|
expect(state.panelOutputEntries[state.panelOutputEntries.length - 1].message).toContain('hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deletes script from editor action', async () => {
|
||||||
|
const deleteMock = vi.fn().mockResolvedValue(true);
|
||||||
|
(window as any).electronAPI.scripts.delete = deleteMock;
|
||||||
|
|
||||||
|
useAppStore.setState({
|
||||||
|
tabs: [{ type: 'scripts', id: 'script-1', isTransient: false }],
|
||||||
|
activeTabId: 'script-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ScriptsView scriptId="script-1" />);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('button', { name: 'Delete Script' }));
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(deleteMock).toHaveBeenCalledWith('script-1');
|
||||||
|
expect(useAppStore.getState().tabs).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,21 @@ describe('Sidebar scripts list behavior', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
const listeners = new Map<string, Set<(event: Event) => void>>();
|
||||||
|
(window as any).addEventListener = vi.fn((type: string, listener: (event: Event) => void) => {
|
||||||
|
if (!listeners.has(type)) {
|
||||||
|
listeners.set(type, new Set());
|
||||||
|
}
|
||||||
|
listeners.get(type)?.add(listener);
|
||||||
|
});
|
||||||
|
(window as any).removeEventListener = vi.fn((type: string, listener: (event: Event) => void) => {
|
||||||
|
listeners.get(type)?.delete(listener);
|
||||||
|
});
|
||||||
|
(window as any).dispatchEvent = vi.fn((event: Event) => {
|
||||||
|
listeners.get(event.type)?.forEach((listener) => listener(event));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
(window as any).electronAPI = {
|
(window as any).electronAPI = {
|
||||||
...(window as any).electronAPI,
|
...(window as any).electronAPI,
|
||||||
scripts: {
|
scripts: {
|
||||||
@@ -43,9 +58,11 @@ describe('Sidebar scripts list behavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('opens a transient script tab on single click', async () => {
|
it('opens a transient script tab on single click', async () => {
|
||||||
render(<Sidebar />);
|
const { container } = render(<Sidebar />);
|
||||||
|
|
||||||
const scriptRow = await screen.findByRole('button', { name: 'Hello Script' });
|
const scriptRow = await screen.findByRole('button', { name: 'Hello Script' });
|
||||||
|
expect(scriptRow).toHaveClass('chat-list-item');
|
||||||
|
expect(container.querySelector('.chat-item-date')).not.toBeNull();
|
||||||
fireEvent.click(scriptRow);
|
fireEvent.click(scriptRow);
|
||||||
|
|
||||||
expect(useAppStore.getState().tabs).toEqual([
|
expect(useAppStore.getState().tabs).toEqual([
|
||||||
@@ -144,4 +161,70 @@ describe('Sidebar scripts list behavior', () => {
|
|||||||
]);
|
]);
|
||||||
expect(useAppStore.getState().activeTabId).toBe('script-1');
|
expect(useAppStore.getState().activeTabId).toBe('script-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deletes a script from sidebar action', async () => {
|
||||||
|
const deleteMock = vi.fn().mockResolvedValue(true);
|
||||||
|
(window as any).electronAPI.scripts.delete = deleteMock;
|
||||||
|
|
||||||
|
useAppStore.setState({
|
||||||
|
tabs: [{ type: 'scripts', id: 'script-1', isTransient: false }],
|
||||||
|
activeTabId: 'script-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<Sidebar />);
|
||||||
|
|
||||||
|
const deleteButton = await screen.findByTitle('Delete script');
|
||||||
|
fireEvent.click(deleteButton);
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(deleteMock).toHaveBeenCalledWith('script-1');
|
||||||
|
expect(useAppStore.getState().tabs).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refreshes scripts list when scripts-changed event is emitted', async () => {
|
||||||
|
const getAllMock = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
id: 'script-1',
|
||||||
|
projectId: 'default',
|
||||||
|
slug: 'hello_script',
|
||||||
|
title: 'Hello Script',
|
||||||
|
kind: 'utility',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 1,
|
||||||
|
filePath: '/tmp/hello-script.py',
|
||||||
|
content: 'print("hello")',
|
||||||
|
createdAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
id: 'script-1',
|
||||||
|
projectId: 'default',
|
||||||
|
slug: 'renamed_script',
|
||||||
|
title: 'Renamed Script',
|
||||||
|
kind: 'utility',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 2,
|
||||||
|
filePath: '/tmp/hello-script.py',
|
||||||
|
content: 'print("hello")',
|
||||||
|
createdAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-02-22T00:01:00.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
(window as any).electronAPI.scripts.getAll = getAllMock;
|
||||||
|
|
||||||
|
render(<Sidebar />);
|
||||||
|
|
||||||
|
await screen.findByRole('button', { name: 'Hello Script' });
|
||||||
|
window.dispatchEvent(new CustomEvent('bds:scripts-changed'));
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: 'Renamed Script' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user