fix(import): apply layouts when connections already exist
This commit is contained in:
1 parent
71136377e5
commit
2f3a32955c
4 files changed
+264
-3
No files matched your search
@@ -2,6 +2,7 @@ import { ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { hasSidebarLayoutEntries } from "@/lib/sidebar/sidebarLayout";
|
||||
import type { SidebarLayout } from "@/types/database";
|
||||
|
||||
const showTransferDialog = ref(false);
|
||||
@@ -310,7 +311,7 @@ export function useDialogSources() {
|
||||
: t("configExport.importNone"),
|
||||
4000,
|
||||
);
|
||||
if (layout && count > 0) {
|
||||
if (hasSidebarLayoutEntries(layout)) {
|
||||
pendingImportLayout.value = layout;
|
||||
showImportLayoutConfirm.value = true;
|
||||
}
|
||||
@@ -325,7 +326,7 @@ export function useDialogSources() {
|
||||
const { count, layout } = await connectionStore.importConnectionsFromFile(pendingImportContent.value, passphrase);
|
||||
showConfigPassphraseDialog.value = false;
|
||||
toast(count > 0 ? t("configExport.importSuccess", { count }) : t("configExport.importNone"), 2000);
|
||||
if (layout && count > 0) {
|
||||
if (hasSidebarLayoutEntries(layout)) {
|
||||
pendingImportLayout.value = layout;
|
||||
showImportLayoutConfirm.value = true;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ export function emptyLayout(): SidebarLayout {
|
||||
return { groups: [], order: [] };
|
||||
}
|
||||
|
||||
export function hasSidebarLayoutEntries(layout: SidebarLayout | null | undefined): layout is SidebarLayout {
|
||||
return !!layout && (layout.groups.length > 0 || layout.order.length > 0);
|
||||
}
|
||||
|
||||
function folderPathSegments(path: string | undefined): string[] {
|
||||
return (path ?? "").split("/").filter((segment) => segment.length > 0);
|
||||
}
|
||||
@@ -121,6 +125,58 @@ export function remapSidebarLayoutConnectionIds(layout: SidebarLayout, connectio
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge an imported layout into the current one instead of replacing it.
|
||||
* Folders are matched by name within the same level so repeated imports reuse
|
||||
* them, and connections outside the imported set keep their current placement.
|
||||
*/
|
||||
export function mergeSidebarLayout(current: SidebarLayout, imported: SidebarLayout): SidebarLayout {
|
||||
const importedGroups = new Map(imported.groups.map((group) => [group.id, group]));
|
||||
const groups = current.groups.map((group) => ({ ...group }));
|
||||
const groupNameById = new Map(groups.map((group) => [group.id, group.name]));
|
||||
const order = cloneEntries(current.order);
|
||||
|
||||
// The imported layout owns the placement of the connections it lists, so drop
|
||||
// their current entries first — otherwise they would end up listed twice.
|
||||
const importedConnectionIds: string[] = [];
|
||||
const collectConnections = (entries: SidebarOrderEntry[]) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.type === "connection") importedConnectionIds.push(entry.id);
|
||||
else collectConnections(entryChildren(entry));
|
||||
}
|
||||
};
|
||||
collectConnections(imported.order);
|
||||
for (const id of importedConnectionIds) removeEntry(order, id);
|
||||
|
||||
const merge = (target: SidebarOrderEntry[], source: SidebarOrderEntry[]) => {
|
||||
for (const entry of source) {
|
||||
if (entry.type === "connection") {
|
||||
target.push({ type: "connection", id: entry.id });
|
||||
continue;
|
||||
}
|
||||
|
||||
const importedGroup = importedGroups.get(entry.id);
|
||||
if (!importedGroup) continue;
|
||||
|
||||
let destination = target.find((candidate): candidate is Extract<SidebarOrderEntry, { type: "group" }> => candidate.type === "group" && groupNameById.get(candidate.id) === importedGroup.name);
|
||||
if (!destination) {
|
||||
// A fresh id keeps the merged tree free of collisions with the group ids
|
||||
// the current layout already uses.
|
||||
const id = uuid();
|
||||
destination = { type: "group", id, children: [] };
|
||||
groups.push({ ...importedGroup, id });
|
||||
groupNameById.set(id, importedGroup.name);
|
||||
target.push(destination);
|
||||
}
|
||||
destination.children ??= [];
|
||||
merge(destination.children, entryChildren(entry));
|
||||
}
|
||||
};
|
||||
merge(order, imported.order);
|
||||
|
||||
return { groups, order };
|
||||
}
|
||||
|
||||
export function connectionSidebarSearchAliases(config: Pick<ConnectionConfig, "host" | "username">): string[] {
|
||||
return [config.host, config.username].filter((value) => value.trim().length > 0);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
collapseAllGroups as collapseAllGroupsOp,
|
||||
moveConnectionToGroup as moveConnectionToGroupOp,
|
||||
remapSidebarLayoutConnectionIds,
|
||||
mergeSidebarLayout,
|
||||
reorderEntry as reorderEntryOp,
|
||||
buildConnectionGroupPathMap,
|
||||
connectionSidebarSearchAliases,
|
||||
@@ -7857,7 +7858,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
function applySidebarLayout(layout: SidebarLayout) {
|
||||
const reconciledLayout = reconcileLayout(
|
||||
connections.value.map((c) => c.id),
|
||||
layout,
|
||||
mergeSidebarLayout(sidebarLayout.value, layout),
|
||||
);
|
||||
updateLayoutAndRebuild(reconciledLayout);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user