mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
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:
215
apps/server/src/routes/features/routes/import.ts
Normal file
215
apps/server/src/routes/features/routes/import.ts
Normal 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) });
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user