up
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { X, Plus } from 'lucide-react';
|
||||
import { ADIFFields } from '../../wailsjs/go/main/App';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
type FieldDef = {
|
||||
name: string; kind: string; category: string;
|
||||
promoted: boolean; deprecated: boolean; intl: boolean;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
// The QSO's extras map (uppercase ADIF tag → raw value).
|
||||
value: Record<string, string> | undefined;
|
||||
onChange: (next: Record<string, string> | undefined) => void;
|
||||
}
|
||||
|
||||
// AdifExtrasEditor — a dictionary-driven editor for every ADIF field that is
|
||||
// NOT promoted to a first-class column. Backed by the QSO's extras map, so any
|
||||
// ADIF 3.1.7 field (plus custom/vendor tags) can be viewed and edited. This is
|
||||
// what makes "100% of ADIF fields available" true without 160 DB columns.
|
||||
export function AdifExtrasEditor({ value, onChange }: Props) {
|
||||
const [dict, setDict] = useState<FieldDef[]>([]);
|
||||
const [showDeprecated, setShowDeprecated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
ADIFFields().then((f) => setDict((f ?? []) as any)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Entries currently set, sorted for stable display.
|
||||
const entries = useMemo(
|
||||
() => Object.entries(value ?? {}).sort((a, b) => a[0].localeCompare(b[0])),
|
||||
[value],
|
||||
);
|
||||
|
||||
// Addable fields: standard non-promoted ADIF fields not already present.
|
||||
// Deprecated (import-only) fields are hidden unless the toggle is on.
|
||||
const addable = useMemo(() => {
|
||||
const have = new Set(Object.keys(value ?? {}));
|
||||
return dict
|
||||
.filter((f) => !f.promoted && !have.has(f.name) && (showDeprecated || !f.deprecated))
|
||||
.map((f) => f.name);
|
||||
}, [dict, value, showDeprecated]);
|
||||
|
||||
const meta = useMemo(() => {
|
||||
const m: Record<string, FieldDef> = {};
|
||||
for (const f of dict) m[f.name] = f;
|
||||
return m;
|
||||
}, [dict]);
|
||||
|
||||
function setKV(key: string, val: string) {
|
||||
const next = { ...(value ?? {}) };
|
||||
next[key] = val;
|
||||
onChange(next);
|
||||
}
|
||||
function remove(key: string) {
|
||||
const next = { ...(value ?? {}) };
|
||||
delete next[key];
|
||||
onChange(Object.keys(next).length ? next : undefined);
|
||||
}
|
||||
function addField(name: string) {
|
||||
const key = name.trim().toUpperCase();
|
||||
if (!key || (value && key in value)) return;
|
||||
setKV(key, '');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Every ADIF 3.1.7 field not shown in the other tabs. Pick a field to add it,
|
||||
or type a custom/vendor tag (e.g. <code className="bg-muted px-1 rounded font-mono">APP_*</code>).
|
||||
Stored losslessly and exported in the <strong>full</strong> ADIF mode.
|
||||
</p>
|
||||
|
||||
{/* Add a field */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 max-w-xs">
|
||||
<Combobox
|
||||
value=""
|
||||
options={addable}
|
||||
placeholder="Add ADIF field…"
|
||||
allowFreeText
|
||||
onChange={addField}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-1.5 text-[11px] text-muted-foreground cursor-pointer">
|
||||
<input type="checkbox" checked={showDeprecated} onChange={(e) => setShowDeprecated(e.target.checked)} />
|
||||
Show deprecated
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Current entries */}
|
||||
{entries.length === 0 ? (
|
||||
<div className="text-[11px] text-muted-foreground italic border border-dashed border-border rounded-md px-3 py-4 text-center">
|
||||
No extra ADIF fields. Use the picker above to add one.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{entries.map(([k, v]) => {
|
||||
const def = meta[k];
|
||||
return (
|
||||
<div key={k} className="flex items-center gap-2">
|
||||
<div className="w-48 shrink-0">
|
||||
<span className="font-mono text-xs font-semibold">{k}</span>
|
||||
{def && (
|
||||
<span className="block text-[10px] text-muted-foreground leading-tight">
|
||||
{def.category}{def.deprecated ? ' · deprecated' : ''}{def.intl ? ' · intl' : ''}
|
||||
{!def && ''}
|
||||
</span>
|
||||
)}
|
||||
{!def && <span className="block text-[10px] text-amber-600 leading-tight">non-standard</span>}
|
||||
</div>
|
||||
<Input
|
||||
className="flex-1 h-8 text-xs font-mono"
|
||||
value={v}
|
||||
onChange={(e) => setKV(k, e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => remove(k)} title="Remove field"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user