feat(ui): add export and import features functionality

- Introduced new routes for exporting and importing features, enhancing project management capabilities.
- Added UI components for export and import dialogs, allowing users to easily manage feature data.
- Updated HTTP API client to support export and import operations with appropriate options and responses.
- Enhanced board view with controls for triggering export and import actions, improving user experience.
- Defined new types for feature export and import, ensuring type safety and clarity in data handling.
This commit is contained in:
Shirone
2026-01-21 13:00:34 +01:00
parent db71dc9aa5
commit 2214c2700b
16 changed files with 2431 additions and 15 deletions

View File

@@ -0,0 +1,215 @@
/**
* POST /import endpoint - Import features from JSON or YAML format
*/
import type { Request, Response } from 'express';
import type { FeatureLoader } from '../../../services/feature-loader.js';
import type { FeatureImportResult, Feature, FeatureExport } from '@automaker/types';
import { getFeatureExportService } from '../../../services/feature-export-service.js';
import { getErrorMessage, logError } from '../common.js';
interface ImportRequest {
projectPath: string;
/** Raw JSON or YAML string containing feature data */
data: string;
/** Whether to overwrite existing features with same ID */
overwrite?: boolean;
/** Whether to preserve branch info from imported features */
preserveBranchInfo?: boolean;
/** Optional category to assign to all imported features */
targetCategory?: string;
}
interface ConflictCheckRequest {
projectPath: string;
/** Raw JSON or YAML string containing feature data */
data: string;
}
interface ConflictInfo {
featureId: string;
title?: string;
existingTitle?: string;
hasConflict: boolean;
}
export function createImportHandler(featureLoader: FeatureLoader) {
const exportService = getFeatureExportService();
return async (req: Request, res: Response): Promise<void> => {
try {
const {
projectPath,
data,
overwrite = false,
preserveBranchInfo = false,
targetCategory,
} = req.body as ImportRequest;
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!data) {
res.status(400).json({ success: false, error: 'data is required' });
return;
}
// Detect format and parse the data
const format = exportService.detectFormat(data);
if (!format) {
res.status(400).json({
success: false,
error: 'Invalid data format. Expected valid JSON or YAML.',
});
return;
}
const parsed = exportService.parseImportData(data);
if (!parsed) {
res.status(400).json({
success: false,
error: 'Failed to parse import data. Ensure it is valid JSON or YAML.',
});
return;
}
// Determine if this is a single feature or bulk import
const isBulkImport =
'features' in parsed && Array.isArray((parsed as { features: unknown }).features);
let results: FeatureImportResult[];
if (isBulkImport) {
// Bulk import
results = await exportService.importFeatures(projectPath, data, {
overwrite,
preserveBranchInfo,
targetCategory,
});
} else {
// Single feature import - we know it's not a bulk export at this point
// It must be either a Feature or FeatureExport
const singleData = parsed as Feature | FeatureExport;
const result = await exportService.importFeature(projectPath, {
data: singleData,
overwrite,
preserveBranchInfo,
targetCategory,
});
results = [result];
}
const successCount = results.filter((r) => r.success).length;
const failureCount = results.filter((r) => !r.success).length;
const allSuccessful = failureCount === 0;
res.json({
success: allSuccessful,
importedCount: successCount,
failedCount: failureCount,
results,
});
} catch (error) {
logError(error, 'Import features failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}
/**
* Create handler for checking conflicts before import
*/
export function createConflictCheckHandler(featureLoader: FeatureLoader) {
const exportService = getFeatureExportService();
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, data } = req.body as ConflictCheckRequest;
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!data) {
res.status(400).json({ success: false, error: 'data is required' });
return;
}
// Parse the import data
const format = exportService.detectFormat(data);
if (!format) {
res.status(400).json({
success: false,
error: 'Invalid data format. Expected valid JSON or YAML.',
});
return;
}
const parsed = exportService.parseImportData(data);
if (!parsed) {
res.status(400).json({
success: false,
error: 'Failed to parse import data.',
});
return;
}
// Extract features from the data
type FeatureExportType = { feature: { id: string; title?: string } };
type BulkExportType = { features: FeatureExportType[] };
type RawFeatureType = { id: string; title?: string };
let featuresToCheck: Array<{ id: string; title?: string }> = [];
if ('features' in parsed && Array.isArray((parsed as BulkExportType).features)) {
// Bulk export format
featuresToCheck = (parsed as BulkExportType).features.map((f) => ({
id: f.feature.id,
title: f.feature.title,
}));
} else if ('feature' in parsed) {
// Single FeatureExport format
const featureExport = parsed as FeatureExportType;
featuresToCheck = [
{
id: featureExport.feature.id,
title: featureExport.feature.title,
},
];
} else if ('id' in parsed) {
// Raw Feature format
const rawFeature = parsed as RawFeatureType;
featuresToCheck = [{ id: rawFeature.id, title: rawFeature.title }];
}
// Check each feature for conflicts
const conflicts: ConflictInfo[] = [];
for (const feature of featuresToCheck) {
const existing = await featureLoader.get(projectPath, feature.id);
conflicts.push({
featureId: feature.id,
title: feature.title,
existingTitle: existing?.title,
hasConflict: !!existing,
});
}
const hasConflicts = conflicts.some((c) => c.hasConflict);
res.json({
success: true,
hasConflicts,
conflicts,
totalFeatures: featuresToCheck.length,
conflictCount: conflicts.filter((c) => c.hasConflict).length,
});
} catch (error) {
logError(error, 'Conflict check failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}