- Add MaterialTypeManagementDialog component for managing material type keywords - Add MaterialsTypeToBeDeletedDAO for database operations - Add material-type-handler IPC handlers - Update CleanerPage with type management button - Add database fix scripts for AUTO_INCREMENT - Update documentation for settings partial save and validation flow Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
26 KiB
Settings Partial Save Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Fix settings save to only update modified fields, preventing unintended overwrites of unmodified .env configuration values.
Architecture: Implement partial save strategy using deep merge + whitelist validation. ConfigManager validates fields, merges with current config, backs up .env, and atomically writes changes.
Tech Stack: TypeScript 5.9, Electron 39, Vitest, Node.js fs module
Task 1: Add Utility Functions to ConfigManager
Files:
- Modify:
src/main/services/config/config-manager.ts
Step 1: Write failing test for deep merge
Create test file: tests/main/services/config/config-manager.test.ts
import { describe, it, expect } from 'vitest'
import { ConfigManager } from '@/services/config/config-manager'
import type { SettingsData } from '@/types/settings.types'
describe('ConfigManager - deep merge utilities', () => {
it('should deep merge objects, updating only specified fields', async () => {
const manager = ConfigManager.getInstance()
await manager.initialize()
// Setup initial state
const initial: SettingsData = {
erp: {
url: 'http://old.com',
username: 'user1',
password: 'pass1',
headless: true,
ignoreHttpsErrors: true,
autoCloseBrowser: true
},
database: {
dbType: 'mysql',
server: '',
mysqlHost: 'localhost',
mysqlPort: 3306,
database: 'db',
username: 'user',
password: ''
},
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'validation.xlsx' },
extraction: {
batchSize: 100,
verbose: true,
autoConvert: true,
mergeBatches: true,
enableDbPersistence: true
},
validation: {
dataSource: 'database_full',
batchSize: 2000,
matchMode: 'substring',
enableCrud: false,
defaultManager: ''
},
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
execution: { dryRun: false }
}
// Load initial settings
await manager.saveAllSettings(initial)
// Partial update
const partial = {
erp: { url: 'http://new.com' }
}
const result = await manager.savePartialSettings(partial)
expect(result.success).toBe(true)
const current = manager.getAllSettings()
// Updated field
expect(current.erp.url).toBe('http://new.com')
// Preserved fields
expect(current.erp.username).toBe('user1')
expect(current.database.dbType).toBe('mysql')
expect(current.paths.dataDir).toBe('/data')
})
})
Step 2: Run test to verify it fails
Run: cd D:/Node/ERPAuto-settings-fix && npm test -- tests/main/services/config/config-manager.test.ts
Expected: FAIL - "savePartialSettings is not a function"
Step 3: Add helper functions to ConfigManager
In src/main/services/config/config-manager.ts, add after the DEFAULT_SETTINGS constant (around line 78):
/**
* Check if value is a plain object
*/
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/**
* Deep merge two objects, only updating fields present in target
* Preserves all fields from source that are not in target
*/
function deepMerge<T>(source: T, target: Partial<T>): T {
const result = { ...source }
for (const key in target) {
if (key in target) {
const targetValue = target[key]
const sourceValue = result[key]
if (isObject(targetValue) && isObject(sourceValue)) {
result[key] = deepMerge(sourceValue, targetValue)
} else if (targetValue !== undefined) {
result[key] = targetValue as T[Extract<keyof T, string>]
}
}
}
return result
}
/**
* UI editable field whitelist
* Fields that can be modified through the settings UI
*/
const UI_EDITABLE_FIELDS: string[] = [
'erp.url',
'erp.username',
'erp.password'
// Add more fields as UI expands
]
/**
* Validate that settings only contain editable fields
*/
function validateEditableFields(settings: Partial<SettingsData>): {
valid: boolean
invalidFields: string[]
} {
const invalidFields: string[] = []
for (const [section, values] of Object.entries(settings)) {
if (values && typeof values === 'object') {
for (const field of Object.keys(values)) {
const fieldPath = `${section}.${field}`
if (!UI_EDITABLE_FIELDS.includes(fieldPath)) {
invalidFields.push(fieldPath)
}
}
}
}
return {
valid: invalidFields.length === 0,
invalidFields
}
}
Step 4: Run test to verify it still fails (method not implemented yet)
Run: npm test -- tests/main/services/config/config-manager.test.ts
Expected: FAIL - "savePartialSettings is not a function"
Step 5: Commit
cd D:/Node/ERPAuto-settings-fix
git add src/main/services/config/config-manager.ts tests/main/services/config/config-manager.test.ts
git commit -m "feat: add deep merge and validation utility functions to ConfigManager"
Task 2: Add Backup and Restore Mechanism
Files:
- Modify:
src/main/services/config/config-manager.ts
Step 1: Write test for backup functionality
Add to tests/main/services/config/config-manager.test.ts:
describe('ConfigManager - backup and restore', () => {
it('should create backup before saving', async () => {
const manager = ConfigManager.getInstance()
await manager.initialize()
const backupSuccess = await manager['backupEnvFile']()
expect(backupSuccess).toBe(true)
// Check backup file exists
const fs = await import('fs')
const path = await import('path')
const backupPath = path.resolve(process.cwd(), '.env.backup')
expect(fs.existsSync(backupPath)).toBe(true)
})
it('should restore from backup when save fails', async () => {
const manager = ConfigManager.getInstance()
await manager.initialize()
// Create initial state
const initial = manager.getAllSettings()
const originalUrl = initial.erp.url
// Mock fs.writeFileSync to fail
const fs = await import('fs')
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {
throw new Error('Disk full')
})
const result = await manager.savePartialSettings({
erp: { url: 'http://should-not-save.com' }
})
expect(result.success).toBe(false)
// Restore should have happened
const current = manager.getAllSettings()
expect(current.erp.url).toBe(originalUrl)
writeFileSyncSpy.mockRestore()
})
})
Step 2: Run test to verify it fails
Run: npm test -- tests/main/services/config/config-manager.test.ts
Expected: FAIL - "backupEnvFile is not a function"
Step 3: Add backup path property and methods to ConfigManager class
In ConfigManager class, modify constructor (around line 88) to add backupPath:
export class ConfigManager {
private static instance: ConfigManager | null = null
private envPath: string
private backupPath: string // ADD THIS LINE
private configCache: Map<string, string> = new Map()
private initialized: boolean = false
private constructor() {
if (this.initialized) {
return
}
this.envPath = path.resolve(__dirname, '../../.env')
this.backupPath = path.resolve(__dirname, '../../.env.backup') // ADD THIS LINE
this.initialized = true
}
Add private methods at the end of the class (before getInstance()):
/**
* Backup current .env file
*/
private async backupEnvFile(): Promise<boolean> {
try {
if (fs.existsSync(this.envPath)) {
fs.copyFileSync(this.envPath, this.backupPath)
log.debug('Backup created', { path: this.backupPath })
return true
}
return false
} catch (error) {
log.error('Failed to backup .env file', { error })
return false
}
}
/**
* Restore .env file from backup
*/
private async restoreBackup(): Promise<boolean> {
try {
if (fs.existsSync(this.backupPath)) {
fs.copyFileSync(this.backupPath, this.envPath)
await this.loadEnvFile()
log.info('Restored from backup')
return true
}
return false
} catch (error) {
log.error('Failed to restore backup', { error })
return false
}
}
Step 4: Run test to verify it passes
Run: npm test -- tests/main/services/config/config-manager.test.ts
Expected: PASS
Step 5: Commit
cd D:/Node/ERPAuto-settings-fix
git add src/main/services/config/config-manager.ts tests/main/services/config/config-manager.test.ts
git commit -m "feat: add backup and restore mechanism to ConfigManager"
Task 3: Implement savePartialSettings Method
Files:
- Modify:
src/main/services/config/config-manager.ts
Step 1: Write comprehensive test for savePartialSettings
Add to tests/main/services/config/config-manager.test.ts:
describe('ConfigManager.savePartialSettings', () => {
it('should save only specified fields and preserve others', async () => {
const manager = ConfigManager.getInstance()
await manager.initialize()
// Setup initial state with multiple categories
await manager.saveAllSettings({
erp: {
url: 'http://old.com',
username: 'user1',
password: 'pass1',
headless: true,
ignoreHttpsErrors: true,
autoCloseBrowser: true
},
database: {
dbType: 'mysql',
server: '',
mysqlHost: '192.168.1.1',
mysqlPort: 3306,
database: 'testdb',
username: 'dbuser',
password: ''
},
paths: { dataDir: '/old/path', defaultOutput: 'out.xlsx', validationOutput: 'val.xlsx' },
extraction: {
batchSize: 50,
verbose: true,
autoConvert: true,
mergeBatches: true,
enableDbPersistence: true
},
validation: {
dataSource: 'database_full',
batchSize: 1000,
matchMode: 'exact',
enableCrud: false,
defaultManager: ''
},
ui: { fontFamily: 'Tahoma', fontSize: 14, productionIdInputWidth: 25 },
execution: { dryRun: true }
})
// Update only ERP URL
const result = await manager.savePartialSettings({
erp: { url: 'http://new.com' }
})
expect(result.success).toBe(true)
const current = manager.getAllSettings()
// Verify updated field
expect(current.erp.url).toBe('http://new.com')
// Verify preserved ERP fields
expect(current.erp.username).toBe('user1')
expect(current.erp.password).toBe('pass1')
// Verify preserved other categories
expect(current.database.dbType).toBe('mysql')
expect(current.database.mysqlHost).toBe('192.168.1.1')
expect(current.paths.dataDir).toBe('/old/path')
expect(current.extraction.batchSize).toBe(50)
expect(current.ui.fontFamily).toBe('Tahoma')
})
it('should reject updates to non-whitelisted fields', async () => {
const manager = ConfigManager.getInstance()
await manager.initialize()
const result = await manager.savePartialSettings({
database: { dbType: 'postgres' }
})
expect(result.success).toBe(false)
expect(result.error).toContain('不允许修改')
expect(result.error).toContain('database.dbType')
})
it('should handle nested object updates correctly', async () => {
const manager = ConfigManager.getInstance()
await manager.initialize()
await manager.saveAllSettings({
erp: {
url: 'http://test.com',
username: 'u',
password: 'p',
headless: false,
ignoreHttpsErrors: false,
autoCloseBrowser: false
},
database: {
dbType: 'mysql',
server: '',
mysqlHost: 'localhost',
mysqlPort: 3306,
database: 'db',
username: 'user',
password: ''
},
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'val.xlsx' },
extraction: {
batchSize: 100,
verbose: true,
autoConvert: true,
mergeBatches: true,
enableDbPersistence: true
},
validation: {
dataSource: 'database_full',
batchSize: 2000,
matchMode: 'substring',
enableCrud: false,
defaultManager: ''
},
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
execution: { dryRun: false }
})
// Update multiple ERP fields at once
const result = await manager.savePartialSettings({
erp: {
url: 'http://updated.com',
username: 'newuser',
password: 'newpass'
}
})
expect(result.success).toBe(true)
const current = manager.getAllSettings()
expect(current.erp.url).toBe('http://updated.com')
expect(current.erp.username).toBe('newuser')
expect(current.erp.password).toBe('newpass')
expect(current.erp.headless).toBe(false) // preserved
})
it('should restore backup on save failure', async () => {
const manager = ConfigManager.getInstance()
await manager.initialize()
const originalUrl = manager.getAllSettings().erp.url
// Mock save to fail
const originalSave = manager.save.bind(manager)
vi.spyOn(manager, 'save').mockResolvedValueOnce(false)
const result = await manager.savePartialSettings({
erp: { url: 'http://should-not-apply.com' }
})
expect(result.success).toBe(false)
expect(result.error).toContain('保存配置失败')
// Verify rollback
expect(manager.getAllSettings().erp.url).toBe(originalUrl)
manager.save.mockRestore()
})
})
Step 2: Run test to verify it fails
Run: npm test -- tests/main/services/config/config-manager.test.ts
Expected: FAIL - "savePartialSettings is not a function" or implementation incomplete
Step 3: Implement savePartialSettings method
Add this public method to ConfigManager class (after saveAllSettings method, around line 483):
/**
* Save partial settings (only update provided fields)
* Preserves all existing fields not included in the update
*/
public async savePartialSettings(
settings: Partial<SettingsData>
): Promise<{ success: boolean; error?: string }> {
try {
// Step 1: Validate field whitelist
const validation = validateEditableFields(settings)
if (!validation.valid) {
log.warn('Attempted to save non-editable fields', {
invalidFields: validation.invalidFields
})
return {
success: false,
error: `包含不允许修改的字段:${validation.invalidFields.join(', ')}`
}
}
// Step 2: Read current settings
const currentSettings = this.getAllSettings()
// Step 3: Deep merge
const mergedSettings = deepMerge(currentSettings, settings)
// Step 4: Backup and save
const backupSuccess = await this.backupEnvFile()
if (!backupSuccess) {
log.warn('Failed to backup .env file, proceeding with caution')
}
const saveSuccess = await this.saveAllSettings(mergedSettings)
if (!saveSuccess) {
// Save failed, attempt restore
await this.restoreBackup()
return {
success: false,
error: '保存配置失败,已恢复原配置'
}
}
log.info('Settings saved successfully', {
updatedFields: Object.keys(settings)
})
return { success: true }
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Error in savePartialSettings', { error: message })
await this.restoreBackup()
return {
success: false,
error: `保存配置时发生错误:${message}`
}
}
}
Step 4: Run test to verify it passes
Run: npm test -- tests/main/services/config/config-manager.test.ts
Expected: PASS
Step 5: Run typecheck
Run: cd D:/Node/ERPAuto-settings-fix && npm run typecheck:node
Expected: PASS (no type errors)
Step 6: Commit
cd D:/Node/ERPAuto-settings-fix
git add src/main/services/config/config-manager.ts tests/main/services/config/config-manager.test.ts
git commit -m "feat: implement savePartialSettings with validation and rollback"
Task 4: Update IPC Handler to Use Partial Save
Files:
- Modify:
src/main/ipc/settings-handler.ts
Step 1: Update settings:saveSettings handler
Find the settings:saveSettings handler (around line 83) and replace it:
/**
* Save settings (updated to use partial save)
*/
ipcMain.handle(
'settings:saveSettings',
async (_event, settings: Partial<SettingsData>): Promise<SaveSettingsResult> => {
try {
log.info('Saving settings', {
sections: Object.keys(settings)
})
// Use partial save method
const result = await configManager.savePartialSettings(settings)
if (result.success) {
log.info('Settings saved successfully')
return { success: true }
} else {
log.warn('Failed to save settings', {
error: result.error
})
return {
success: false,
error: result.error || '保存设置失败'
}
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
log.error('Error saving settings', { error: message })
return {
success: false,
error: `保存设置失败:${message}`
}
}
}
)
Step 2: Run typecheck
Run: cd D:/Node/ERPAuto-settings-fix && npm run typecheck:node
Expected: PASS
Step 3: Commit
cd D:/Node/ERPAuto-settings-fix
git add src/main/ipc/settings-handler.ts
git commit -m "feat: update settings handler to use savePartialSettings"
Task 5: Update Frontend to Send Only Necessary Fields
Files:
- Modify:
src/renderer/src/pages/SettingsPage.tsx
Step 1: Update handleSaveSettings to send partial settings
Find the handleSaveSettings function (around line 61) and replace it:
const handleSaveSettings = async () => {
try {
// Only send UI-supported fields (double safety)
const partialSettings = {
erp: {
url: settings.erp?.url,
username: settings.erp?.username,
password: settings.erp?.password
}
}
const result = await window.electron.settings.saveSettings(partialSettings as any)
if (result.success) {
setIsModified(false)
showMessage('success', '设置保存成功')
} else {
showMessage('error', result.error || '保存失败')
}
} catch (error) {
showMessage('error', '保存设置时发生错误')
}
}
Step 2: Run typecheck
Run: cd D:/Node/ERPAuto-settings-fix && npm run typecheck:web
Expected: PASS
Step 3: Commit
cd D:/Node/ERPAuto-settings-fix
git add src/renderer/src/pages/SettingsPage.tsx
git commit -m "feat: send only ERP fields from settings page (defensive programming)"
Task 6: Manual Testing and Verification
Files:
- Manual test procedure
Step 1: Prepare test environment
cd D:/Node/ERPAuto-settings-fix
# Create a test .env file with all fields
cat > .env << 'EOF'
# Test Configuration
ERP_URL=http://original-test.com
ERP_USERNAME=testuser
ERP_PASSWORD=testpass
ERP_HEADLESS=true
ERP_IGNORE_HTTPS_ERRORS=true
ERP_AUTO_CLOSE_BROWSER=true
DB_TYPE=mysql
DB_NAME=testdb
DB_USERNAME=dbuser
DB_PASSWORD=dbpass
DB_MYSQL_HOST=192.168.100.50
DB_MYSQL_PORT=3306
DB_MYSQL_CHARSET=utf8mb4
PATH_DATA_DIR=/test/data
PATH_DEFAULT_OUTPUT=test.xlsx
PATH_VALIDATION_OUTPUT=validation.xlsx
EXTRACTION_BATCH_SIZE=100
EXTRACTION_VERBOSE=true
EXTRACTION_AUTO_CONVERT=true
EXTRACTION_MERGE_BATCHES=true
EXTRACTION_ENABLE_DB_PERSISTENCE=true
VALIDATION_DATA_SOURCE=database_full
VALIDATION_USE_DATABASE=true
VALIDATION_BATCH_SIZE=2000
VALIDATION_ENABLE_CRUD=false
VALIDATION_DEFAULT_MANAGER=admin
VALIDATION_MATCH_MODE=substring
UI_FONT_FAMILY=TestFont
UI_FONT_SIZE=11
UI_PRODUCTION_ID_INPUT_WIDTH=15
EXECUTION_DRYRUN=false
EOF
Step 2: Start development server
Run: npm run dev
Step 3: Navigate to settings page
- Login to the application
- Navigate to Settings page
- Modify only ERP URL to
http://modified-test.com - Click "保存并应用配置"
Step 4: Verify .env file preservation
Check .env file:
cat .env
Expected results:
ERP_URLshould behttp://modified-test.com(CHANGED)DB_TYPEshould still bemysql(PRESERVED)VALIDATION_MATCH_MODEshould still besubstring(PRESERVED)- All other fields should remain unchanged
Step 5: Test whitelist validation
Add test code to temporarily send invalid field:
// In SettingsPage.tsx handleSaveSettings, temporarily add:
const partialSettings = {
erp: {
url: settings.erp?.url,
username: settings.erp?.username,
password: settings.erp?.password
},
database: { dbType: 'postgres' } // Should be rejected
}
Click save, should see error: "包含不允许修改的字段:database.dbType"
Remove test code after verification.
Step 6: Test rollback mechanism
Simulate save failure by temporarily making .env read-only:
chmod -w .env # On Linux/Mac
# or on Windows with file properties
Attempt to save settings, should see error: "保存配置失败,已恢复原配置"
Verify .env content unchanged, then restore write permissions:
chmod +w .env # On Linux/Mac
Step 7: Document test results
Create test report:
cat > docs/test-reports/settings-partial-save-manual-test.md << 'EOF'
# Settings Partial Save - Manual Test Report
**Date:** 2026-03-03
**Tester:** [Your Name]
**Branch:** fix/settings-partial-save
## Test Results
### Test 1: Partial Field Preservation
- [x] Modified ERP URL only
- [x] Verified DB_TYPE unchanged
- [x] Verified all other fields preserved
### Test 2: Whitelist Validation
- [x] Attempted to modify database.dbType
- [x] Received error message about unauthorized field
- [x] No changes applied to .env
### Test 3: Backup and Rollback
- [x] Backup file created before save
- [x] Save failure triggered rollback
- [x] Original configuration restored
## Conclusion
All manual tests passed successfully.
EOF
Step 8: Commit
cd D:/Node/ERPAuto-settings-fix
git add docs/test-reports/settings-partial-save-manual-test.md
git commit -m "test: add manual test report for settings partial save"
Task 7: Update Documentation
Files:
- Create:
docs/settings-partial-save.md - Update:
README.md(if applicable)
Step 1: Create feature documentation
Create docs/settings-partial-save.md:
# Settings Partial Save Feature
## Overview
The settings system now implements partial save functionality to prevent unintended overwrites of configuration values.
## How It Works
1. **Field Whitelist**: Only fields exposed in the UI can be modified
2. **Deep Merge**: Updates are merged with existing config, preserving unmodified fields
3. **Backup & Rollback**: Config is backed up before save; failures trigger automatic rollback
## Editable Fields
Currently editable via UI:
- `erp.url` - ERP system URL
- `erp.username` - ERP login username
- `erp.password` - ERP login password
## Adding New Editable Fields
To add a new field to the UI:
1. Add field to whitelist in `src/main/services/config/config-manager.ts`:
```typescript
const UI_EDITABLE_FIELDS: string[] = [
'erp.url',
'erp.username',
'erp.password',
'database.dbType' // Add new field here
]
```
- Add UI input in
src/renderer/src/pages/SettingsPage.tsx - Update
handleSaveSettingsto include the new field
API
savePartialSettings(settings: Partial)
Saves only the provided fields, preserving all existing configuration.
Returns: { success: boolean, error?: string }
Validation:
- Checks whitelist before applying changes
- Returns error for unauthorized fields
Error Handling
- Unauthorized field: Returns error message listing invalid fields
- Save failure: Automatically restores from backup
- Backup failure: Logs warning, continues with save
Backup File
Location: .env.backup (in project root)
Created before every save operation. Used for rollback on failure.
**Step 2: Update CLAUDE.md if needed**
Add to "Development Commands" or "Architecture Overview" sections if there's relevant information about config management.
**Step 3: Commit**
```bash
cd D:/Node/ERPAuto-settings-fix
git add docs/settings-partial-save.md
git commit -m "docs: add settings partial save feature documentation"
Task 8: Final Verification and Cleanup
Files:
- All modified files
Step 1: Run full test suite
Run: cd D:/Node/ERPAuto-settings-fix && npm test
Expected: All tests pass
Step 2: Run type checking
Run: npm run typecheck
Expected: No type errors
Step 3: Run linting
Run: npm run lint
Expected: No linting errors (or fix if present)
Step 4: Build verification
Run: npm run build
Expected: Build succeeds without errors
Step 5: Review all changes
cd D:/Node/ERPAuto-settings-fix
git diff dev --stat
Verify all changes are expected.
Step 6: Final commit
cd D:/Node/ERPAuto-settings-fix
git add -A
git commit -m "chore: final verification and cleanup for settings partial save feature"
Summary
This implementation plan fixes the settings save issue through:
- ✅ Deep merge utilities that preserve unmodified fields
- ✅ Field whitelist validation to prevent unauthorized changes
- ✅ Backup and rollback mechanism for safe saves
- ✅ Updated IPC handler with partial save support
- ✅ Frontend defensive programming (sends only necessary fields)
- ✅ Comprehensive unit and manual testing
- ✅ Complete documentation
Total estimated implementation time: 2-3 hours
Key files modified:
src/main/services/config/config-manager.ts(core logic)src/main/ipc/settings-handler.ts(IPC layer)src/renderer/src/pages/SettingsPage.tsx(frontend)tests/main/services/config/config-manager.test.ts(tests)