Compare commits
8 Commits
v1.1.0
..
2d9fb083dc
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d9fb083dc | |||
| 4ab3c3b00a | |||
| 3e07b51134 | |||
| a811e323e6 | |||
| 0843c51c7d | |||
| 2a0802b22f | |||
| 0f1cdbef3c | |||
| 165c65be5f |
@@ -1620,14 +1620,14 @@ class MainWindow(QMainWindow):
|
||||
self._refresh_tables(select_index=min(idx, len(self.servers) - 1))
|
||||
self._mark_dirty()
|
||||
|
||||
def paste_json(self):
|
||||
dlg = PasteDialog(self)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted or not dlg.result_servers:
|
||||
return
|
||||
self._push_undo()
|
||||
added, replaced = 0, 0
|
||||
def _import_server(self, name: str, data: dict) -> tuple[bool, bool]:
|
||||
"""
|
||||
Add a pasted/dropped `name`/`data` server to self.servers, resolving
|
||||
a name collision with an explicit prompt (never a silent overwrite).
|
||||
|
||||
Returns (added, replaced).
|
||||
"""
|
||||
existing = {s.name: i for i, s in enumerate(self.servers)}
|
||||
for name, data in dlg.result_servers.items():
|
||||
if name in existing:
|
||||
ans = QMessageBox.question(
|
||||
self,
|
||||
@@ -1637,17 +1637,21 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
if ans == QMessageBox.StandardButton.Yes:
|
||||
self.servers[existing[name]].data = data
|
||||
replaced += 1
|
||||
continue
|
||||
new = f"{name}-2"
|
||||
k = 3
|
||||
names = {s.name for s in self.servers}
|
||||
while new in names:
|
||||
new = f"{name}-{k}"
|
||||
k += 1
|
||||
name = new
|
||||
return False, True
|
||||
name = core.resolve_name_collision(name, {s.name for s in self.servers})
|
||||
self.servers.append(core.ServerEntry(name, data, True))
|
||||
added += 1
|
||||
return True, False
|
||||
|
||||
def paste_json(self):
|
||||
dlg = PasteDialog(self)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted or not dlg.result_servers:
|
||||
return
|
||||
self._push_undo()
|
||||
added, replaced = 0, 0
|
||||
for name, data in dlg.result_servers.items():
|
||||
a, r = self._import_server(name, data)
|
||||
added += int(a)
|
||||
replaced += int(r)
|
||||
self._refresh_tables(select_index=len(self.servers) - 1)
|
||||
self._mark_dirty()
|
||||
self.status.setText(f"Imported {added} added, {replaced} replaced. Review and Save.")
|
||||
@@ -1835,15 +1839,16 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.warning(self, "Couldn't import", f"{Path(path).name}:\n{ex}")
|
||||
continue
|
||||
self._push_undo()
|
||||
added, replaced = 0, 0
|
||||
for name, data in servers.items():
|
||||
names = {s.name for s in self.servers}
|
||||
if name in names:
|
||||
name = f"{name}-imported"
|
||||
self.servers.append(core.ServerEntry(name, data, True))
|
||||
a, r = self._import_server(name, data)
|
||||
added += int(a)
|
||||
replaced += int(r)
|
||||
self._refresh_tables(select_index=len(self.servers) - 1)
|
||||
self._mark_dirty()
|
||||
self.status.setText(
|
||||
f"Imported {len(servers)} server(s) from {Path(path).name}. Review and Save."
|
||||
f"Imported {added} added, {replaced} replaced from {Path(path).name}. "
|
||||
"Review and Save."
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
+19
@@ -189,6 +189,25 @@ def extract_servers(cfg: dict) -> list[ServerEntry]:
|
||||
return out
|
||||
|
||||
|
||||
def resolve_name_collision(name: str, existing: set[str]) -> str:
|
||||
"""
|
||||
Return a name guaranteed not to collide with `existing`.
|
||||
|
||||
If `name` isn't already taken it's returned unchanged. Otherwise a
|
||||
`-2`, `-3`, ... suffix is appended until the result is unique — this is
|
||||
the "keep both (renamed)" branch used by paste/import when the user
|
||||
doesn't want to overwrite an existing server of the same name.
|
||||
"""
|
||||
if name not in existing:
|
||||
return name
|
||||
n = 2
|
||||
candidate = f"{name}-{n}"
|
||||
while candidate in existing:
|
||||
n += 1
|
||||
candidate = f"{name}-{n}"
|
||||
return candidate
|
||||
|
||||
|
||||
def apply_servers(cfg: dict, servers: list[ServerEntry]) -> dict:
|
||||
"""
|
||||
Write the server list back into `cfg` in place, preserving every other key
|
||||
|
||||
@@ -638,3 +638,36 @@ def test_args_secret_warning_env_not_triggered():
|
||||
def test_args_secret_warning_empty():
|
||||
assert c.args_secret_warning({}) is None
|
||||
assert c.args_secret_warning({"args": []}) is None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# resolve_name_collision (paste/import duplicate-name handling — issue #8)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_resolve_name_collision_no_conflict_returns_unchanged():
|
||||
assert c.resolve_name_collision("brave-search", {"other"}) == "brave-search"
|
||||
|
||||
|
||||
def test_resolve_name_collision_single_conflict_appends_dash_two():
|
||||
assert c.resolve_name_collision("brave-search", {"brave-search"}) == "brave-search-2"
|
||||
|
||||
|
||||
def test_resolve_name_collision_skips_taken_suffixes():
|
||||
existing = {"brave-search", "brave-search-2", "brave-search-3"}
|
||||
assert c.resolve_name_collision("brave-search", existing) == "brave-search-4"
|
||||
|
||||
|
||||
def test_resolve_name_collision_empty_existing_set():
|
||||
assert c.resolve_name_collision("brave-search", set()) == "brave-search"
|
||||
|
||||
|
||||
def test_resolve_name_collision_repeated_calls_stay_unique():
|
||||
"""Simulates dropping the same file twice in a row: each resolved name
|
||||
must be fed back into `existing` before resolving the next one, or two
|
||||
servers could end up sharing a name (and silently collapse into one
|
||||
when apply_servers() rebuilds its dict at save time)."""
|
||||
existing = {"brave-search"}
|
||||
first = c.resolve_name_collision("brave-search", existing)
|
||||
existing.add(first)
|
||||
second = c.resolve_name_collision("brave-search", existing)
|
||||
assert first != second
|
||||
assert {first, second} == {"brave-search-2", "brave-search-3"}
|
||||
|
||||
Reference in New Issue
Block a user