From a1af4fec58a49c7e2a6f167e5909905e3b9a5efe Mon Sep 17 00:00:00 2001 From: deanokk Date: Sat, 11 Apr 2026 01:09:13 +0800 Subject: [PATCH] feat(store): prevent deletion of active strategies and update translations (#1461) Co-authored-by: Dean --- store/strategy.go | 3 + web/src/i18n/translations.ts | 3 + web/src/pages/StrategyStudioPage.tsx | 85 ++++++++++++++++++++-------- web/src/types/strategy.ts | 2 +- 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/store/strategy.go b/store/strategy.go index 19bc9526..8860e8fe 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -444,6 +444,9 @@ func (s *StrategyStore) Delete(userID, id string) error { if st.IsDefault { return fmt.Errorf("cannot delete system default strategy") } + if st.IsActive { + return fmt.Errorf("cannot delete active strategy") + } } // Check if any trader references this strategy diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 48a6ef8c..57bc63ee 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -1066,6 +1066,7 @@ export const translations = { newStrategyName: 'New Strategy', strategyCopy: 'Strategy Copy', strategyDeleted: 'Strategy deleted', + cannotDeleteActiveStrategy: 'Active strategy cannot be deleted', confirmDeleteStrategy: 'Delete this strategy?', confirmDelete: 'Confirm Delete', delete: 'Delete', @@ -2373,6 +2374,7 @@ export const translations = { newStrategyName: '新策略', strategyCopy: '策略副本', strategyDeleted: '策略已删除', + cannotDeleteActiveStrategy: '激活中的策略不能删除', confirmDeleteStrategy: '确定删除此策略?', confirmDelete: '确认删除', delete: '删除', @@ -3482,6 +3484,7 @@ export const translations = { newStrategyName: 'Strategi Baru', strategyCopy: 'Salinan Strategi', strategyDeleted: 'Strategi dihapus', + cannotDeleteActiveStrategy: 'Strategi aktif tidak bisa dihapus', confirmDeleteStrategy: 'Hapus strategi ini?', confirmDelete: 'Konfirmasi Hapus', delete: 'Hapus', diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx index 5e4ee255..62ddea00 100644 --- a/web/src/pages/StrategyStudioPage.tsx +++ b/web/src/pages/StrategyStudioPage.tsx @@ -30,7 +30,7 @@ import { Upload, Globe, } from 'lucide-react' -import type { Strategy, StrategyConfig, AIModel } from '../types' +import type { Strategy, StrategyConfig, AIModel, GridStrategyConfig } from '../types' import { confirmToast, notify } from '../lib/notify' import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor' import { IndicatorEditor } from '../components/strategy/IndicatorEditor' @@ -94,6 +94,7 @@ export function StrategyStudioPage() { duration_ms?: number } | null>(null) const [isRunningAiTest, setIsRunningAiTest] = useState(false) + const gridConfigCacheRef = useRef>({}) const toggleSection = (section: keyof typeof expandedSections) => { setExpandedSections((prev) => ({ @@ -156,6 +157,12 @@ export function StrategyStudioPage() { fetchAiModels() }, [fetchStrategies, fetchAiModels]) + useEffect(() => { + if (!selectedStrategy?.id || !editingConfig?.grid_config) return + + gridConfigCacheRef.current[selectedStrategy.id] = { ...editingConfig.grid_config } + }, [selectedStrategy?.id, editingConfig?.grid_config]) + // Track previous language to detect actual changes const prevLanguageRef = useRef(language) @@ -248,6 +255,12 @@ export function StrategyStudioPage() { // Delete strategy const handleDeleteStrategy = async (id: string) => { if (!token) return + const strategy = strategies.find((item) => item.id === id) + + if (strategy?.is_active) { + notify.error(tr('cannotDeleteActiveStrategy')) + return + } // Check if strategy is in use by any trader before showing dialog try { @@ -443,14 +456,51 @@ export function StrategyStudioPage() { section: K, value: StrategyConfig[K] ) => { - if (!editingConfig) return - setEditingConfig({ - ...editingConfig, - [section]: value, + setEditingConfig((prev) => { + if (!prev) return prev + return { + ...prev, + [section]: value, + } }) setHasChanges(true) } + const handleStrategyTypeChange = (strategyType: NonNullable) => { + if (selectedStrategy?.is_default) return + + const cachedGridConfig = selectedStrategy?.id + ? gridConfigCacheRef.current[selectedStrategy.id] + : null + + setEditingConfig((prev) => { + if (!prev) return prev + + if (strategyType === 'ai_trading') { + if (selectedStrategy?.id && prev.grid_config) { + gridConfigCacheRef.current[selectedStrategy.id] = { ...prev.grid_config } + } + + return { + ...prev, + strategy_type: 'ai_trading', + // Use null so the field is preserved in JSON and backend merge can actually clear it. + grid_config: null, + } + } + + return { + ...prev, + strategy_type: 'grid_trading', + grid_config: cachedGridConfig ?? prev.grid_config ?? { ...defaultGridConfig }, + } + }) + + setPromptPreview(null) + setAiTestResult(null) + setHasChanges(true) + } + // Fetch prompt preview const fetchPromptPreview = async () => { if (!token || !editingConfig) return @@ -666,7 +716,7 @@ export function StrategyStudioPage() {
-

{tr('strategyStudio')}

+

{tr('title')}

{tr('subtitle')}

@@ -743,8 +793,9 @@ export function StrategyStudioPage() { @@ -848,13 +899,7 @@ export function StrategyStudioPage() {