Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ internal/appinfo/update_config.json

*.asc
dist/
.gocache
124 changes: 63 additions & 61 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,6 @@ type RequestOptions struct {
PostResponseScript string `json:"postResponseScript,omitempty"`
}

type OpenAPIImportCollectionResult struct {
Warnings []string `json:"warnings"`
BasePath string `json:"basePath"`
Servers []string `json:"servers"`
}

func (a *App) GetAppInfo() appinfo.AppInfo {
return appinfo.GetAppInfo(wailsJSON)
}
Expand Down Expand Up @@ -446,44 +440,33 @@ func (a *App) SetSelectedEnvironment(name string) error {

// Collection Management Methods

// ImportPostmanCollection imports a Postman v2.1 collection file into Solo.
func (a *App) ImportPostmanCollection(path string) error {
imp := importer.NewPostmanImporter()

coll, err := imp.Import(path)
if err != nil {
return fmt.Errorf("failed to import collection: %w", err)
}

// Save the imported collection using the existing manager.
// UpdateCollection will write the entire JSON object to disk using its Name.
if err := a.collectionManager.UpdateCollection(*coll); err != nil {
return fmt.Errorf("failed to save imported collection: %w", err)
// ImportPostmanCollections imports Postman v2.1 collection files into Solo.
func (a *App) ImportPostmanCollections(paths []string) (collection.BatchImportResult, error) {
if a.collectionManager == nil {
return collection.BatchImportResult{}, fmt.Errorf("collection manager not initialized")
}

return nil
imp := importer.NewPostmanImporter()
return a.collectionManager.ImportBatch(paths, func(path string) (*collection.Collection, []string, error) {
coll, err := imp.Import(path)
return coll, nil, err
})
}

// ImportOpenAPICollection imports an OpenAPI 3.x or Swagger 2.x collection
// (JSON or YAML) into Solo.
// Returns warning messages and source metadata used by the frontend to build baseUrl.
func (a *App) ImportOpenAPICollection(path string) (OpenAPIImportCollectionResult, error) {
imp := importer.NewOpenAPIImporter()

result, err := imp.Import(path)
if err != nil {
return OpenAPIImportCollectionResult{}, fmt.Errorf("failed to import OpenAPI collection: %w", err)
}

if err := a.collectionManager.UpdateCollection(*result.Collection); err != nil {
return OpenAPIImportCollectionResult{}, fmt.Errorf("failed to save imported OpenAPI collection: %w", err)
// ImportOpenAPICollections imports OpenAPI 3.x or Swagger 2.x collection files into Solo.
func (a *App) ImportOpenAPICollections(paths []string) (collection.BatchImportResult, error) {
if a.collectionManager == nil {
return collection.BatchImportResult{}, fmt.Errorf("collection manager not initialized")
}

return OpenAPIImportCollectionResult{
Warnings: result.Warnings,
BasePath: result.BasePath,
Servers: result.Servers,
}, nil
imp := importer.NewOpenAPIImporter()
return a.collectionManager.ImportBatch(paths, func(path string) (*collection.Collection, []string, error) {
result, err := imp.Import(path)
if err != nil {
return nil, nil, err
}
return result.Collection, result.Warnings, nil
})
}

// ImportCurlRequest parses a cURL command string and adds the resulting request
Expand Down Expand Up @@ -586,34 +569,41 @@ func (a *App) ExportEnvironment(environmentName string) error {
return os.WriteFile(path, data, 0644)
}

// ImportSoloCollection imports a Solo-native collection JSON file.
// If overwrite is false and a collection with the same name already exists,
// returns an error of the form "collection <name> already exists".
func (a *App) ImportSoloCollection(path string, overwrite bool) error {
func (a *App) loadSoloCollectionForImport(path string, overwrite bool) (*collection.Collection, []string, error) {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
return nil, nil, fmt.Errorf("failed to read file: %w", err)
}

var coll collection.Collection
if err := json.Unmarshal(data, &coll); err != nil {
return fmt.Errorf("invalid %s collection file: %w", tools.APP_NAME, err)
return nil, nil, fmt.Errorf("invalid %s collection file: %w", tools.APP_NAME, err)
}
if coll.Name == "" {
return fmt.Errorf("collection file has no name field")
return nil, nil, fmt.Errorf("collection file has no name field")
}

if !overwrite {
existing, _ := a.collectionManager.LoadCollection(coll.Name)
if existing != nil {
return fmt.Errorf("collection %s already exists", coll.Name)
return nil, nil, fmt.Errorf("collection %s already exists", coll.Name)
}
}

if err := a.collectionManager.UpdateCollection(coll); err != nil {
return fmt.Errorf("failed to save collection: %w", err)
return &coll, nil, nil
}

// ImportSoloCollections imports Solo-native collection JSON files into Solo.
func (a *App) ImportSoloCollections(paths []string) (collection.BatchImportResult, error) {
if a.collectionManager == nil {
return collection.BatchImportResult{}, fmt.Errorf("collection manager not initialized")
}
return nil

importOne := func(path string) (*collection.Collection, []string, error) {
return a.loadSoloCollectionForImport(path, true)
}

return a.collectionManager.ImportBatch(paths, importOne)
}

// ImportSoloEnvironment imports a Solo-native environment JSON file.
Expand Down Expand Up @@ -727,20 +717,17 @@ func (a *App) ExportLogsZip() (bool, error) {
return true, nil
}

// ImportBrunoCollection imports a Bruno collection from a directory.
func (a *App) ImportBrunoCollection(path string) error {
imp := importer.NewBrunoImporter()

coll, err := imp.Import(path)
if err != nil {
return fmt.Errorf("failed to import Bruno collection: %w", err)
// ImportBrunoCollections imports Bruno collection directories into Solo.
func (a *App) ImportBrunoCollections(paths []string) (collection.BatchImportResult, error) {
if a.collectionManager == nil {
return collection.BatchImportResult{}, fmt.Errorf("collection manager not initialized")
}

if err := a.collectionManager.UpdateCollection(*coll); err != nil {
return fmt.Errorf("failed to save imported Bruno collection: %w", err)
}

return nil
imp := importer.NewBrunoImporter()
return a.collectionManager.ImportBatch(paths, func(path string) (*collection.Collection, []string, error) {
coll, err := imp.Import(path)
return coll, nil, err
})
}

// ImportPostmanEnvironment imports a Postman environment JSON file.
Expand Down Expand Up @@ -1292,6 +1279,21 @@ func (a *App) SelectFile(title, patterns, displayName string) (string, error) {
})
}

// SelectFiles opens a native file dialog to select multiple files.
// It takes a title for the dialog and a pattern for file filtering (e.g., "*.pem;*.crt").
func (a *App) SelectFiles(title, patterns, displayName string) ([]string, error) {
slog.Debug("Opening multiple files dialog", "title", title, "patterns", patterns)
return runtime.OpenMultipleFilesDialog(a.ctx, runtime.OpenDialogOptions{
Title: title,
Filters: []runtime.FileFilter{
{
DisplayName: displayName,
Pattern: patterns, // e.g., "*.pem;*.crt;*.key"
},
},
})
}

// ── Git helpers ──────────────────────────────────────────────────────────────

// resolveGitCollectionDir resolves a collectionId to its local git repo dir
Expand Down
61 changes: 18 additions & 43 deletions frontend/src/lib/components/Collections/CollectionList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
const deleteRequestModal = modalStack.createModal("collections-delete-request");
const importCollectionModal = modalStack.createModal("collections-import");
const collectionVariablesModal = modalStack.createModal("collections-variables");
const soloCollectionOverwriteModal = modalStack.createModal("collections-solo-overwrite");
const exportCollectionModal = modalStack.createModal("collections-export");
let exportCollectionTargetName = $state<string | null>(null);
let gitImportActionState: { loading: boolean; disabled: boolean; submit: () => void } | null =
Expand Down Expand Up @@ -138,10 +137,6 @@
}
});

$effect(() => {
soloCollectionOverwriteModal.open = !!collectionImportStoreState.soloCollectionOverwriteName;
});

$effect(() => {
if (
collectionVariablesModal.open &&
Expand Down Expand Up @@ -667,23 +662,22 @@
async function runPendingLocalImport() {
await collectionImportStore.runPendingLocalImport();

if (
!collectionImportStoreState.pendingLocalImport &&
!collectionImportStoreState.soloCollectionOverwriteName
) {
if (!collectionImportStoreState.pendingLocalImport) {
importCollectionModal.open = false;
}
}

async function confirmSoloCollectionOverwrite() {
await collectionImportStore.confirmSoloCollectionOverwrite();

if (
!collectionImportStoreState.pendingLocalImport &&
!collectionImportStoreState.soloCollectionOverwriteName
) {
importCollectionModal.open = false;
async function handleLocalImportSelection(
format: CollectionLocalImportFormat,
droppedPaths?: string[]
) {
if (!droppedPaths) {
collectionImportStoreState.selectedLocalFormat = format;
await collectionImportStore.pickLocalImportPath();
return;
}

await collectionImportStore.setPendingLocalImportFromDrop(format, droppedPaths);
}

function isContextMenuOpen(): boolean {
Expand Down Expand Up @@ -745,7 +739,6 @@
modalStack.destroyModal(deleteRequestModal.id);
modalStack.destroyModal(importCollectionModal.id);
modalStack.destroyModal(collectionVariablesModal.id);
modalStack.destroyModal(soloCollectionOverwriteModal.id);
modalStack.destroyModal(exportCollectionModal.id);
collectionTreeUI.closeCollectionContextMenu();
collectionTreeUI.closeRequestContextMenu();
Expand All @@ -768,6 +761,11 @@
? collectionImportStoreState.pendingLocalImport
: null
);
let localActionDisabled = $derived(
!activePendingLocalImport ||
activePendingLocalImport.paths.length === 0 ||
collectionImportStoreState.localImportLoading
);
let collectionVariablesTarget = $derived(
collectionVariablesTargetName
? collections.find(
Expand Down Expand Up @@ -1278,7 +1276,7 @@
onClose={closeImportModal}
showCurlSection
localActionLabel={collectionImportStoreState.localImportLoading ? "Importing..." : "Import"}
localActionDisabled={!activePendingLocalImport || collectionImportStoreState.localImportLoading}
{localActionDisabled}
onLocalAction={runPendingLocalImport}
curlActionLabel="Import Request"
curlActionDisabled={!curlInput.trim() || (!curlTargetCollection && !curlCreatingNew)}
Expand All @@ -1297,7 +1295,7 @@
<LocalImportPane
formats={COLLECTION_LOCAL_IMPORT_FORMATS}
bind:selectedFormat={collectionImportStoreState.selectedLocalFormat}
onImport={collectionImportStore.setPendingLocalImportFromDrop}
onImport={handleLocalImportSelection}
/>
{/if}
{/snippet}
Expand Down Expand Up @@ -1386,29 +1384,6 @@
</Modal>
{/if}

{#if soloCollectionOverwriteModal.open}
<Modal
title="Overwrite collection?"
bind:open={soloCollectionOverwriteModal.open}
onclose={() => {
collectionImportStore.cancelSoloCollectionOverwrite();
soloCollectionOverwriteModal.open = false;
}}
size="xl"
>
{#if $topModalId === soloCollectionOverwriteModal.id}
<ToastContainer />
{/if}
<p>Collection "{collectionImportStoreState.soloCollectionOverwriteName}" already exists.</p>
<p class="text-sm text-neutral-600 dark:text-neutral-400">Do you want to overwrite it?</p>
{#snippet footer()}
<div class="flex w-full justify-end gap-2">
<Button color="red" onclick={confirmSoloCollectionOverwrite}>Overwrite</Button>
</div>
{/snippet}
</Modal>
{/if}

<ExportCollectionModal
bind:open={exportCollectionModal.open}
onExport={(format) => void handleExportCollection(format)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@
</script>

{#snippet importDropdown(triggeredBy: string, isOpen: boolean | undefined, onClose: () => void)}
<ContextMenu {triggeredBy} {isOpen} menuClass="z-50 w-50" {onClose}>
<ContextMenu {triggeredBy} {isOpen} menuClass="z-50 w-56" {onClose}>
<ContextMenuItem
onclick={() => {
openImportModal();
Expand Down Expand Up @@ -616,7 +616,7 @@
<LocalImportPane
formats={ENVIRONMENT_LOCAL_IMPORT_FORMATS}
bind:selectedFormat={localImportFormat}
onImport={handleLocalEnvironmentImport}
onImport={(format, paths) => handleLocalEnvironmentImport(format, paths?.[0])}
/>
{/snippet}

Expand Down
14 changes: 10 additions & 4 deletions frontend/src/lib/components/MainLayout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import CogSolid from "flowbite-svelte-icons/CogSolid.svelte";
import GlobeSolid from "flowbite-svelte-icons/GlobeSolid.svelte";
import Button from "flowbite-svelte/Button.svelte";
import CloseButton from "flowbite-svelte/CloseButton.svelte";
import Modal from "flowbite-svelte/Modal.svelte";
import { onDestroy } from "svelte";

Expand Down Expand Up @@ -81,10 +82,15 @@
{#if environmentManagerModal.open}
<Modal
bind:open={environmentManagerModal.open}
classes={{ body: "h-full overflow-y-auto" }}
fullscreen
size="none"
dismissable={false}
size="xl"
classes={{ body: "h-[min(80dvh,44rem)] overflow-hidden p-0" }}
>
{#snippet header()}
<div class="flex w-full justify-end">
<CloseButton onclick={() => (environmentManagerModal.open = false)} />
</div>
{/snippet}
{#if $topModalId === environmentManagerModal.id}
<ToastContainer />
{/if}
Expand All @@ -97,7 +103,7 @@
title="Settings"
bind:open={settingsModal.open}
size="xl"
classes={{ body: "h-[600px] overflow-hidden p-4" }}
classes={{ body: "h-[min(80dvh,44rem)] overflow-hidden p-4" }}
>
{#if $topModalId === settingsModal.id}
<ToastContainer />
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/lib/components/imports/ImportModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@
<Modal
bind:open
{title}
classes={{ body: "h-[450px] overflow-hidden p-4" }}
class="max-h-[calc(100vh-2rem)]"
classes={{ body: "h-[min(450px,calc(100vh-12rem))] overflow-hidden p-4" }}
placement="center"
size="xl"
onclose={(event) => {
selectedSection = "local";
Expand Down
Loading