Skip to content

feat: add app import preview flow#239

Open
super-niuma-001 wants to merge 1 commit intodotnetcore:masterfrom
super-niuma-001:feat/issue-235-import-flow
Open

feat: add app import preview flow#239
super-niuma-001 wants to merge 1 commit intodotnetcore:masterfrom
super-niuma-001:feat/issue-235-import-flow

Conversation

@super-niuma-001
Copy link
Copy Markdown
Contributor

Closes #235

Summary

  • add app import preview and import endpoints for exported app JSON files
  • validate duplicate ids/names, missing parent apps, and cyclic inheritance before import
  • import apps in topological order and add a UI modal for upload + preview

Testing

  • dotnet test test/ApiSiteTests/ApiSiteTests.csproj --filter TestAppController
  • NODE_OPTIONS=--openssl-legacy-provider npm run build

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an “app import” workflow that lets users upload an exported apps JSON file, preview validation results, and then import apps/configs in dependency order.

Changes:

  • Added API endpoints to preview and import exported app JSON files, including validation (duplicates/missing parents/cycles) and topological ordering.
  • Added UI import modal flow (upload → preview table → import) and supporting request/types/i18n.
  • Added API tests covering preview validation and import ordering/config creation.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/ApiSiteTests/TestAppController.cs Adds tests for preview validation errors and import ordering/config creation.
src/AgileConfig.Server.UI/react-ui-antd/src/pages/Apps/service.ts Adds client API calls for preview-import (multipart upload) and import (JSON body).
src/AgileConfig.Server.UI/react-ui-antd/src/pages/Apps/index.tsx Adds “Import” button and toggles the import modal.
src/AgileConfig.Server.UI/react-ui-antd/src/pages/Apps/data.d.ts Defines types for import file schema and preview response payload.
src/AgileConfig.Server.UI/react-ui-antd/src/pages/Apps/comps/AppImport.tsx Implements the upload/preview/import modal UX.
src/AgileConfig.Server.UI/react-ui-antd/src/locales/zh-CN/pages.ts Adds zh-CN strings for import flow UI.
src/AgileConfig.Server.UI/react-ui-antd/src/locales/en-US/pages.ts Adds en-US strings for import flow UI.
src/AgileConfig.Server.Apisite/Models/AppExportVM.cs Adds import request model and preview view models.
src/AgileConfig.Server.Apisite/Controllers/AppController.cs Implements PreviewImport/Import endpoints, validation, and topological sort.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

preview.Errors.Add($"App '{item.App.Id}' references missing parent '{parentId}'. Parent must already exist or be included in the import file.");
}
}

Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildImportPreviewAsync can add validation errors for missing/duplicate AppIds but still proceeds to TryTopologicalSort. ToDictionary(x => x.App.Id, ...) will throw when an AppId is null/whitespace or duplicated, turning a user error into a 500. Mandatory fix: short-circuit before topo sort when preview.Errors.Any(), and/or pass only items with non-empty unique AppIds into TryTopologicalSort so preview errors are returned as JSON instead of throwing.

Suggested change
if (preview.Errors.Any()) return preview;

Copilot uses AI. Check for mistakes.
Comment on lines +581 to +589
private static Dictionary<string, int> TryTopologicalSort(List<AppExportItemVM> appItems, HashSet<string> importedAppIds, List<string> errors)
{
var dependencyMap = appItems.ToDictionary(
x => x.App.Id,
x => (x.App.InheritancedApps ?? new List<string>())
.Where(parentId => !string.IsNullOrWhiteSpace(parentId) && importedAppIds.Contains(parentId))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(),
StringComparer.OrdinalIgnoreCase);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildImportPreviewAsync can add validation errors for missing/duplicate AppIds but still proceeds to TryTopologicalSort. ToDictionary(x => x.App.Id, ...) will throw when an AppId is null/whitespace or duplicated, turning a user error into a 500. Mandatory fix: short-circuit before topo sort when preview.Errors.Any(), and/or pass only items with non-empty unique AppIds into TryTopologicalSort so preview errors are returned as JSON instead of throwing.

Copilot uses AI. Check for mistakes.
Comment on lines +434 to +435
var inheritanceApps = BuildInheritanceLinks(importItem.App.InheritancedApps, app.Id);
await _appService.AddAsync(app, inheritanceApps);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import endpoint always returns success = true even if _appService.AddAsync(...) or _configService.AddAsync(...) fails (these return bool). This can silently produce partial imports and inconsistent state. Mandatory fix: check return values and fail fast (return success=false with an error message), and consider wrapping the whole import in a transaction/unit-of-work so apps/configs are imported atomically.

Copilot uses AI. Check for mistakes.
OnlineStatus = OnlineStatus.WaitPublish,
EditStatus = EditStatus.Add
};
await _configService.AddAsync(config, envConfigs.Key);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import endpoint always returns success = true even if _appService.AddAsync(...) or _configService.AddAsync(...) fails (these return bool). This can silently produce partial imports and inconsistent state. Mandatory fix: check return values and fail fast (return success=false with an error message), and consider wrapping the whole import in a transaction/unit-of-work so apps/configs are imported atomically.

Copilot uses AI. Check for mistakes.
Comment on lines +460 to +464
return Json(new
{
success = true,
data = preview
});
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import endpoint always returns success = true even if _appService.AddAsync(...) or _configService.AddAsync(...) fails (these return bool). This can silently produce partial imports and inconsistent state. Mandatory fix: check return values and fail fast (return success=false with an error message), and consider wrapping the whole import in a transaction/unit-of-work so apps/configs are imported atomically.

Copilot uses AI. Check for mistakes.
Comment on lines +485 to +493
private async Task<AppExportFileVM> ReadImportFileAsync(IFormFile file)
{
if (file == null || file.Length == 0) throw new ArgumentException("file");

using var stream = file.OpenReadStream();
using var reader = new System.IO.StreamReader(stream, Encoding.UTF8);
var content = await reader.ReadToEndAsync();
var importFile = JsonConvert.DeserializeObject<AppExportFileVM>(content);
if (importFile == null) throw new ArgumentException("file");
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReadImportFileAsync reads the entire uploaded file into memory with no upper bound and throws ArgumentException("file") on bad input, which will surface as 500s unless handled elsewhere. Mandatory fix: enforce a maximum upload size (reject large files early using file.Length and/or request limits) and return a structured 400-style JSON error for invalid/empty/invalid-JSON uploads instead of throwing generic exceptions.

Copilot uses AI. Check for mistakes.
return request<{ success: boolean; data: AppImportPreviewResult; message?: string }>('app/PreviewImport', {
method: 'POST',
data: formData,
requestType: 'form',
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are sending a FormData payload but set requestType: 'form' (typically URL-encoded). With @umijs/request, multipart uploads generally require requestType: 'formData' (or omitting requestType so it can infer from FormData). As-is, the file may not be transmitted correctly and the API will receive file=null.

Suggested change
requestType: 'form',
requestType: 'formData',

Copilot uses AI. Check for mistakes.
Group = importItem.App.Group,
Secret = importItem.App.Secret,
Enabled = importItem.App.Enabled,
Type = importItem.App.Inheritanced ? AppType.Inheritance : AppType.PRIVATE,
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import derives App.Type solely from Inheritanced, ignoring the exported type field included in the import schema (and likely in AppExportAppVM). This can change app types on import (e.g., any non-inheritance type becomes PRIVATE). Mandatory fix: map the exported Type value to App.Type (with validation/fallback if the value is unknown) instead of hardcoding it based on Inheritanced.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

完整的导入导出功能

2 participants