feat(store): prevent deletion of active strategies and update translations (#1461)

Co-authored-by: Dean <afei.wuhao@gmail.com>
This commit is contained in:
deanokk
2026-04-11 01:09:13 +08:00
committed by GitHub
parent 6fe849c18d
commit a1af4fec58
4 changed files with 68 additions and 25 deletions
+3
View File
@@ -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
+3
View File
@@ -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',
+61 -24
View File
@@ -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<Record<string, GridStrategyConfig>>({})
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<StrategyConfig['strategy_type']>) => {
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() {
<Sparkles className="w-5 h-5 text-black" />
</div>
<div>
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
<h1 className="text-lg font-bold text-nofx-text">{tr('title')}</h1>
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
</div>
</div>
@@ -743,8 +793,9 @@ export function StrategyStudioPage() {
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
title={tr('deleteTooltip')}
disabled={strategy.is_active}
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
title={strategy.is_active ? tr('cannotDeleteActiveStrategy') : tr('deleteTooltip')}
>
<Trash2 className="w-3 h-3" />
</button>
@@ -848,13 +899,7 @@ export function StrategyStudioPage() {
</div>
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => {
if (!selectedStrategy?.is_default) {
updateConfig('strategy_type', 'ai_trading')
// Clear grid config when switching to AI trading
updateConfig('grid_config', undefined)
}
}}
onClick={() => handleStrategyTypeChange('ai_trading')}
disabled={selectedStrategy?.is_default}
className={`p-3 rounded-lg border transition-all ${
(!editingConfig.strategy_type || editingConfig.strategy_type === 'ai_trading')
@@ -869,15 +914,7 @@ export function StrategyStudioPage() {
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
</button>
<button
onClick={() => {
if (!selectedStrategy?.is_default) {
updateConfig('strategy_type', 'grid_trading')
// Initialize grid config if not exists
if (!editingConfig.grid_config) {
updateConfig('grid_config', defaultGridConfig)
}
}
}}
onClick={() => handleStrategyTypeChange('grid_trading')}
disabled={selectedStrategy?.is_default}
className={`p-3 rounded-lg border transition-all ${
editingConfig.strategy_type === 'grid_trading'
+1 -1
View File
@@ -50,7 +50,7 @@ export interface StrategyConfig {
risk_control: RiskControlConfig;
prompt_sections?: PromptSectionsConfig;
// Grid trading configuration (only used when strategy_type is 'grid_trading')
grid_config?: GridStrategyConfig;
grid_config?: GridStrategyConfig | null;
}
// Grid trading specific configuration