@ham2k/extension-sdk 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/activityExports.js +37 -6
- package/dist/exportSettings.js +133 -0
- package/dist/index.d.ts +64 -3
- package/dist/index.js +1 -0
- package/docs/forms.md +30 -2
- package/docs/hooks.md +74 -6
- package/docs/settings.md +7 -6
- package/docs/templates.md +15 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,26 @@ offline and describe the version installed here rather than whatever the
|
|
|
53
53
|
repository says today. If an agent is writing your extension, point it at
|
|
54
54
|
`node_modules/@ham2k/extension-sdk/AGENTS.md` — `h2kext-init` does that for you.
|
|
55
55
|
|
|
56
|
+
## Export types and settings
|
|
57
|
+
|
|
58
|
+
Export hooks can register shared types with `getExportTypes()`. Use
|
|
59
|
+
`exportTypeDefinition(activationType, format, label)` for an identity shared
|
|
60
|
+
by extensions serving the same activity. `exportType` identifies the settings;
|
|
61
|
+
`exportKey` identifies an individual file, such as one park in an activation.
|
|
62
|
+
|
|
63
|
+
The `activityExportHook` and `huntingExportHook` helpers register their types
|
|
64
|
+
and forward export settings to the ADIF generator. Reference activity types
|
|
65
|
+
use `templateCategory: 'reference'`; other types use the other-activity defaults.
|
|
66
|
+
`GlobalExportSettings` provides separate normal filename, compact filename,
|
|
67
|
+
and title templates for each category, plus shared ADIF field templates.
|
|
68
|
+
|
|
69
|
+
When updating an exporter from SDK 0.3, keep a reference-specific identifier
|
|
70
|
+
in `exportKey`, register a stable `exportType`, and forward `exportSettings`,
|
|
71
|
+
`exportData`, `exportTitle`, and `includeLookupData` when delegating generation.
|
|
72
|
+
Per-type custom templates override inherited defaults only when enabled.
|
|
73
|
+
See `docs/hooks.md` §`export` for the complete contract and `docs/forms.md`
|
|
74
|
+
for the `textTemplate` form field.
|
|
75
|
+
|
|
56
76
|
## About the peer dependencies
|
|
57
77
|
|
|
58
78
|
This package lists the libraries the extension host carries — `@ham2k/lib-*`,
|
package/dist/activityExports.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { exportTypeDefinition } from "./exportSettings.js";
|
|
1
2
|
import { exportFilename, startMillisOf } from "./exportNames.js";
|
|
2
3
|
import { segmentsWith } from "./segments.js";
|
|
3
4
|
import { createCachedTranslator } from "./i18n.js";
|
|
@@ -21,11 +22,23 @@ function filenameFor(operation, qsos, ref, compact) {
|
|
|
21
22
|
function activityExportHook(rules) {
|
|
22
23
|
const prefix = `${rules.key}-adif`;
|
|
23
24
|
return {
|
|
25
|
+
async getExportTypes() {
|
|
26
|
+
return [{
|
|
27
|
+
...exportTypeDefinition(rules.activationType, "adif", rules.label),
|
|
28
|
+
templateCategory: "reference",
|
|
29
|
+
templateSample: rules.templateSample ?? {
|
|
30
|
+
log: { ref: "REF-1234", refName: "Example Reference" },
|
|
31
|
+
operation: { refs: [{ type: rules.activationType, ref: "REF-1234" }] }
|
|
32
|
+
}
|
|
33
|
+
}];
|
|
34
|
+
},
|
|
24
35
|
async suggestExportOptions({ operation, qsos, compactFilenames }, ctx) {
|
|
25
36
|
const refs = refsOfType(operation, rules.activationType);
|
|
26
37
|
const t = tFor(ctx);
|
|
27
38
|
return refs.map((ref) => ({
|
|
28
|
-
exportType: `${
|
|
39
|
+
exportType: `${rules.activationType}-adif`,
|
|
40
|
+
templateData: { ref: ref.ref, refName: ref.name ?? "", activity: rules.label },
|
|
41
|
+
exportKey: `${prefix}${SEPARATOR}${ref.ref}`,
|
|
29
42
|
format: "adif",
|
|
30
43
|
// The FORMAT leads: a park activated on a contest weekend offers
|
|
31
44
|
// several files for the same reference, and "POTA US-1234" alone said
|
|
@@ -44,10 +57,11 @@ function activityExportHook(rules) {
|
|
|
44
57
|
},
|
|
45
58
|
async generateExport(args, ctx) {
|
|
46
59
|
const exportType = String(args.exportType ?? "");
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
const exportKey = String(args.exportKey ?? "");
|
|
61
|
+
if (exportType !== `${rules.activationType}-adif` || !exportKey.startsWith(`${prefix}${SEPARATOR}`)) {
|
|
62
|
+
throw new Error(`${rules.key}: unknown export type/key '${exportType}' / '${exportKey}'`);
|
|
49
63
|
}
|
|
50
|
-
const wanted =
|
|
64
|
+
const wanted = exportKey.slice(prefix.length + SEPARATOR.length);
|
|
51
65
|
if (!refsOfType(args.operation, rules.activationType).some((r) => r.ref === wanted)) {
|
|
52
66
|
throw new Error(`${rules.key}: operation has no ${rules.activationType} reference '${wanted}'`);
|
|
53
67
|
}
|
|
@@ -62,6 +76,10 @@ function activityExportHook(rules) {
|
|
|
62
76
|
qsos: args.qsos,
|
|
63
77
|
segments: segmentsWith(args.segments, claimOnly),
|
|
64
78
|
includePrivateData: args.includePrivateData,
|
|
79
|
+
includeLookupData: args.includeLookupData,
|
|
80
|
+
exportSettings: args.exportSettings,
|
|
81
|
+
exportData: args.exportData,
|
|
82
|
+
exportTitle: args.exportTitle,
|
|
65
83
|
mainHandler: rules.key,
|
|
66
84
|
includeFieldsFrom: rules.includeFieldsFrom
|
|
67
85
|
});
|
|
@@ -83,17 +101,22 @@ function filenameForHunt(operation, qsos, activity, compact) {
|
|
|
83
101
|
});
|
|
84
102
|
}
|
|
85
103
|
function huntingExportHook(rules) {
|
|
86
|
-
const exportType = `${rules.
|
|
104
|
+
const exportType = `${rules.huntingType}-hunter`;
|
|
87
105
|
function huntedQsos(qsos) {
|
|
88
106
|
return qsos.filter((qso) => refsOfType(qso, rules.huntingType).length > 0);
|
|
89
107
|
}
|
|
90
108
|
return {
|
|
109
|
+
async getExportTypes() {
|
|
110
|
+
return [{ exportType, format: "adif", label: `${rules.label} Hunter Log` }];
|
|
111
|
+
},
|
|
91
112
|
async suggestExportOptions({ operation, qsos, compactFilenames }, ctx) {
|
|
92
113
|
if (refsOfType(operation, rules.activationType).length > 0) return [];
|
|
93
114
|
if (huntedQsos(qsos ?? []).length === 0) return [];
|
|
94
115
|
const t = tFor(ctx);
|
|
95
116
|
return [{
|
|
96
117
|
exportType,
|
|
118
|
+
qsoCount: huntedQsos(qsos ?? []).length,
|
|
119
|
+
templateData: { activity: rules.label },
|
|
97
120
|
format: "adif",
|
|
98
121
|
label: t("adifForHunter", { program: rules.label }),
|
|
99
122
|
filename: filenameForHunt(operation, qsos ?? [], rules.label, compactFilenames),
|
|
@@ -119,6 +142,10 @@ function huntingExportHook(rules) {
|
|
|
119
142
|
qsos: huntedQsos(args.qsos ?? []),
|
|
120
143
|
segments: args.segments,
|
|
121
144
|
includePrivateData: args.includePrivateData,
|
|
145
|
+
includeLookupData: args.includeLookupData,
|
|
146
|
+
exportSettings: args.exportSettings,
|
|
147
|
+
exportData: args.exportData,
|
|
148
|
+
exportTitle: args.exportTitle,
|
|
122
149
|
// The hunter file is submitted to ONE program too — see
|
|
123
150
|
// `activityExportHook.generateExport`. `rules.key` is the ACTIVITY's
|
|
124
151
|
// key ('pota'), which is what its `adifFields` hook registers under;
|
|
@@ -136,7 +163,7 @@ function huntingExportHook(rules) {
|
|
|
136
163
|
}
|
|
137
164
|
};
|
|
138
165
|
}
|
|
139
|
-
async function adifForExport({ operation, qsos, segments, includePrivateData, mainHandler, includeFieldsFrom }) {
|
|
166
|
+
async function adifForExport({ operation, qsos, segments, includePrivateData, includeLookupData, exportSettings, exportData, exportTitle, mainHandler, includeFieldsFrom }) {
|
|
140
167
|
const { hooks } = await import("@ham2k/extension-sdk");
|
|
141
168
|
const entries = await hooks.invokeOne("export", "adif", "generateExport", {
|
|
142
169
|
operation,
|
|
@@ -146,6 +173,10 @@ async function adifForExport({ operation, qsos, segments, includePrivateData, ma
|
|
|
146
173
|
// Forwarded, or the delegate defaults to withholding and the user's
|
|
147
174
|
// "include private info" setting silently does nothing here.
|
|
148
175
|
includePrivateData,
|
|
176
|
+
includeLookupData,
|
|
177
|
+
exportSettings,
|
|
178
|
+
exportData,
|
|
179
|
+
exportTitle,
|
|
149
180
|
mainHandler,
|
|
150
181
|
includeFieldsFrom
|
|
151
182
|
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { startMillisOf } from "./exportNames.js";
|
|
2
|
+
import { templateContext } from "./templateContext.js";
|
|
3
|
+
import { oneLine, renderTemplate } from "./templates.js";
|
|
4
|
+
import { isTestOperation } from "./testOperation.js";
|
|
5
|
+
const EXPORT_DEFAULTS = {
|
|
6
|
+
includePrivateData: false,
|
|
7
|
+
includeLookupData: true,
|
|
8
|
+
customTemplates: false,
|
|
9
|
+
filenameTemplate: "{{ op.date }} {{ log.station }}{% if log.ref != blank %} at {{ log.ref }}{% elsif log.activity != blank %} for {{ log.activity }}{% endif %} {{ log.modifier }}",
|
|
10
|
+
compactFilenameTemplate: "{{ log.station | dash }}{% if log.ref != blank %}@{{ log.ref | dash }}{% elsif log.activity != blank %}-{{ log.activity | downcase | dash }}{% endif %}-{{ op.dateCompact }}{% if log.modifier != blank %}-{{ log.modifier | downcase | dash }}{% endif %}",
|
|
11
|
+
titleTemplate: "{{ log.station }}{% if log.ref != blank %}: {{ log.activity }} at {{ log.ref }} {{ log.refName }}{% elsif log.activity != blank %}: {{ log.activity }}{% endif %} on {{ op.date }}",
|
|
12
|
+
adifNotesTemplate: "{{ qso.notes }}",
|
|
13
|
+
adifCommentTemplate: "{{ qso.notes }}",
|
|
14
|
+
adifQslMessageTemplate: ""
|
|
15
|
+
};
|
|
16
|
+
const GLOBAL_EXPORT_DEFAULTS = {
|
|
17
|
+
...EXPORT_DEFAULTS,
|
|
18
|
+
referenceFilenameTemplate: "{{ op.date }} {{ log.station }} at {{ log.ref }} {{ log.modifier }}",
|
|
19
|
+
referenceCompactFilenameTemplate: "{{ log.station | dash }}@{{ log.ref | dash }}-{{ op.dateCompact }}{% if log.modifier != blank %}-{{ log.modifier | downcase | dash }}{% endif %}",
|
|
20
|
+
referenceTitleTemplate: "{{ log.station }}: {{ log.activity }} at {{ log.ref }} {{ log.refName }} on {{ op.date }}",
|
|
21
|
+
otherFilenameTemplate: "{{ op.date }} {{ log.station }}{% if log.activity != blank %} for {{ log.activity }}{% endif %} {{ log.modifier }}",
|
|
22
|
+
otherCompactFilenameTemplate: "{{ log.station | dash }}{% if log.activity != blank %}-{{ log.activity | downcase | dash }}{% endif %}-{{ op.dateCompact }}{% if log.modifier != blank %}-{{ log.modifier | downcase | dash }}{% endif %}",
|
|
23
|
+
otherTitleTemplate: "{{ log.station }}{% if log.activity != blank %}: {{ log.activity }}{% endif %} on {{ op.date }}"
|
|
24
|
+
};
|
|
25
|
+
function resolveGlobalExportSettings(global = {}) {
|
|
26
|
+
const resolved = { ...GLOBAL_EXPORT_DEFAULTS, ...global };
|
|
27
|
+
for (const prefix of ["reference", "other"]) {
|
|
28
|
+
for (const suffix of ["FilenameTemplate", "CompactFilenameTemplate", "TitleTemplate"]) {
|
|
29
|
+
const key = `${prefix}${suffix}`;
|
|
30
|
+
const legacy = `${suffix[0].toLowerCase()}${suffix.slice(1)}`;
|
|
31
|
+
if (global[key] === void 0 && typeof global[legacy] === "string") {
|
|
32
|
+
Object.assign(resolved, { [key]: global[legacy] });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
function exportTypeDefinition(activationType, format, label, defaults) {
|
|
39
|
+
return { exportType: `${activationType}-${format}`, activationType, format, label, defaults };
|
|
40
|
+
}
|
|
41
|
+
const templateKeys = [
|
|
42
|
+
"filenameTemplate",
|
|
43
|
+
"compactFilenameTemplate",
|
|
44
|
+
"titleTemplate",
|
|
45
|
+
"adifNotesTemplate",
|
|
46
|
+
"adifCommentTemplate",
|
|
47
|
+
"adifQslMessageTemplate"
|
|
48
|
+
];
|
|
49
|
+
function resolveExportSettings(definition, global = {}, overrides = {}) {
|
|
50
|
+
const defaults = resolveGlobalExportSettings(global);
|
|
51
|
+
const reference = definition.templateCategory === "reference";
|
|
52
|
+
const resolved = {
|
|
53
|
+
includePrivateData: defaults.includePrivateData,
|
|
54
|
+
includeLookupData: defaults.includeLookupData,
|
|
55
|
+
adifNotesTemplate: defaults.adifNotesTemplate,
|
|
56
|
+
adifCommentTemplate: defaults.adifCommentTemplate,
|
|
57
|
+
adifQslMessageTemplate: defaults.adifQslMessageTemplate,
|
|
58
|
+
filenameTemplate: reference ? defaults.referenceFilenameTemplate : defaults.otherFilenameTemplate,
|
|
59
|
+
compactFilenameTemplate: reference ? defaults.referenceCompactFilenameTemplate : defaults.otherCompactFilenameTemplate,
|
|
60
|
+
titleTemplate: reference ? defaults.referenceTitleTemplate : defaults.otherTitleTemplate,
|
|
61
|
+
...definition.defaults
|
|
62
|
+
};
|
|
63
|
+
for (const key of ["includePrivateData", "includeLookupData"]) {
|
|
64
|
+
if (typeof overrides[key] === "boolean") resolved[key] = overrides[key];
|
|
65
|
+
}
|
|
66
|
+
resolved.customTemplates = overrides.customTemplates === true;
|
|
67
|
+
if (resolved.customTemplates) {
|
|
68
|
+
for (const key of templateKeys) {
|
|
69
|
+
if (typeof overrides[key] === "string") resolved[key] = overrides[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (definition.format !== "adif") {
|
|
73
|
+
delete resolved.includePrivateData;
|
|
74
|
+
delete resolved.includeLookupData;
|
|
75
|
+
for (const key of templateKeys.slice(2)) delete resolved[key];
|
|
76
|
+
}
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
function exportQso(qso, settings) {
|
|
80
|
+
const copy = { ...qso };
|
|
81
|
+
for (const side of ["their", "our"]) {
|
|
82
|
+
if (copy[side] && typeof copy[side] === "object" && !Array.isArray(copy[side])) {
|
|
83
|
+
const station = { ...copy[side] };
|
|
84
|
+
if (settings.includeLookupData === false) delete station.guess;
|
|
85
|
+
if (settings.includePrivateData === false) {
|
|
86
|
+
for (const key of ["name", "city", "email", "notes", "lat", "lon", "multiIdentifier"]) delete station[key];
|
|
87
|
+
if (typeof station.grid === "string") station.grid = station.grid.slice(0, 6);
|
|
88
|
+
if (station.guess && typeof station.guess === "object") {
|
|
89
|
+
const guess = { ...station.guess };
|
|
90
|
+
for (const key of ["name", "city", "email", "notes", "lat", "lon", "multiIdentifier"]) delete guess[key];
|
|
91
|
+
if (typeof guess.grid === "string") guess.grid = guess.grid.slice(0, 6);
|
|
92
|
+
station.guess = guess;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
copy[side] = station;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (settings.includePrivateData === false) delete copy.notes;
|
|
99
|
+
return copy;
|
|
100
|
+
}
|
|
101
|
+
function prepareExportOption(option, operation, qsos, compact) {
|
|
102
|
+
const settings = option.exportSettings;
|
|
103
|
+
const extension = { adif: "adi", cabrillo: "log", reg1test: "edi" }[option.format] ?? option.filename?.split(".").pop() ?? "txt";
|
|
104
|
+
const data = {
|
|
105
|
+
station: String(operation.stationCall ?? ""),
|
|
106
|
+
ref: "",
|
|
107
|
+
activity: "",
|
|
108
|
+
modifier: "",
|
|
109
|
+
...option.templateData,
|
|
110
|
+
exportType: option.exportType,
|
|
111
|
+
format: option.format,
|
|
112
|
+
extension,
|
|
113
|
+
compact
|
|
114
|
+
};
|
|
115
|
+
if (isTestOperation(operation.stationCall)) data.modifier = [data.modifier, "Testing"].filter(Boolean).join(" ");
|
|
116
|
+
const context = templateContext({ operation, qsoCount: option.qsoCount ?? qsos.length, atMillis: startMillisOf(operation, qsos) ?? Date.now(), log: data });
|
|
117
|
+
const template = compact ? settings.compactFilenameTemplate : settings.filenameTemplate;
|
|
118
|
+
const rendered = oneLine(renderTemplate(template ?? "", context, "Export filename"));
|
|
119
|
+
const stem = rendered.replace(/[/\\:*?"<>|\x00-\x1f]+/g, "-").replace(/[. ]+$/g, "").trim() || "export";
|
|
120
|
+
const filename = stem.toLowerCase().endsWith(`.${extension}`) ? stem : `${stem}.${extension}`;
|
|
121
|
+
const titleContext = settings.includePrivateData === false ? { ...context, op: { ...context.op, title: "", userTitle: "", userNotes: "", grid: String(context.op.grid ?? "").slice(0, 6) }, log: { ...context.log, modifier: "" } } : context;
|
|
122
|
+
const title = oneLine(renderTemplate(settings.titleTemplate ?? "", titleContext, "Export title"));
|
|
123
|
+
return { ...option, filename, exportTitle: title, exportData: settings.includePrivateData === false ? { ...data, modifier: "" } : data };
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
EXPORT_DEFAULTS,
|
|
127
|
+
GLOBAL_EXPORT_DEFAULTS,
|
|
128
|
+
exportQso,
|
|
129
|
+
exportTypeDefinition,
|
|
130
|
+
prepareExportOption,
|
|
131
|
+
resolveExportSettings,
|
|
132
|
+
resolveGlobalExportSettings
|
|
133
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.
|
|
1
|
+
// @ham2k/extension-sdk 0.4.0
|
|
2
2
|
export type CallInfo = {
|
|
3
3
|
call: string;
|
|
4
4
|
baseCall?: string;
|
|
@@ -388,11 +388,44 @@ export interface AdifImportHook {
|
|
|
388
388
|
}[];
|
|
389
389
|
}, ctx: HookContext): Promise<(AdifImportResult | null)[]>;
|
|
390
390
|
}
|
|
391
|
+
export interface ExportSettings {
|
|
392
|
+
includePrivateData?: boolean;
|
|
393
|
+
includeLookupData?: boolean;
|
|
394
|
+
customTemplates?: boolean;
|
|
395
|
+
filenameTemplate?: string;
|
|
396
|
+
compactFilenameTemplate?: string;
|
|
397
|
+
titleTemplate?: string;
|
|
398
|
+
adifNotesTemplate?: string;
|
|
399
|
+
adifCommentTemplate?: string;
|
|
400
|
+
adifQslMessageTemplate?: string;
|
|
401
|
+
}
|
|
402
|
+
export interface GlobalExportSettings extends ExportSettings {
|
|
403
|
+
referenceFilenameTemplate?: string;
|
|
404
|
+
referenceCompactFilenameTemplate?: string;
|
|
405
|
+
referenceTitleTemplate?: string;
|
|
406
|
+
otherFilenameTemplate?: string;
|
|
407
|
+
otherCompactFilenameTemplate?: string;
|
|
408
|
+
otherTitleTemplate?: string;
|
|
409
|
+
}
|
|
410
|
+
export interface ExportTypeDefinition {
|
|
411
|
+
exportType: string;
|
|
412
|
+
label: string;
|
|
413
|
+
format: string;
|
|
414
|
+
activationType?: string;
|
|
415
|
+
templateCategory?: "reference" | "other";
|
|
416
|
+
defaults?: ExportSettings;
|
|
417
|
+
templateSample?: Record<string, JSONValue>;
|
|
418
|
+
}
|
|
391
419
|
export interface ExportRequest {
|
|
420
|
+
exportSettings?: ExportSettings;
|
|
421
|
+
exportData?: Record<string, JSONValue>;
|
|
422
|
+
exportTitle?: string;
|
|
423
|
+
includeLookupData?: boolean;
|
|
392
424
|
operation: Record<string, JSONValue>;
|
|
393
425
|
qsos: Record<string, JSONValue>[];
|
|
394
426
|
segments?: OperationSegmentPayload[];
|
|
395
427
|
exportType?: string;
|
|
428
|
+
exportKey?: string;
|
|
396
429
|
compactFilenames?: boolean;
|
|
397
430
|
includePrivateData?: boolean;
|
|
398
431
|
mainHandler?: string;
|
|
@@ -409,7 +442,10 @@ export interface ExportOptionsRequest {
|
|
|
409
442
|
compactFilenames?: boolean;
|
|
410
443
|
}
|
|
411
444
|
export interface ExportOption {
|
|
445
|
+
qsoCount?: number;
|
|
446
|
+
templateData?: Record<string, JSONValue>;
|
|
412
447
|
exportType: string;
|
|
448
|
+
exportKey?: string;
|
|
413
449
|
format: string;
|
|
414
450
|
label: string;
|
|
415
451
|
filename?: string;
|
|
@@ -421,6 +457,7 @@ export interface ExportOption {
|
|
|
421
457
|
refType?: string;
|
|
422
458
|
}
|
|
423
459
|
export interface ExportHook {
|
|
460
|
+
getExportTypes?(args: Record<string, never>, ctx: HookContext): Promise<ExportTypeDefinition[]>;
|
|
424
461
|
suggestExportOptions?(args: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
425
462
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
426
463
|
}
|
|
@@ -871,7 +908,7 @@ export interface DataFileDefinition {
|
|
|
871
908
|
onLoadRawData?: (data: any) => void;
|
|
872
909
|
onRemoveRawData?: () => Promise<void>;
|
|
873
910
|
}
|
|
874
|
-
export type FormFieldType = "text" | "multiline" | "email" | "callsign" | "number" | "select" | "radio" | "checkbox" | "secret" | "list" | "multiselect" | "account";
|
|
911
|
+
export type FormFieldType = "text" | "multiline" | "textTemplate" | "email" | "callsign" | "number" | "select" | "radio" | "checkbox" | "secret" | "list" | "multiselect" | "account";
|
|
875
912
|
export interface FormFieldOption {
|
|
876
913
|
label: string;
|
|
877
914
|
value: any;
|
|
@@ -885,6 +922,8 @@ export interface FormField {
|
|
|
885
922
|
label: string;
|
|
886
923
|
value?: any;
|
|
887
924
|
placeholder?: string;
|
|
925
|
+
templateContext?: "export" | "adif" | "text";
|
|
926
|
+
templateSample?: Record<string, JSONValue>;
|
|
888
927
|
uppercase?: boolean;
|
|
889
928
|
options?: FormFieldOption[] | string;
|
|
890
929
|
validate?: (value: any, state: Record<string, any>) => string | null | undefined | Promise<string | null | undefined>;
|
|
@@ -934,6 +973,8 @@ export interface FormMarkdownBlock {
|
|
|
934
973
|
title?: string;
|
|
935
974
|
}
|
|
936
975
|
export interface FormActionElement {
|
|
976
|
+
style?: "row";
|
|
977
|
+
icon?: string;
|
|
937
978
|
type: "action";
|
|
938
979
|
key: string;
|
|
939
980
|
label: string;
|
|
@@ -1235,12 +1276,14 @@ export type ActivityScoresheet = {
|
|
|
1235
1276
|
export declare function activityScorer(rules: ActivityScoringRules): ContestScorer<ActivityScoresheet>;
|
|
1236
1277
|
export interface ActivityExportRules {
|
|
1237
1278
|
key: string;
|
|
1279
|
+
templateSample?: Record<string, JSONValue>;
|
|
1238
1280
|
label: string;
|
|
1239
1281
|
activationType: string;
|
|
1240
1282
|
icon?: string;
|
|
1241
1283
|
includeFieldsFrom?: string[];
|
|
1242
1284
|
}
|
|
1243
1285
|
export declare function activityExportHook(rules: ActivityExportRules): {
|
|
1286
|
+
getExportTypes(): Promise<ExportTypeDefinition[]>;
|
|
1244
1287
|
suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
1245
1288
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
1246
1289
|
};
|
|
@@ -1254,14 +1297,19 @@ export interface HuntingExportRules {
|
|
|
1254
1297
|
includeFieldsFrom?: string[];
|
|
1255
1298
|
}
|
|
1256
1299
|
export declare function huntingExportHook(rules: HuntingExportRules): {
|
|
1300
|
+
getExportTypes(): Promise<ExportTypeDefinition[]>;
|
|
1257
1301
|
suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
1258
1302
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
1259
1303
|
};
|
|
1260
|
-
export declare function adifForExport({ operation, qsos, segments, includePrivateData, mainHandler, includeFieldsFrom }: {
|
|
1304
|
+
export declare function adifForExport({ operation, qsos, segments, includePrivateData, includeLookupData, exportSettings, exportData, exportTitle, mainHandler, includeFieldsFrom }: {
|
|
1261
1305
|
operation: Record<string, JSONValue>;
|
|
1262
1306
|
qsos: Record<string, JSONValue>[];
|
|
1263
1307
|
segments?: OperationSegmentPayload[];
|
|
1264
1308
|
includePrivateData?: boolean;
|
|
1309
|
+
includeLookupData?: boolean;
|
|
1310
|
+
exportSettings?: ExportSettings;
|
|
1311
|
+
exportData?: Record<string, JSONValue>;
|
|
1312
|
+
exportTitle?: string;
|
|
1265
1313
|
mainHandler: string;
|
|
1266
1314
|
includeFieldsFrom?: string[];
|
|
1267
1315
|
}): Promise<string>;
|
|
@@ -1284,6 +1332,19 @@ export interface ExportNameParts {
|
|
|
1284
1332
|
}
|
|
1285
1333
|
export declare function exportFilename(parts: ExportNameParts): string;
|
|
1286
1334
|
export declare function startMillisOf(operation: Record<string, JSONValue>, qsos?: Record<string, JSONValue>[]): number | undefined;
|
|
1335
|
+
export declare const EXPORT_DEFAULTS: ExportSettings;
|
|
1336
|
+
export declare const GLOBAL_EXPORT_DEFAULTS: GlobalExportSettings;
|
|
1337
|
+
export declare function resolveGlobalExportSettings(global?: GlobalExportSettings): GlobalExportSettings;
|
|
1338
|
+
export declare function exportTypeDefinition(activationType: string, format: string, label: string, defaults?: ExportSettings): ExportTypeDefinition;
|
|
1339
|
+
export declare function resolveExportSettings(definition: ExportTypeDefinition, global?: GlobalExportSettings, overrides?: ExportSettings): ExportSettings;
|
|
1340
|
+
export declare function exportQso(qso: Record<string, JSONValue>, settings: ExportSettings): Record<string, JSONValue>;
|
|
1341
|
+
export declare function prepareExportOption(option: ExportOption & {
|
|
1342
|
+
exportSettings: ExportSettings;
|
|
1343
|
+
}, operation: Record<string, JSONValue>, qsos: Record<string, JSONValue>[], compact: boolean): ExportOption & {
|
|
1344
|
+
exportSettings: ExportSettings;
|
|
1345
|
+
exportTitle: string;
|
|
1346
|
+
exportData: Record<string, JSONValue>;
|
|
1347
|
+
};
|
|
1287
1348
|
export type RefTransform = {
|
|
1288
1349
|
pattern: string;
|
|
1289
1350
|
replacement: string;
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./activityScoring.js";
|
|
|
9
9
|
export * from "./activityExports.js";
|
|
10
10
|
export * from "./activityAdifImport.js";
|
|
11
11
|
export * from "./exportNames.js";
|
|
12
|
+
export * from "./exportSettings.js";
|
|
12
13
|
export * from "./referenceActivity.js";
|
|
13
14
|
export * from "./refTransforms.js";
|
|
14
15
|
export * from "./templates.js";
|
package/docs/forms.md
CHANGED
|
@@ -14,6 +14,7 @@ All types are defined in `extensions/sdk/src/types.ts`.
|
|
|
14
14
|
export type FormFieldType =
|
|
15
15
|
| 'text'
|
|
16
16
|
| 'multiline'
|
|
17
|
+
| 'textTemplate'
|
|
17
18
|
| 'email'
|
|
18
19
|
| 'callsign'
|
|
19
20
|
| 'number'
|
|
@@ -70,8 +71,8 @@ export interface FormField {
|
|
|
70
71
|
// fields — a whole section header can be dev-mode-only too.
|
|
71
72
|
devMode?: boolean;
|
|
72
73
|
// Settings-panel-only (ignored in ad hoc forms) — see settings.md's
|
|
73
|
-
// "Common
|
|
74
|
-
// the app's Common
|
|
74
|
+
// "Common Preferences and environment gating". Also shows this field on
|
|
75
|
+
// the app's Common Preferences quick-access panel.
|
|
75
76
|
common?: boolean;
|
|
76
77
|
// Settings-panel-only. Restricts which platform(s) show this field at
|
|
77
78
|
// all: one or more platform tokens (`ios`, `android`, `macos`,
|
|
@@ -129,6 +130,8 @@ export interface FormMarkdownBlock {
|
|
|
129
130
|
// SDK already permits (see settings.md's settingsPanel note). Not used for
|
|
130
131
|
// account credentials — those have their own testCredentials on AccountHook.
|
|
131
132
|
export interface FormActionElement {
|
|
133
|
+
style?: 'row'; // icon row with a chevron; omit for a button
|
|
134
|
+
icon?: string;
|
|
132
135
|
type: 'action';
|
|
133
136
|
key: string;
|
|
134
137
|
label: string;
|
|
@@ -277,3 +280,28 @@ api.registerHook('form', {
|
|
|
277
280
|
- Any validation error returns back to Dart and is displayed underneath the field. If any fields have errors, submission is aborted.
|
|
278
281
|
3. **Submit transformation**: When all fields are successfully validated, Dart invokes any JS transformation callbacks for each field.
|
|
279
282
|
4. **Completion**: The final transformed state is resolved and returned back to the caller (or saved in the view context).
|
|
283
|
+
|
|
284
|
+
## Text templates
|
|
285
|
+
|
|
286
|
+
`fieldType: 'textTemplate'` is a string field displayed as an editor row in
|
|
287
|
+
forms and settings. It opens a multiline Liquid editor with a sample preview,
|
|
288
|
+
syntax errors, insertable attributes and flow-control examples, and a read-only
|
|
289
|
+
filter reference inside collapsed Templating Docs. Editing and preview stay
|
|
290
|
+
above the scrollable reference. Template rows also display rendered samples. Save commits
|
|
291
|
+
the draft; Cancel, Escape and dismissal discard it. `defaultValue` supplies
|
|
292
|
+
the Reset button. Disabled fields cannot open the editor.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
{
|
|
296
|
+
type: 'field', fieldType: 'textTemplate', key: 'filename',
|
|
297
|
+
label: 'File name', value: '{{ op.date }} {{ log.station }}',
|
|
298
|
+
templateContext: 'export', // 'adif' adds contact attributes; 'text' is generic
|
|
299
|
+
templateSample: { log: { activity: 'POTA', ref: 'US-1234' } },
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The preview uses the same runtime and vocabulary as exports; sample values
|
|
304
|
+
are illustrative. `templateSample` can replace values in `operation`, `qso`
|
|
305
|
+
and `log`. The app provides the rendering callback through
|
|
306
|
+
`TemplateEditorScope`, keeping the form widget independent of extensions.
|
|
307
|
+
See [templates.md](templates.md) for Liquid syntax and supported namespaces.
|
package/docs/hooks.md
CHANGED
|
@@ -248,23 +248,91 @@ fetch-only for now.
|
|
|
248
248
|
|
|
249
249
|
```ts
|
|
250
250
|
interface ExportHook {
|
|
251
|
+
getExportTypes?(args: {}, ctx): Promise<ExportTypeDefinition[]>
|
|
251
252
|
suggestExportOptions?(args: ExportOptionsRequest, ctx): Promise<ExportOption[]>
|
|
252
253
|
generateExport(args: ExportRequest, ctx): Promise<ExportResult>
|
|
253
254
|
}
|
|
254
255
|
// ExportOptionsRequest: {operation, qsos, compactFilenames?}
|
|
255
|
-
// ExportOption: {exportType, format, label, filename?, icon?, color?, refType?, priority?, selectedByDefault?}
|
|
256
|
-
// ExportRequest: {operation, qsos, exportType?, compactFilenames?} — full QSON, one coarse call
|
|
256
|
+
// ExportOption: {exportType, exportKey?, format, label, filename?, icon?, color?, refType?, priority?, selectedByDefault?}
|
|
257
|
+
// ExportRequest: {operation, qsos, exportType?, exportKey?, compactFilenames?} — full QSON, one coarse call
|
|
257
258
|
// ExportResult: {filename, mimeType, content}
|
|
258
259
|
```
|
|
259
260
|
|
|
260
261
|
Two-step flow: the Exports Panel calls `suggestExportOptions` on every
|
|
261
262
|
`export` hook to build its selectable list (a hook that omits it never
|
|
262
263
|
appears in the panel), then calls `generateExport` — passing back the
|
|
263
|
-
chosen option's `exportType` — only for the options the user
|
|
264
|
-
core owns file I/O and save/share; the hook only produces content.
|
|
264
|
+
chosen option's `exportType` and `exportKey` — only for the options the user
|
|
265
|
+
selected. The core owns file I/O and save/share; the hook only produces content.
|
|
266
|
+
|
|
267
|
+
`exportType` is the stable kind (for example `potaActivation-adif`); `exportKey`
|
|
268
|
+
identifies an individual option within the hook (for example
|
|
269
|
+
`pota-adif:US-1234`). Options sharing a type must have distinct keys. Omit
|
|
270
|
+
`exportKey` for a single option of a type: selection and generation default
|
|
271
|
+
it to `exportType`. Both values round-trip unchanged when an explicit key is
|
|
272
|
+
provided; the core does not interpret them. Selection also includes the
|
|
273
|
+
hook key and station callsign, keeping different hooks and stations independent.
|
|
274
|
+
|
|
265
275
|
Exporters should compose per-QSO program fields from `adifFields` hooks (see
|
|
266
|
-
below) rather than knowing about specific activities. Implemented by
|
|
267
|
-
|
|
276
|
+
below) rather than knowing about specific activities. Implemented by the whole-log `adif` exporter, activity exports, and contest
|
|
277
|
+
ADIF/Cabrillo exporters.
|
|
278
|
+
|
|
279
|
+
**Registering types and settings.** `getExportTypes` runs without an operation,
|
|
280
|
+
so Settings can list types even before the operator has a matching log:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
async getExportTypes() {
|
|
284
|
+
return [{
|
|
285
|
+
exportType: 'potaActivation-adif',
|
|
286
|
+
activationType: 'potaActivation',
|
|
287
|
+
format: 'adif',
|
|
288
|
+
label: 'POTA',
|
|
289
|
+
defaults: { includePrivateData: false, includeLookupData: true },
|
|
290
|
+
}]
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Prefer `<activationType>-<format>` to an extension-key namespace. Two
|
|
295
|
+
extensions offering that type share one settings entry. The highest-priority
|
|
296
|
+
registration supplies its definition; other exporters using it must honor
|
|
297
|
+
that contract. Distinct sponsor requirements can use a distinct type. The
|
|
298
|
+
SDK's `exportTypeDefinition(activationType, format, label, defaults?)` builds
|
|
299
|
+
the conventional definition; the activity and hunting helpers register their
|
|
300
|
+
own types. An exporter without `getExportTypes` still works, but does not
|
|
301
|
+
appear in the export settings list.
|
|
302
|
+
|
|
303
|
+
`ExportSettings` contains `includePrivateData`, `includeLookupData`,
|
|
304
|
+
`customTemplates`, `filenameTemplate`, `compactFilenameTemplate`,
|
|
305
|
+
`titleTemplate`, `adifNotesTemplate`, `adifCommentTemplate`, and
|
|
306
|
+
`adifQslMessageTemplate`. All are optional. Defaults resolve from the SDK,
|
|
307
|
+
then global preferences, then the type's declared defaults.
|
|
308
|
+
`GlobalExportSettings` adds `referenceFilenameTemplate`,
|
|
309
|
+
`referenceCompactFilenameTemplate`, `referenceTitleTemplate` and the matching
|
|
310
|
+
`other…` fields. A type with `templateCategory: 'reference'` uses the reference
|
|
311
|
+
set; other types use the other set. The activity export helper registers
|
|
312
|
+
reference types automatically. ADIF field templates remain common to both sets.
|
|
313
|
+
Saved common filename/title defaults remain fallbacks for categories without
|
|
314
|
+
an explicit setting. Per-type data
|
|
315
|
+
choices override those; per-type template overrides apply only when
|
|
316
|
+
`customTemplates` is on. Empty templates intentionally suppress text. Turning
|
|
317
|
+
custom templates off preserves the operator's edits for later. Non-ADIF
|
|
318
|
+
formats expose only the filename templates and their custom-template switch.
|
|
319
|
+
|
|
320
|
+
Type definitions may supply `templateSample: {operation, log}` for representative
|
|
321
|
+
editor examples. Each format supplies its own filename extension in the sample.
|
|
322
|
+
|
|
323
|
+
An option can supply `qsoCount` when its hook further filters the contacts
|
|
324
|
+
(for example, a hunter export). Otherwise the host derives the count from
|
|
325
|
+
activity and station scope. Filename and title templates use this count.
|
|
326
|
+
|
|
327
|
+
Options can carry `templateData: {ref, refName, activity, modifier, ...}`,
|
|
328
|
+
which populates the `log` namespace. The app renders filenames and titles
|
|
329
|
+
before showing the option, and passes the same `exportSettings`, `exportData`
|
|
330
|
+
and `exportTitle` to generation. It also passes the resolved
|
|
331
|
+
`includePrivateData` and `includeLookupData` for ADIF. Forward those fields
|
|
332
|
+
when delegating through `adifForExport`, or a program's preferences will not
|
|
333
|
+
reach the ADIF writer. Settings apply globally and per type, never per operation.
|
|
334
|
+
The whole-log ADIF defaults to including private data, but its type setting
|
|
335
|
+
can override that default.
|
|
268
336
|
|
|
269
337
|
**How a row looks.** `refType` names the activity the export covers — the
|
|
270
338
|
core slices the file's QSOs by it, and the panel takes the row's icon and
|
package/docs/settings.md
CHANGED
|
@@ -372,17 +372,18 @@ Aliases are for names an operator would plausibly *reach for* — not a thesauru
|
|
|
372
372
|
Every alias is one more thing that can collide with another setting's real name,
|
|
373
373
|
and a collision costs a disambiguation prompt on a query that used to be exact.
|
|
374
374
|
|
|
375
|
-
## Common
|
|
375
|
+
## Common Preferences and environment gating
|
|
376
376
|
|
|
377
|
-
The app has one settings screen, **Settings
|
|
378
|
-
|
|
377
|
+
The app has one settings screen, **Settings**, whose **Application
|
|
378
|
+
Preferences** panel has a collapsible section per declared group and panel,
|
|
379
|
+
led by **Common Preferences**, a short list of the
|
|
379
380
|
handful of settings most users ever touch. Which fields land on Common
|
|
380
|
-
|
|
381
|
+
Preferences —
|
|
381
382
|
and which platforms show a field at all — is declared per-element, not
|
|
382
383
|
maintained as a separate list somewhere else in the app. Any field, link, or
|
|
383
384
|
action (core-declared or extension-declared, Tier 1 or Tier 2 alike) can set:
|
|
384
385
|
|
|
385
|
-
- **`common: true`** — also show this element in the Common
|
|
386
|
+
- **`common: true`** — also show this element in the Common Preferences
|
|
386
387
|
section, on top of its own group's. Off by default. Not the same thing as
|
|
387
388
|
`FormFieldOption.common`/`uncommon` above — that's a `multiselect`
|
|
388
389
|
option's own disclosure tier (upfront vs. behind "Show more"), a
|
|
@@ -395,7 +396,7 @@ action (core-declared or extension-declared, Tier 1 or Tier 2 alike) can set:
|
|
|
395
396
|
- **`environment: 'ios,android'`** (or `'-web'`, or an array either way) —
|
|
396
397
|
restrict which platform(s) show this element at all. This is a platform
|
|
397
398
|
gate, not a common-vs-everything one: an excluded field disappears from every
|
|
398
|
-
settings surface on that platform, not just Common
|
|
399
|
+
settings surface on that platform, not just Common Preferences. Every
|
|
399
400
|
token unprefixed is a whitelist (show ONLY there); every token
|
|
400
401
|
`-`-prefixed is a blacklist (show everywhere EXCEPT there); mixing the two
|
|
401
402
|
forms in one attribute is invalid and treated as if `environment` were
|
package/docs/templates.md
CHANGED
|
@@ -19,7 +19,7 @@ const text = renderTemplate("{{ op.station }} — {{ op.qsoCount }} QSOs", templ
|
|
|
19
19
|
|
|
20
20
|
A template is DATA. Liquid parses and interprets; Handlebars compiles
|
|
21
21
|
through `new Function`. These templates are written by operators, copied
|
|
22
|
-
between operators, and
|
|
22
|
+
between operators, and are editable in settings, so the one
|
|
23
23
|
that cannot become code is the one to have. Liquid also has comparisons
|
|
24
24
|
(`{% if op.qsoCount > 100 %}`) and loops built in, where Handlebars needs a
|
|
25
25
|
registered helper for `>` and Mustache cannot compare at all.
|
|
@@ -42,15 +42,15 @@ the namespaces are deliberately polo's. The syntax around them does not:
|
|
|
42
42
|
|---|---|---|---|---|
|
|
43
43
|
| `app` | ✓ | — | ✓ | ✓ |
|
|
44
44
|
| `now` | ✓ | ✓ | ✓ | ✓ |
|
|
45
|
-
| `op` | ✓ |
|
|
45
|
+
| `op` | ✓ | ✓ | ✓ | ✓ |
|
|
46
46
|
| `qso` | when the placement's triggers ask for it | — | ✓ | the draft contact, as far as it is typed |
|
|
47
47
|
| `config` | ✓ | — | — | — |
|
|
48
48
|
| `log` | — | ✓ | ✓ | — |
|
|
49
49
|
|
|
50
50
|
A namespace a surface has nothing for is **absent**, not blank — which is
|
|
51
|
-
what makes `{% if qso %}` an honest question.
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
what makes `{% if qso %}` an honest question. Registered exports get the operation plus the selected file’s `log` values.
|
|
52
|
+
The low-level `exportFilename(parts)` helper still supplies only dates and
|
|
53
|
+
filename parts.
|
|
54
54
|
|
|
55
55
|
### `app`
|
|
56
56
|
`app.name` — the platform-appropriate name ("Ham2K Logger" on desktop,
|
|
@@ -191,16 +191,16 @@ will jump rather than count. Show HH:MM.
|
|
|
191
191
|
| what | where | editable |
|
|
192
192
|
|---|---|---|
|
|
193
193
|
| Panel documents | `custom-text`'s content and tab name | by the operator, in the panel's config form |
|
|
194
|
-
| Export filenames | `sdk/src/
|
|
195
|
-
| ADIF NOTES / COMMENT / QSLMSG | `core/adif`
|
|
194
|
+
| Export filenames and titles | Registered export types and `sdk/src/exportSettings.ts` | Settings → Exports, globally and per type |
|
|
195
|
+
| ADIF NOTES / COMMENT / QSLMSG | Export type settings, consumed by `core/adif` | Settings → Exports; empty templates suppress a field |
|
|
196
196
|
| CW messages | Radio settings `cwMessage1..8`, keyed on F1-F8 through the radio (docs/design/cat.md § CW keying) — rendered by the `template` hook (hooks.md) | by the operator, in the Station dialog's Messages… dialog |
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
defaults
|
|
200
|
-
references
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
NOTES and COMMENT default to QSO notes and are withheld when private data
|
|
199
|
+
is off. QSLMSG defaults to empty for program exports, and to the operation’s
|
|
200
|
+
references for the whole-log ADIF. Custom QSL messages may use contact values.
|
|
201
|
+
Templates receive no withheld private or lookup values. File titles obey the
|
|
202
|
+
private-data choice; filename templates are labels for the operator’s files.
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
The `textTemplate` form field provides sample previews and an insertable
|
|
205
|
+
reference; see [forms.md](forms.md). Invalid Liquid reports an error rather
|
|
206
|
+
than silently replacing the operator’s template with a default.
|