fix: Bug where renaming the main folder did not update db path
settings where the ones of the previous folder.
This commit is contained in:
@@ -523,9 +523,23 @@ func (a *App) startup(ctx context.Context) {
|
||||
// Windows reinstall. It lives OUTSIDE the DB since we must know the path
|
||||
// before opening it.
|
||||
if custom := readDBPointer(dataDir); custom != "" {
|
||||
// Portability guard: a pointer that is merely ANOTHER folder's default DB
|
||||
// location ("…/<other>/data/opslog.db") means the portable folder was
|
||||
// renamed or copied — its config.json still points at the original. Ignore
|
||||
// it and use THIS folder's own data (and clear the stale pointer so it
|
||||
// stops happening). A genuine custom location — another drive, a different
|
||||
// filename — is NOT default-style, so it's still honoured.
|
||||
stale := strings.EqualFold(filepath.Base(custom), "opslog.db") &&
|
||||
strings.EqualFold(filepath.Base(filepath.Dir(custom)), "data") &&
|
||||
!strings.EqualFold(filepath.Clean(filepath.Dir(custom)), filepath.Clean(dataDir))
|
||||
if stale {
|
||||
fmt.Printf("OpsLog: ignoring stale DB pointer %q (folder moved) — using %s\n", custom, a.dbPath)
|
||||
_ = writeDBPointer(dataDir, "")
|
||||
} else {
|
||||
a.dbPath = custom
|
||||
usingDefault = false
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(a.dbPath), 0o755); err != nil {
|
||||
a.startupErr = "cannot create db folder: " + err.Error()
|
||||
fmt.Println("OpsLog:", a.startupErr)
|
||||
@@ -3174,6 +3188,15 @@ func (a *App) DeleteQSO(id int64) error {
|
||||
return a.qso.Delete(a.ctx, id)
|
||||
}
|
||||
|
||||
// DeleteQSOs removes several QSOs at once (multi-row selection). Returns the
|
||||
// number actually deleted.
|
||||
func (a *App) DeleteQSOs(ids []int64) (int64, error) {
|
||||
if a.qso == nil {
|
||||
return 0, fmt.Errorf("db not initialized")
|
||||
}
|
||||
return a.qso.DeleteMany(a.ctx, ids)
|
||||
}
|
||||
|
||||
// QSLBulkUpdate carries the paper-QSL fields to apply to a selection. An empty
|
||||
// string leaves that field unchanged (so you can set only "received = Y + date"
|
||||
// without touching the sent side).
|
||||
|
||||
+35
-23
@@ -7,7 +7,7 @@ import {
|
||||
import {
|
||||
AddQSO, ListQSO, CountQSO, ListQSOFiltered, CountQSOFiltered,
|
||||
OpenADIFFile, ImportADIF, SaveADIFFile, ExportADIF, ExportADIFFiltered, ExportADIFSelected,
|
||||
GetQSO, UpdateQSO, DeleteQSO, DeleteAllQSO,
|
||||
GetQSO, UpdateQSO, DeleteQSO, DeleteQSOs, DeleteAllQSO,
|
||||
UpdateQSOsFromCty, UpdateQSOsFromQRZ, UpdateQSOsFromClublog, UploadQSOsManual, SendQSORecordingEmail,
|
||||
LookupCallsign, GetStationSettings, GetListsSettings,
|
||||
GetStartupStatus, CheckForUpdate,
|
||||
@@ -701,8 +701,10 @@ export default function App() {
|
||||
|
||||
// === Modals ===
|
||||
const [editingQSO, setEditingQSO] = useState<QSO | null>(null);
|
||||
const [deletingQSO, setDeletingQSO] = useState<QSO | null>(null);
|
||||
// QSOs queued for the delete confirm (1 or many — multi-row selection).
|
||||
const [deletingIds, setDeletingIds] = useState<number[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
// Re-read the "beam on map" toggle when Preferences closes (it's edited there).
|
||||
useEffect(() => { if (!showSettings) setShowBeamOnMap(localStorage.getItem('opslog.showBeamOnMap') !== '0'); }, [showSettings]);
|
||||
@@ -1584,8 +1586,8 @@ export default function App() {
|
||||
} catch (err: any) { setError(String(err?.message ?? err)); }
|
||||
}
|
||||
function onModalDelete(id: number) {
|
||||
const q = editingQSO; setEditingQSO(null);
|
||||
if (q) setDeletingQSO(q); else askDelete(id);
|
||||
setEditingQSO(null);
|
||||
setDeletingIds([id]);
|
||||
}
|
||||
|
||||
// Bulk grid actions (right-click menu). Recompute country/zones from
|
||||
@@ -1654,20 +1656,25 @@ export default function App() {
|
||||
showToast(`Exported ${r.count} selected QSO${r.count > 1 ? 's' : ''} → ${r.path}`);
|
||||
} catch (e: any) { setError(String(e?.message ?? e)); }
|
||||
}
|
||||
function askDelete(id: number) {
|
||||
const q = qsos.find((x) => x.id === id);
|
||||
if (q) setDeletingQSO(q);
|
||||
function askDelete(id: number) { setDeletingIds([id]); }
|
||||
// Delete the whole multi-row selection (Edit menu / Delete key).
|
||||
function askDeleteSelected() {
|
||||
if (selectedIds.length > 0) setDeletingIds(selectedIds);
|
||||
else if (selectedId != null) setDeletingIds([selectedId]);
|
||||
}
|
||||
async function confirmDelete() {
|
||||
if (!deletingQSO) return;
|
||||
if (deletingIds.length === 0) return;
|
||||
const ids = deletingIds;
|
||||
try {
|
||||
await DeleteQSO(deletingQSO.id);
|
||||
if (selectedId === deletingQSO.id) setSelectedId(null);
|
||||
setDeletingQSO(null);
|
||||
if (ids.length === 1) await DeleteQSO(ids[0]);
|
||||
else await DeleteQSOs(ids as any);
|
||||
setDeletingIds([]);
|
||||
setSelectedId(null);
|
||||
setSelectedIds([]);
|
||||
await refresh();
|
||||
} catch (err: any) {
|
||||
setError(String(err?.message ?? err));
|
||||
setDeletingQSO(null);
|
||||
setDeletingIds([]);
|
||||
}
|
||||
}
|
||||
async function confirmDeleteAll() {
|
||||
@@ -1878,7 +1885,7 @@ export default function App() {
|
||||
]},
|
||||
{ name: 'edit', label: 'Edit', items: [
|
||||
{ type: 'item', label: 'Edit selected QSO…', action: 'edit.edit', shortcut: 'Enter', disabled: selectedId === null },
|
||||
{ type: 'item', label: 'Delete selected QSO', action: 'edit.delete', shortcut: 'Del', disabled: selectedId === null },
|
||||
{ type: 'item', label: selectedIds.length > 1 ? `Delete ${selectedIds.length} selected QSOs` : 'Delete selected QSO', action: 'edit.delete', shortcut: 'Del', disabled: selectedId === null },
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: 'Preferences…', action: 'edit.prefs' },
|
||||
]},
|
||||
@@ -1901,7 +1908,7 @@ export default function App() {
|
||||
{ name: 'help', label: 'Help', items: [
|
||||
{ type: 'item', label: 'About OpsLog', action: 'help.about' },
|
||||
]},
|
||||
], [total, selectedId, ctyRefreshing, refsDownloading, exporting, wkEnabled, dvkEnabled]);
|
||||
], [total, selectedId, selectedIds, ctyRefreshing, refsDownloading, exporting, wkEnabled, dvkEnabled]);
|
||||
|
||||
function handleMenu(action: string) {
|
||||
switch (action) {
|
||||
@@ -1911,7 +1918,7 @@ export default function App() {
|
||||
case 'view.refresh': refresh(); break;
|
||||
case 'view.clearfilters': setFilterCallsign(''); setActiveFilter({ conditions: [], match: 'AND' }); break;
|
||||
case 'edit.edit': if (selectedId !== null) openEdit(selectedId); break;
|
||||
case 'edit.delete': if (selectedId !== null) askDelete(selectedId); break;
|
||||
case 'edit.delete': askDeleteSelected(); break;
|
||||
case 'edit.prefs': setShowSettings(true); break;
|
||||
case 'tools.qslmanager': setQslTabOpen(true); setActiveTab('qsl'); break;
|
||||
case 'tools.qsldesigner': setQslDesignerOpen(true); break;
|
||||
@@ -2008,14 +2015,14 @@ export default function App() {
|
||||
}
|
||||
if (typing) return;
|
||||
if (selectedId !== null) {
|
||||
if (e.key === 'Delete') { e.preventDefault(); askDelete(selectedId); return; }
|
||||
if (e.key === 'Delete') { e.preventDefault(); askDeleteSelected(); return; }
|
||||
if (e.key === 'Enter') { e.preventDefault(); openEdit(selectedId); return; }
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedId, refresh]);
|
||||
}, [selectedId, selectedIds, refresh]);
|
||||
|
||||
// ── Entry-field blocks ─────────────────────────────────────────────────
|
||||
// Each field is defined once here, then composed into either the compact
|
||||
@@ -3213,7 +3220,7 @@ export default function App() {
|
||||
onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)}
|
||||
onExportSelected={exportSelectedADIF}
|
||||
onExportFiltered={exportFilteredADIF}
|
||||
onRowSelected={(id) => setSelectedId(id)}
|
||||
onRowSelected={(ids) => { setSelectedIds(ids); setSelectedId(ids[0] ?? null); }}
|
||||
/>
|
||||
<div className="px-3 py-1.5 border-t border-border/60 text-[11px] text-muted-foreground flex items-center justify-between gap-3 bg-muted/30">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -3592,16 +3599,21 @@ export default function App() {
|
||||
onOpenDesigner={() => setQslDesignerOpen(true)}
|
||||
/>
|
||||
|
||||
{deletingQSO && (
|
||||
{deletingIds.length > 0 && (() => {
|
||||
const single = deletingIds.length === 1 ? qsos.find((x) => x.id === deletingIds[0]) : null;
|
||||
return (
|
||||
<ConfirmDialog
|
||||
title="Delete QSO?"
|
||||
message={`This will permanently delete the QSO with ${deletingQSO.callsign} on ${fmtDateUTC(deletingQSO.qso_date)} (${deletingQSO.band} ${deletingQSO.mode}). This cannot be undone.`}
|
||||
title={deletingIds.length > 1 ? `Delete ${deletingIds.length} QSOs?` : 'Delete QSO?'}
|
||||
message={single
|
||||
? `This will permanently delete the QSO with ${single.callsign} on ${fmtDateUTC(single.qso_date)} (${single.band} ${single.mode}). This cannot be undone.`
|
||||
: `This will permanently delete ${deletingIds.length} selected QSO${deletingIds.length > 1 ? 's' : ''}. This cannot be undone.`}
|
||||
confirmLabel="Delete"
|
||||
danger
|
||||
onConfirm={confirmDelete}
|
||||
onCancel={() => setDeletingQSO(null)}
|
||||
onCancel={() => setDeletingIds([])}
|
||||
/>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
{showDeleteAll && (
|
||||
<ConfirmDialog
|
||||
title="Delete ALL QSOs?"
|
||||
|
||||
@@ -46,7 +46,7 @@ type Props = {
|
||||
rows: QSOForm[];
|
||||
total: number;
|
||||
onRowDoubleClicked?: (q: QSOForm) => void;
|
||||
onRowSelected?: (id: number | null) => void;
|
||||
onRowSelected?: (ids: number[]) => void;
|
||||
onUpdateFromCty?: (ids: number[]) => void;
|
||||
onUpdateFromQRZ?: (ids: number[]) => void;
|
||||
onUpdateFromClublog?: (ids: number[]) => void;
|
||||
@@ -277,8 +277,8 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda
|
||||
if (e.data && onRowDoubleClicked) onRowDoubleClicked(e.data);
|
||||
}
|
||||
function onSelectionChanged() {
|
||||
const sel = gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined;
|
||||
onRowSelected?.(sel && sel[0] ? (sel[0].id as number) : null);
|
||||
const sel = (gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined) ?? [];
|
||||
onRowSelected?.(sel.map((r) => r.id as number).filter((id) => id != null));
|
||||
}
|
||||
|
||||
// ── Column picker (visibility) ──
|
||||
|
||||
Vendored
+2
@@ -87,6 +87,8 @@ export function DeleteProfile(arg1:number):Promise<void>;
|
||||
|
||||
export function DeleteQSO(arg1:number):Promise<void>;
|
||||
|
||||
export function DeleteQSOs(arg1:Array<number>):Promise<number>;
|
||||
|
||||
export function DeleteUDPIntegration(arg1:number):Promise<void>;
|
||||
|
||||
export function DisconnectAllClusters():Promise<void>;
|
||||
|
||||
@@ -146,6 +146,10 @@ export function DeleteQSO(arg1) {
|
||||
return window['go']['main']['App']['DeleteQSO'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteQSOs(arg1) {
|
||||
return window['go']['main']['App']['DeleteQSOs'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteUDPIntegration(arg1) {
|
||||
return window['go']['main']['App']['DeleteUDPIntegration'](arg1);
|
||||
}
|
||||
|
||||
@@ -651,6 +651,25 @@ func (r *Repo) DeleteAll(ctx context.Context) (int64, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// DeleteMany removes several QSOs in one statement. Returns the number deleted.
|
||||
func (r *Repo) DeleteMany(ctx context.Context, ids []int64) (int64, error) {
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
ph := make([]string, len(ids))
|
||||
args := make([]any, len(ids))
|
||||
for i, id := range ids {
|
||||
ph[i] = "?"
|
||||
args[i] = id
|
||||
}
|
||||
res, err := r.db.ExecContext(ctx, `DELETE FROM qso WHERE id IN (`+strings.Join(ph, ",")+`)`, args...)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("delete qsos: %w", err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Delete removes a QSO by id.
|
||||
func (r *Repo) Delete(ctx context.Context, id int64) error {
|
||||
res, err := r.db.ExecContext(ctx, `DELETE FROM qso WHERE id = ?`, id)
|
||||
|
||||
Reference in New Issue
Block a user