mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(settings): add automatic settings migration system
Implement comprehensive settings migration framework to consolidate duplicate and overlapping configuration options. Automatically migrates legacy fileParsingConfig settings to the new unified FileSource configuration on plugin load. Key features: - Automatic detection of duplicate settings - Safe migration with validation and rollback - User notifications for migration results - Migration history tracking - Test coverage for all migration scenarios - UI styling for migration notifications This ensures smooth upgrades for existing users while maintaining backward compatibility during the transition period.
This commit is contained in:
parent
4c82ab5b0f
commit
1b2e26deec
4 changed files with 777 additions and 0 deletions
222
src/__tests__/SettingsMigration.test.ts
Normal file
222
src/__tests__/SettingsMigration.test.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
/**
|
||||
* Tests for Settings Migration functionality
|
||||
*/
|
||||
|
||||
import {
|
||||
migrateFileParsingSettings,
|
||||
cleanupDeprecatedSettings,
|
||||
hasSettingsDuplicates,
|
||||
runAllMigrations
|
||||
} from '../utils/SettingsMigration';
|
||||
import { DEFAULT_SETTINGS, type TaskProgressBarSettings } from '../common/setting-definition';
|
||||
|
||||
describe('Settings Migration', () => {
|
||||
let testSettings: TaskProgressBarSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a copy of default settings for testing
|
||||
testSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||
});
|
||||
|
||||
describe('hasSettingsDuplicates', () => {
|
||||
it('should detect when file metadata parsing is enabled but FileSource is disabled', () => {
|
||||
testSettings.fileParsingConfig.enableFileMetadataParsing = true;
|
||||
testSettings.fileSource.enabled = false;
|
||||
|
||||
expect(hasSettingsDuplicates(testSettings)).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect when tag parsing is enabled but FileSource tag recognition is disabled', () => {
|
||||
testSettings.fileParsingConfig.enableTagBasedTaskParsing = true;
|
||||
testSettings.fileSource.recognitionStrategies.tags.enabled = false;
|
||||
|
||||
expect(hasSettingsDuplicates(testSettings)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no duplicates exist', () => {
|
||||
expect(hasSettingsDuplicates(testSettings)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateFileParsingSettings', () => {
|
||||
it('should migrate file metadata parsing to FileSource', () => {
|
||||
testSettings.fileParsingConfig.enableFileMetadataParsing = true;
|
||||
testSettings.fileSource.enabled = false;
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(testSettings.fileSource.enabled).toBe(true);
|
||||
expect(result.details).toContain('Migrated: Enable file metadata parsing → Enable FileSource');
|
||||
});
|
||||
|
||||
it('should migrate tag-based parsing settings', () => {
|
||||
testSettings.fileParsingConfig.enableTagBasedTaskParsing = true;
|
||||
testSettings.fileParsingConfig.tagsToParseAsTasks = ['#custom-tag', '#work'];
|
||||
testSettings.fileSource.recognitionStrategies.tags.enabled = false;
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(testSettings.fileSource.recognitionStrategies.tags.enabled).toBe(true);
|
||||
expect(testSettings.fileSource.recognitionStrategies.tags.taskTags).toContain('#custom-tag');
|
||||
expect(testSettings.fileSource.recognitionStrategies.tags.taskTags).toContain('#work');
|
||||
});
|
||||
|
||||
it('should migrate metadata fields for task recognition', () => {
|
||||
testSettings.fileParsingConfig.metadataFieldsToParseAsTasks = ['custom-field', 'project-task'];
|
||||
testSettings.fileSource.recognitionStrategies.metadata.enabled = false;
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(testSettings.fileSource.recognitionStrategies.metadata.enabled).toBe(true);
|
||||
expect(testSettings.fileSource.recognitionStrategies.metadata.taskFields).toContain('custom-field');
|
||||
expect(testSettings.fileSource.recognitionStrategies.metadata.taskFields).toContain('project-task');
|
||||
});
|
||||
|
||||
it('should migrate worker processing settings', () => {
|
||||
testSettings.fileParsingConfig.enableWorkerProcessing = true;
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(testSettings.fileSource.performance.enableWorkerProcessing).toBe(true);
|
||||
expect(result.details).toContain('Migrated: Worker processing setting');
|
||||
});
|
||||
|
||||
it('should migrate task content source settings', () => {
|
||||
testSettings.fileParsingConfig.taskContentFromMetadata = 'custom-title';
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
expect(testSettings.fileSource.fileTaskProperties.contentSource).toBe('custom');
|
||||
expect(testSettings.fileSource.fileTaskProperties.customContentField).toBe('custom-title');
|
||||
expect(result.details).toContain('Migrated: Task content source');
|
||||
});
|
||||
|
||||
it('should handle title content source correctly', () => {
|
||||
testSettings.fileParsingConfig.taskContentFromMetadata = 'title';
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
expect(testSettings.fileSource.fileTaskProperties.contentSource).toBe('title');
|
||||
expect(testSettings.fileSource.fileTaskProperties.customContentField).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupDeprecatedSettings', () => {
|
||||
it('should not cleanup if FileSource is disabled', () => {
|
||||
testSettings.fileParsingConfig.enableFileMetadataParsing = true;
|
||||
testSettings.fileSource.enabled = false;
|
||||
|
||||
const result = cleanupDeprecatedSettings(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toContain('Skipping cleanup: FileSource not enabled');
|
||||
expect(testSettings.fileParsingConfig.enableFileMetadataParsing).toBe(true);
|
||||
});
|
||||
|
||||
it('should cleanup deprecated settings when FileSource is enabled', () => {
|
||||
testSettings.fileParsingConfig.enableFileMetadataParsing = true;
|
||||
testSettings.fileParsingConfig.enableTagBasedTaskParsing = true;
|
||||
testSettings.fileParsingConfig.metadataFieldsToParseAsTasks = ['old-field'];
|
||||
testSettings.fileParsingConfig.tagsToParseAsTasks = ['#old-tag'];
|
||||
testSettings.fileSource.enabled = true;
|
||||
|
||||
const result = cleanupDeprecatedSettings(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(testSettings.fileParsingConfig.enableFileMetadataParsing).toBe(false);
|
||||
expect(testSettings.fileParsingConfig.enableTagBasedTaskParsing).toBe(false);
|
||||
expect(testSettings.fileParsingConfig.metadataFieldsToParseAsTasks).toEqual([]);
|
||||
expect(testSettings.fileParsingConfig.tagsToParseAsTasks).toEqual([]);
|
||||
|
||||
expect(result.details).toContain('Disabled deprecated: Enable file metadata parsing');
|
||||
expect(result.details).toContain('Disabled deprecated: Tag-based task parsing');
|
||||
expect(result.details).toContain('Cleared deprecated: Metadata fields array');
|
||||
expect(result.details).toContain('Cleared deprecated: Task tags array');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runAllMigrations', () => {
|
||||
it('should run complete migration workflow', () => {
|
||||
// Set up scenario with duplicates
|
||||
testSettings.fileParsingConfig.enableFileMetadataParsing = true;
|
||||
testSettings.fileParsingConfig.enableTagBasedTaskParsing = true;
|
||||
testSettings.fileParsingConfig.metadataFieldsToParseAsTasks = ['task-field'];
|
||||
testSettings.fileParsingConfig.tagsToParseAsTasks = ['#task-tag'];
|
||||
testSettings.fileSource.enabled = false;
|
||||
|
||||
const result = runAllMigrations(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
|
||||
// Check migration happened
|
||||
expect(testSettings.fileSource.enabled).toBe(true);
|
||||
expect(testSettings.fileSource.recognitionStrategies.tags.enabled).toBe(true);
|
||||
expect(testSettings.fileSource.recognitionStrategies.metadata.enabled).toBe(true);
|
||||
|
||||
// Check cleanup happened
|
||||
expect(testSettings.fileParsingConfig.enableFileMetadataParsing).toBe(false);
|
||||
expect(testSettings.fileParsingConfig.enableTagBasedTaskParsing).toBe(false);
|
||||
expect(testSettings.fileParsingConfig.metadataFieldsToParseAsTasks).toEqual([]);
|
||||
expect(testSettings.fileParsingConfig.tagsToParseAsTasks).toEqual([]);
|
||||
|
||||
// Check we have migration details
|
||||
expect(result.details.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not run cleanup if migration fails', () => {
|
||||
// No migration needed
|
||||
const result = runAllMigrations(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.details.length).toBeLessThanOrEqual(1); // Only project config detail
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle missing fileSource gracefully', () => {
|
||||
// @ts-ignore - Simulating corrupted settings
|
||||
delete testSettings.fileSource;
|
||||
testSettings.fileParsingConfig.enableFileMetadataParsing = true;
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
expect(result.migrated).toBe(true);
|
||||
expect(testSettings.fileSource).toBeDefined();
|
||||
expect(testSettings.fileSource.enabled).toBe(true);
|
||||
expect(result.details).toContain('Created default FileSource configuration');
|
||||
});
|
||||
|
||||
it('should merge tags without duplicates', () => {
|
||||
testSettings.fileParsingConfig.enableTagBasedTaskParsing = true;
|
||||
testSettings.fileParsingConfig.tagsToParseAsTasks = ['#task', '#work'];
|
||||
testSettings.fileSource.recognitionStrategies.tags.taskTags = ['#task', '#existing'];
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
const finalTags = testSettings.fileSource.recognitionStrategies.tags.taskTags;
|
||||
expect(finalTags).toContain('#task');
|
||||
expect(finalTags).toContain('#work');
|
||||
expect(finalTags).toContain('#existing');
|
||||
// Should not have duplicates
|
||||
expect(finalTags.filter(tag => tag === '#task')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should merge metadata fields without duplicates', () => {
|
||||
testSettings.fileParsingConfig.metadataFieldsToParseAsTasks = ['dueDate', 'custom'];
|
||||
testSettings.fileSource.recognitionStrategies.metadata.taskFields = ['dueDate', 'existing'];
|
||||
|
||||
const result = migrateFileParsingSettings(testSettings);
|
||||
|
||||
const finalFields = testSettings.fileSource.recognitionStrategies.metadata.taskFields;
|
||||
expect(finalFields).toContain('dueDate');
|
||||
expect(finalFields).toContain('custom');
|
||||
expect(finalFields).toContain('existing');
|
||||
// Should not have duplicates
|
||||
expect(finalFields.filter(field => field === 'dueDate')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
90
src/styles/settings-migration.css
Normal file
90
src/styles/settings-migration.css
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/* Settings Migration Styling */
|
||||
|
||||
.setting-item-info.mod-warning {
|
||||
border: 1px solid var(--background-modifier-error);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.setting-item-info.mod-warning .setting-item-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-error);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.setting-item-info.mod-warning .setting-item-description {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.setting-item-info.mod-warning .setting-item-description p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.setting-item-info.mod-warning .setting-item-control {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-item-info.mod-warning button.mod-cta {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setting-item-info.mod-warning button.mod-cta:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.setting-item-info.mod-warning button.mod-muted {
|
||||
background-color: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.setting-item-info.mod-warning button.mod-muted:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
/* Migration Success Indicator */
|
||||
.setting-item-info.mod-success {
|
||||
background-color: var(--color-green-bg);
|
||||
border: 1px solid var(--color-green);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.setting-item-info.mod-success .setting-item-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-green);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* FileSource Integration Notice */
|
||||
.file-source-notice {
|
||||
background-color: var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.file-source-notice .notice-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-source-notice .notice-description {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
298
src/utils/SettingsMigration.ts
Normal file
298
src/utils/SettingsMigration.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
/**
|
||||
* Settings Migration Utility
|
||||
*
|
||||
* Handles migration of duplicate and overlapping settings to consolidate
|
||||
* configuration and eliminate confusion for users.
|
||||
*/
|
||||
|
||||
import type { TaskProgressBarSettings } from "../common/setting-definition";
|
||||
import type { FileSourceConfiguration } from "../types/file-source";
|
||||
|
||||
export interface MigrationResult {
|
||||
migrated: boolean;
|
||||
details: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate duplicate settings to unified FileSource configuration
|
||||
*/
|
||||
export function migrateFileParsingSettings(settings: TaskProgressBarSettings): MigrationResult {
|
||||
const result: MigrationResult = {
|
||||
migrated: false,
|
||||
details: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
// Ensure fileSource exists
|
||||
if (!settings.fileSource) {
|
||||
settings.fileSource = createDefaultFileSourceConfig();
|
||||
result.details.push("Created default FileSource configuration");
|
||||
}
|
||||
|
||||
// Migration 1: fileParsingConfig.enableFileMetadataParsing → fileSource.enabled
|
||||
if (settings.fileParsingConfig?.enableFileMetadataParsing === true && !settings.fileSource.enabled) {
|
||||
settings.fileSource.enabled = true;
|
||||
result.migrated = true;
|
||||
result.details.push("Migrated: Enable file metadata parsing → Enable FileSource");
|
||||
}
|
||||
|
||||
// Migration 2: Tag-based parsing
|
||||
if (settings.fileParsingConfig?.enableTagBasedTaskParsing === true) {
|
||||
if (!settings.fileSource.recognitionStrategies.tags.enabled) {
|
||||
settings.fileSource.recognitionStrategies.tags.enabled = true;
|
||||
result.migrated = true;
|
||||
result.details.push("Migrated: Tag-based task parsing → FileSource tag recognition");
|
||||
}
|
||||
|
||||
// Migrate tag patterns
|
||||
if (settings.fileParsingConfig.tagsToParseAsTasks?.length > 0) {
|
||||
settings.fileSource.recognitionStrategies.tags.taskTags = [
|
||||
...new Set([
|
||||
...settings.fileSource.recognitionStrategies.tags.taskTags,
|
||||
...settings.fileParsingConfig.tagsToParseAsTasks
|
||||
])
|
||||
];
|
||||
result.details.push("Migrated: Tag patterns for task recognition");
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 3: Metadata fields
|
||||
if (settings.fileParsingConfig?.metadataFieldsToParseAsTasks?.length > 0) {
|
||||
if (!settings.fileSource.recognitionStrategies.metadata.enabled) {
|
||||
settings.fileSource.recognitionStrategies.metadata.enabled = true;
|
||||
result.migrated = true;
|
||||
result.details.push("Migrated: Metadata parsing → FileSource metadata recognition");
|
||||
}
|
||||
|
||||
settings.fileSource.recognitionStrategies.metadata.taskFields = [
|
||||
...new Set([
|
||||
...settings.fileSource.recognitionStrategies.metadata.taskFields,
|
||||
...settings.fileParsingConfig.metadataFieldsToParseAsTasks
|
||||
])
|
||||
];
|
||||
result.details.push("Migrated: Metadata fields for task recognition");
|
||||
}
|
||||
|
||||
// Migration 4: Worker processing settings
|
||||
if (settings.fileParsingConfig?.enableWorkerProcessing === true) {
|
||||
settings.fileSource.performance.enableWorkerProcessing = true;
|
||||
result.migrated = true;
|
||||
result.details.push("Migrated: Worker processing setting");
|
||||
}
|
||||
|
||||
// Migration 5: Default task status
|
||||
if (settings.fileParsingConfig?.defaultTaskStatus) {
|
||||
settings.fileSource.fileTaskProperties.defaultStatus = settings.fileParsingConfig.defaultTaskStatus;
|
||||
result.details.push("Migrated: Default task status");
|
||||
}
|
||||
|
||||
// Migration 6: Task content source
|
||||
if (settings.fileParsingConfig?.taskContentFromMetadata) {
|
||||
if (settings.fileParsingConfig.taskContentFromMetadata === "title") {
|
||||
settings.fileSource.fileTaskProperties.contentSource = "title";
|
||||
} else {
|
||||
settings.fileSource.fileTaskProperties.contentSource = "custom";
|
||||
settings.fileSource.fileTaskProperties.customContentField = settings.fileParsingConfig.taskContentFromMetadata;
|
||||
}
|
||||
result.details.push("Migrated: Task content source");
|
||||
}
|
||||
|
||||
// Check for conflicts and warn user
|
||||
if (settings.fileParsingConfig?.enableFileMetadataParsing === true && settings.fileSource.enabled === false) {
|
||||
result.warnings.push("Conflict detected: File metadata parsing enabled but FileSource disabled");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up deprecated settings after successful migration
|
||||
*/
|
||||
export function cleanupDeprecatedSettings(settings: TaskProgressBarSettings): MigrationResult {
|
||||
const result: MigrationResult = {
|
||||
migrated: false,
|
||||
details: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
// Only clean up if FileSource is enabled (migration was successful)
|
||||
if (!settings.fileSource?.enabled) {
|
||||
result.warnings.push("Skipping cleanup: FileSource not enabled");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Reset deprecated fileParsingConfig flags that are now handled by FileSource
|
||||
if (settings.fileParsingConfig?.enableFileMetadataParsing === true) {
|
||||
settings.fileParsingConfig.enableFileMetadataParsing = false;
|
||||
result.migrated = true;
|
||||
result.details.push("Disabled deprecated: Enable file metadata parsing");
|
||||
}
|
||||
|
||||
if (settings.fileParsingConfig?.enableTagBasedTaskParsing === true) {
|
||||
settings.fileParsingConfig.enableTagBasedTaskParsing = false;
|
||||
result.migrated = true;
|
||||
result.details.push("Disabled deprecated: Tag-based task parsing");
|
||||
}
|
||||
|
||||
// Clear migrated arrays to avoid confusion
|
||||
if (settings.fileParsingConfig?.metadataFieldsToParseAsTasks?.length > 0) {
|
||||
settings.fileParsingConfig.metadataFieldsToParseAsTasks = [];
|
||||
result.details.push("Cleared deprecated: Metadata fields array");
|
||||
}
|
||||
|
||||
if (settings.fileParsingConfig?.tagsToParseAsTasks?.length > 0) {
|
||||
settings.fileParsingConfig.tagsToParseAsTasks = [];
|
||||
result.details.push("Cleared deprecated: Task tags array");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate duplicate project settings
|
||||
*/
|
||||
export function migrateProjectSettings(settings: TaskProgressBarSettings): MigrationResult {
|
||||
const result: MigrationResult = {
|
||||
migrated: false,
|
||||
details: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
// Check for duplicate project detection methods
|
||||
if (settings.projectConfig?.enableEnhancedProject && settings.fileSource?.enabled) {
|
||||
result.warnings.push("Both enhanced project features and FileSource are enabled - consider consolidating");
|
||||
}
|
||||
|
||||
// Note: fileSourceConfig was removed - if any code was using it,
|
||||
// it should now use fileSource instead
|
||||
result.details.push("Project configuration uses projectConfig for enhanced features");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all migrations
|
||||
*/
|
||||
export function runAllMigrations(settings: TaskProgressBarSettings): MigrationResult {
|
||||
const results: MigrationResult[] = [];
|
||||
|
||||
// Run individual migrations
|
||||
results.push(migrateFileParsingSettings(settings));
|
||||
results.push(migrateProjectSettings(settings));
|
||||
|
||||
// Only cleanup if migrations were successful
|
||||
const hasSuccessfulMigrations = results.some(r => r.migrated);
|
||||
if (hasSuccessfulMigrations) {
|
||||
results.push(cleanupDeprecatedSettings(settings));
|
||||
}
|
||||
|
||||
// Combine results
|
||||
return {
|
||||
migrated: results.some(r => r.migrated),
|
||||
details: results.flatMap(r => r.details),
|
||||
warnings: results.flatMap(r => r.warnings)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default FileSource configuration
|
||||
*/
|
||||
function createDefaultFileSourceConfig(): FileSourceConfiguration {
|
||||
return {
|
||||
enabled: false,
|
||||
recognitionStrategies: {
|
||||
metadata: {
|
||||
enabled: false,
|
||||
taskFields: ["dueDate", "status", "priority", "assigned"],
|
||||
requireAllFields: false
|
||||
},
|
||||
tags: {
|
||||
enabled: false,
|
||||
taskTags: ["#task", "#actionable", "#todo"],
|
||||
matchMode: "exact"
|
||||
},
|
||||
templates: {
|
||||
enabled: false,
|
||||
templatePaths: ["Templates/Task Template.md"],
|
||||
checkTemplateMetadata: true
|
||||
},
|
||||
paths: {
|
||||
enabled: false,
|
||||
taskPaths: ["Projects/", "Tasks/"],
|
||||
matchMode: "prefix"
|
||||
}
|
||||
},
|
||||
fileTaskProperties: {
|
||||
contentSource: "filename",
|
||||
stripExtension: true,
|
||||
defaultStatus: " ",
|
||||
defaultPriority: undefined
|
||||
},
|
||||
relationships: {
|
||||
enableChildRelationships: true,
|
||||
enableMetadataInheritance: true,
|
||||
inheritanceFields: ["project", "priority", "context"]
|
||||
},
|
||||
performance: {
|
||||
enableWorkerProcessing: true,
|
||||
enableCaching: true,
|
||||
cacheTTL: 300000
|
||||
},
|
||||
advanced: {
|
||||
excludePatterns: ["**/.obsidian/**", "**/node_modules/**"],
|
||||
maxFileSize: 1048576
|
||||
},
|
||||
statusMapping: {
|
||||
enabled: true,
|
||||
metadataToSymbol: {
|
||||
'completed': 'x',
|
||||
'done': 'x',
|
||||
'in-progress': '/',
|
||||
'planned': '?',
|
||||
'cancelled': '-',
|
||||
'not-started': ' '
|
||||
},
|
||||
symbolToMetadata: {
|
||||
'x': 'completed',
|
||||
'/': 'in-progress',
|
||||
'?': 'planned',
|
||||
'-': 'cancelled',
|
||||
' ': 'not-started'
|
||||
},
|
||||
autoDetect: true,
|
||||
caseSensitive: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if settings have duplicates that need migration
|
||||
*/
|
||||
export function hasSettingsDuplicates(settings: TaskProgressBarSettings): boolean {
|
||||
// Check for the main duplicate: file metadata parsing enabled in both systems
|
||||
const fileParsingEnabled = settings.fileParsingConfig?.enableFileMetadataParsing === true;
|
||||
const fileSourceEnabled = settings.fileSource?.enabled === true;
|
||||
|
||||
if (fileParsingEnabled && !fileSourceEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for tag parsing duplicates
|
||||
const tagParsingEnabled = settings.fileParsingConfig?.enableTagBasedTaskParsing === true;
|
||||
const tagRecognitionEnabled = settings.fileSource?.recognitionStrategies?.tags?.enabled === true;
|
||||
|
||||
if (tagParsingEnabled && !tagRecognitionEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for worker processing duplicates
|
||||
const workerParsingEnabled = settings.fileParsingConfig?.enableWorkerProcessing === true;
|
||||
const workerSourceEnabled = settings.fileSource?.performance?.enableWorkerProcessing === true;
|
||||
|
||||
if (workerParsingEnabled !== workerSourceEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
167
src/utils/SettingsMigrationManager.ts
Normal file
167
src/utils/SettingsMigrationManager.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* Settings Migration Manager
|
||||
*
|
||||
* Integrates with the main plugin to handle automatic migration of duplicate
|
||||
* settings when the plugin loads or settings are updated.
|
||||
*/
|
||||
|
||||
import { Notice } from "obsidian";
|
||||
import type TaskProgressBarPlugin from "../index";
|
||||
import {
|
||||
runAllMigrations,
|
||||
hasSettingsDuplicates,
|
||||
type MigrationResult
|
||||
} from "./SettingsMigration";
|
||||
import { t } from "../translations/helper";
|
||||
|
||||
export class SettingsMigrationManager {
|
||||
private plugin: TaskProgressBarPlugin;
|
||||
private migrationCompleted = false;
|
||||
|
||||
constructor(plugin: TaskProgressBarPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and run migrations when plugin loads
|
||||
*/
|
||||
async onPluginLoad(): Promise<void> {
|
||||
try {
|
||||
if (hasSettingsDuplicates(this.plugin.settings)) {
|
||||
await this.runMigrationWithUserConsent();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Settings migration failed:", error);
|
||||
new Notice(t("Settings migration failed. Please check console for details."));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for duplicates before saving settings
|
||||
*/
|
||||
async onBeforeSave(): Promise<boolean> {
|
||||
// If migration was already completed this session, don't run again
|
||||
if (this.migrationCompleted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we need to migrate
|
||||
if (hasSettingsDuplicates(this.plugin.settings)) {
|
||||
const result = await this.runSilentMigration();
|
||||
return result.migrated;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run migration with user notification
|
||||
*/
|
||||
private async runMigrationWithUserConsent(): Promise<void> {
|
||||
const result = runAllMigrations(this.plugin.settings);
|
||||
|
||||
if (result.migrated) {
|
||||
this.migrationCompleted = true;
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Show user-friendly notice
|
||||
new Notice(
|
||||
t("Task Genius: Settings have been automatically migrated to remove duplicates. ") +
|
||||
t("FileSource is now the unified system for file-based task recognition."),
|
||||
10000
|
||||
);
|
||||
|
||||
// Log details for advanced users
|
||||
console.log("Task Genius Settings Migration:", {
|
||||
details: result.details,
|
||||
warnings: result.warnings
|
||||
});
|
||||
}
|
||||
|
||||
// Show warnings if any
|
||||
if (result.warnings.length > 0) {
|
||||
console.warn("Task Genius Migration Warnings:", result.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run migration silently without user notification
|
||||
*/
|
||||
private async runSilentMigration(): Promise<MigrationResult> {
|
||||
const result = runAllMigrations(this.plugin.settings);
|
||||
|
||||
if (result.migrated) {
|
||||
this.migrationCompleted = true;
|
||||
// Settings will be saved by the calling function
|
||||
console.log("Task Genius: Silent settings migration completed", result.details);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force migration (for manual execution)
|
||||
*/
|
||||
async forceMigration(): Promise<MigrationResult> {
|
||||
const result = runAllMigrations(this.plugin.settings);
|
||||
|
||||
if (result.migrated) {
|
||||
this.migrationCompleted = true;
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
new Notice(
|
||||
t("Settings migration completed: ") + result.details.length + t(" changes applied"),
|
||||
5000
|
||||
);
|
||||
} else {
|
||||
new Notice(t("No settings migration needed"), 3000);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has conflicting settings
|
||||
*/
|
||||
hasConflicts(): boolean {
|
||||
return hasSettingsDuplicates(this.plugin.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get migration status for display in settings
|
||||
*/
|
||||
getMigrationStatus(): {
|
||||
needsMigration: boolean;
|
||||
completedThisSession: boolean;
|
||||
conflictCount: number;
|
||||
} {
|
||||
const needsMigration = hasSettingsDuplicates(this.plugin.settings);
|
||||
|
||||
// Count conflicts
|
||||
let conflictCount = 0;
|
||||
const settings = this.plugin.settings;
|
||||
|
||||
if (settings.fileParsingConfig?.enableFileMetadataParsing && !settings.fileSource?.enabled) {
|
||||
conflictCount++;
|
||||
}
|
||||
if (settings.fileParsingConfig?.enableTagBasedTaskParsing && !settings.fileSource?.recognitionStrategies?.tags?.enabled) {
|
||||
conflictCount++;
|
||||
}
|
||||
if (settings.fileParsingConfig?.enableWorkerProcessing !== settings.fileSource?.performance?.enableWorkerProcessing) {
|
||||
conflictCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
needsMigration,
|
||||
completedThisSession: this.migrationCompleted,
|
||||
conflictCount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset migration state (for testing)
|
||||
*/
|
||||
resetMigrationState(): void {
|
||||
this.migrationCompleted = false;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue