mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
feat(store): prevent deletion of active strategies and update translations (#1461)
Co-authored-by: Dean <afei.wuhao@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user