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._refresh_tables(select_index=min(idx, len(self.servers) - 1))
|
||||||
self._mark_dirty()
|
self._mark_dirty()
|
||||||
|
|
||||||
def paste_json(self):
|
def _import_server(self, name: str, data: dict) -> tuple[bool, bool]:
|
||||||
dlg = PasteDialog(self)
|
"""
|
||||||
if dlg.exec() != QDialog.DialogCode.Accepted or not dlg.result_servers:
|
Add a pasted/dropped `name`/`data` server to self.servers, resolving
|
||||||
return
|
a name collision with an explicit prompt (never a silent overwrite).
|
||||||
self._push_undo()
|
|
||||||
added, replaced = 0, 0
|
Returns (added, replaced).
|
||||||
|
"""
|
||||||
existing = {s.name: i for i, s in enumerate(self.servers)}
|
existing = {s.name: i for i, s in enumerate(self.servers)}
|
||||||
for name, data in dlg.result_servers.items():
|
|
||||||
if name in existing:
|
if name in existing:
|
||||||
ans = QMessageBox.question(
|
ans = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
@@ -1637,17 +1637,21 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
if ans == QMessageBox.StandardButton.Yes:
|
if ans == QMessageBox.StandardButton.Yes:
|
||||||
self.servers[existing[name]].data = data
|
self.servers[existing[name]].data = data
|
||||||
replaced += 1
|
return False, True
|
||||||
continue
|
name = core.resolve_name_collision(name, {s.name for s in self.servers})
|
||||||
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
|
|
||||||
self.servers.append(core.ServerEntry(name, data, True))
|
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._refresh_tables(select_index=len(self.servers) - 1)
|
||||||
self._mark_dirty()
|
self._mark_dirty()
|
||||||
self.status.setText(f"Imported {added} added, {replaced} replaced. Review and Save.")
|
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}")
|
QMessageBox.warning(self, "Couldn't import", f"{Path(path).name}:\n{ex}")
|
||||||
continue
|
continue
|
||||||
self._push_undo()
|
self._push_undo()
|
||||||
|
added, replaced = 0, 0
|
||||||
for name, data in servers.items():
|
for name, data in servers.items():
|
||||||
names = {s.name for s in self.servers}
|
a, r = self._import_server(name, data)
|
||||||
if name in names:
|
added += int(a)
|
||||||
name = f"{name}-imported"
|
replaced += int(r)
|
||||||
self.servers.append(core.ServerEntry(name, data, True))
|
|
||||||
self._refresh_tables(select_index=len(self.servers) - 1)
|
self._refresh_tables(select_index=len(self.servers) - 1)
|
||||||
self._mark_dirty()
|
self._mark_dirty()
|
||||||
self.status.setText(
|
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
|
break
|
||||||
|
|
||||||
|
|||||||
+19
@@ -189,6 +189,25 @@ def extract_servers(cfg: dict) -> list[ServerEntry]:
|
|||||||
return out
|
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:
|
def apply_servers(cfg: dict, servers: list[ServerEntry]) -> dict:
|
||||||
"""
|
"""
|
||||||
Write the server list back into `cfg` in place, preserving every other key
|
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():
|
def test_args_secret_warning_empty():
|
||||||
assert c.args_secret_warning({}) is None
|
assert c.args_secret_warning({}) is None
|
||||||
assert c.args_secret_warning({"args": []}) 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