133 lines
4.9 KiB
TypeScript
133 lines
4.9 KiB
TypeScript
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';
|
|
import { useI18n } from '@/lib/i18n';
|
|
|
|
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 { t } = useI18n();
|
|
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">
|
|
{t('adx.introPart1')}<code className="bg-muted px-1 rounded font-mono">APP_*</code>{t('adx.introPart2')}<strong>{t('adx.fullMode')}</strong>{t('adx.introPart3')}
|
|
</p>
|
|
|
|
{/* Add a field */}
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 max-w-xs">
|
|
<Combobox
|
|
value=""
|
|
options={addable}
|
|
placeholder={t('adx.addFieldPh')}
|
|
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)} />
|
|
{t('adx.showDeprecated')}
|
|
</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">
|
|
{t('adx.noExtra')}
|
|
</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 ? ' · ' + t('adx.deprecated') : ''}{def.intl ? ' · ' + t('adx.intl') : ''}
|
|
{!def && ''}
|
|
</span>
|
|
)}
|
|
{!def && <span className="block text-[10px] text-amber-600 leading-tight">{t('adx.nonStandard')}</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={t('adx.removeField')}
|
|
>
|
|
<X className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|