feat: Winkeyer
This commit is contained in:
@@ -4,13 +4,15 @@ import {
|
||||
type ColDef, type ColumnState, type GridReadyEvent, type RowDoubleClickedEvent,
|
||||
} from 'ag-grid-community';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { Columns3 } from 'lucide-react';
|
||||
import { Columns3, FilterX } from 'lucide-react';
|
||||
import type { QSOForm } from '@/types';
|
||||
import { QSOContextMenu, type QSOMenuState } from './QSOContextMenu';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs';
|
||||
|
||||
// Register every Community feature once. v32+ requires explicit registration;
|
||||
// AllCommunityModule keeps it simple and pulls in sort/filter/resize/reorder/
|
||||
@@ -45,6 +47,8 @@ type Props = {
|
||||
total: number;
|
||||
onRowDoubleClicked?: (q: QSOForm) => void;
|
||||
onRowSelected?: (id: number | null) => void;
|
||||
onUpdateFromCty?: (ids: number[]) => void;
|
||||
onUpdateFromQRZ?: (ids: number[]) => void;
|
||||
};
|
||||
|
||||
const COL_STATE_KEY = 'hamlog.qsoColState.v2';
|
||||
@@ -74,9 +78,11 @@ function fmtDateOnly(s: any): string {
|
||||
// Full catalog of selectable columns, grouped for the picker. `defaultVisible`
|
||||
// = shown out of the box; anything else stays hidden until the user toggles
|
||||
// it in the Columns dialog.
|
||||
type ColEntry = ColDef<QSOForm> & { group: string; label: string; defaultVisible?: boolean };
|
||||
export type ColEntry = ColDef<QSOForm> & { group: string; label: string; defaultVisible?: boolean };
|
||||
|
||||
const COL_CATALOG: ColEntry[] = [
|
||||
// Shared so the Worked-before grid (which now also shows full QSO records)
|
||||
// can offer the exact same column choices without duplicating the catalog.
|
||||
export const COL_CATALOG: ColEntry[] = [
|
||||
// ── QSO basics ──
|
||||
{ group: 'QSO', label: 'Date UTC', colId: 'qso_date', headerName: 'Date UTC', field: 'qso_date' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value), sort: 'desc', defaultVisible: true },
|
||||
{ group: 'QSO', label: 'Date Off', colId: 'qso_date_off', headerName: 'Date off', field: 'qso_date_off' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value) },
|
||||
@@ -190,14 +196,33 @@ const COL_CATALOG: ColEntry[] = [
|
||||
{ group: 'Misc', label: 'Updated', colId: 'updated_at', headerName: 'Updated at', field: 'updated_at' as any, width: 150, valueFormatter: (p) => fmtDateUTC(p.value) },
|
||||
];
|
||||
|
||||
const GROUP_ORDER = [
|
||||
export const GROUP_ORDER = [
|
||||
'QSO', 'Contacted', 'QSL', 'LoTW', 'eQSL', 'Uploads',
|
||||
'Contest', 'Propagation', 'My station', 'Misc',
|
||||
];
|
||||
|
||||
export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected }: Props) {
|
||||
export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ }: Props) {
|
||||
const gridRef = useRef<any>(null);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [menu, setMenu] = useState<QSOMenuState>(null);
|
||||
|
||||
// Right-click: if the clicked row isn't already part of the selection,
|
||||
// select just it; then open the bulk-action menu on the whole selection.
|
||||
function onCellContextMenu(e: any) {
|
||||
const ev = e.event as MouseEvent | undefined;
|
||||
ev?.preventDefault();
|
||||
const api = gridRef.current?.api;
|
||||
if (!api) return;
|
||||
if (e.node && !e.node.isSelected()) {
|
||||
api.deselectAll();
|
||||
e.node.setSelected(true);
|
||||
}
|
||||
const ids = (api.getSelectedRows() as QSOForm[])
|
||||
.map((r) => r.id as number)
|
||||
.filter((n) => !!n);
|
||||
if (ids.length === 0) return;
|
||||
setMenu({ x: ev?.clientX ?? 0, y: ev?.clientY ?? 0, ids });
|
||||
}
|
||||
|
||||
// Compute initial column defs: all columns defined, but those not marked
|
||||
// defaultVisible start hidden. The user's saved state (loaded onGridReady)
|
||||
@@ -215,21 +240,20 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected }: Prop
|
||||
}), []);
|
||||
|
||||
function onGridReady(e: GridReadyEvent) {
|
||||
try {
|
||||
const raw = localStorage.getItem(COL_STATE_KEY);
|
||||
if (raw) {
|
||||
const state = JSON.parse(raw) as ColumnState[];
|
||||
if (Array.isArray(state)) {
|
||||
e.api.applyColumnState({ state, applyOrder: true });
|
||||
}
|
||||
const local = loadLocal(COL_STATE_KEY);
|
||||
if (local) e.api.applyColumnState({ state: local as ColumnState[], applyOrder: true });
|
||||
// Fall back to the portable DB copy when the local cache is empty
|
||||
// (fresh machine / after a reinstall), then re-seed the cache.
|
||||
loadRemote(COL_STATE_KEY).then((remote) => {
|
||||
if (remote && !local) {
|
||||
e.api.applyColumnState({ state: remote as ColumnState[], applyOrder: true });
|
||||
seedLocal(COL_STATE_KEY, remote);
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
const saveColumnState = useCallback(() => {
|
||||
try {
|
||||
const state = gridRef.current?.api?.getColumnState();
|
||||
if (state) localStorage.setItem(COL_STATE_KEY, JSON.stringify(state));
|
||||
} catch {}
|
||||
const state = gridRef.current?.api?.getColumnState();
|
||||
if (state) saveState(COL_STATE_KEY, state);
|
||||
}, []);
|
||||
|
||||
function handleRowDoubleClicked(e: RowDoubleClickedEvent<QSOForm>) {
|
||||
@@ -281,6 +305,10 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected }: Prop
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end gap-2 px-2.5 py-1 border-b border-border/60 bg-muted/20">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-[11px]" onClick={() => gridRef.current?.api?.setFilterModel(null)}
|
||||
title="Clear all column filters">
|
||||
<FilterX className="size-3.5" /> Clear filters
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-[11px]" onClick={() => setPickerOpen(true)}>
|
||||
<Columns3 className="size-3.5" /> Columns
|
||||
</Button>
|
||||
@@ -293,7 +321,7 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected }: Prop
|
||||
rowData={rows}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
rowSelection={{ mode: 'singleRow', checkboxes: false, enableClickSelection: true }}
|
||||
rowSelection={{ mode: 'multiRow', checkboxes: false, headerCheckbox: false, enableClickSelection: true }}
|
||||
onGridReady={onGridReady}
|
||||
onColumnResized={saveColumnState}
|
||||
onColumnMoved={saveColumnState}
|
||||
@@ -302,6 +330,8 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected }: Prop
|
||||
onSortChanged={saveColumnState}
|
||||
onRowDoubleClicked={handleRowDoubleClicked}
|
||||
onSelectionChanged={onSelectionChanged}
|
||||
onCellContextMenu={onCellContextMenu}
|
||||
preventDefaultOnContextMenu
|
||||
animateRows={false}
|
||||
suppressCellFocus
|
||||
getRowId={(p) => String((p.data as any).id)}
|
||||
@@ -309,6 +339,13 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected }: Prop
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<QSOContextMenu
|
||||
menu={menu}
|
||||
onClose={() => setMenu(null)}
|
||||
onUpdateFromCty={(ids) => onUpdateFromCty?.(ids)}
|
||||
onUpdateFromQRZ={(ids) => onUpdateFromQRZ?.(ids)}
|
||||
/>
|
||||
|
||||
<Dialog open={pickerOpen} onOpenChange={setPickerOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
@@ -318,7 +355,7 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected }: Prop
|
||||
Your selection is saved.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-3 gap-4 max-h-[60vh] overflow-y-auto py-2">
|
||||
<div className="grid grid-cols-3 gap-4 max-h-[60vh] overflow-y-auto px-5 py-3">
|
||||
{GROUP_ORDER.map((group) => {
|
||||
const cols = COL_CATALOG.filter((c) => c.group === group);
|
||||
if (cols.length === 0) return null;
|
||||
|
||||
Reference in New Issue
Block a user