CiviCRM APIv4 file custom fields return a bare file id by default; an
extra '.file_name' join is required to get the human-readable filename.
Both the form prefill walk (lib/prefill.ts) and the report walk
(app/api/report/route.ts) now request '<civiField>.file_name' for every
file-type field alongside the primary value, and wrap the prefill into
a { id, file_name } object so downstream UI has both. Falls back to
file_name undefined when the join returns null (eg orphaned id).
The form's FilePriorIndicator already accepts the object shape, so it
now shows the filename inline. The report's FormattedValue gets a
matching case: renders the file_name string if present, falls back to
'Attachment #<id>' when only the id came through.
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
/**
|
|
* Per-field-most-recent prefill walk.
|
|
*
|
|
* Loads all `Org Engagement Submission` activities for an organization,
|
|
* sorted DESC by activity_date_time. For each field name we care about,
|
|
* walk the list and take the first non-null value found.
|
|
*
|
|
* This closes the v1 prefill gap that Webform CiviCRM cannot express in
|
|
* its admin UI: load latest values per-field across multiple activities,
|
|
* without coupling to update-mode.
|
|
*/
|
|
|
|
import { civi } from "./civicrm";
|
|
import type { FieldConfig } from "@/types/form";
|
|
|
|
interface ActivityRow {
|
|
id: number;
|
|
activity_date_time: string;
|
|
// CiviCRM returns custom fields under their machine names like
|
|
// `custom_42` or `Stage_0_Core.peer_group_participation`.
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface PrefillResult {
|
|
/** Form-side keys → most-recent non-null value. */
|
|
values: Record<string, unknown>;
|
|
/** Number of submission activities walked. */
|
|
activityCount: number;
|
|
}
|
|
|
|
/**
|
|
* Walk Org Engagement Submission activities for the org, returning per-field
|
|
* most-recent values keyed by FieldConfig.name. Only fields with a `civiField`
|
|
* are looked up; transient fields are ignored.
|
|
*/
|
|
export async function loadPrefill(
|
|
orgId: number,
|
|
fields: FieldConfig[],
|
|
activityTypeName = "Org Engagement Submission",
|
|
): Promise<PrefillResult> {
|
|
const civiSelected = fields.filter((f) => f.civiField);
|
|
|
|
// CiviCRM File custom fields return a file id by default. To surface a
|
|
// human-readable filename in the prefill (so the prior-attachment
|
|
// indicator can show it), also request the joined `.file_name` for any
|
|
// file-type field.
|
|
const fileFieldRefs = new Set(
|
|
civiSelected.filter((f) => f.type === "file").map((f) => f.civiField!),
|
|
);
|
|
|
|
const selectSet = new Set<string>(["id", "activity_date_time"]);
|
|
for (const f of civiSelected) selectSet.add(f.civiField!);
|
|
for (const ref of fileFieldRefs) selectSet.add(`${ref}.file_name`);
|
|
const select = Array.from(selectSet);
|
|
|
|
const res = await civi<ActivityRow>("Activity", "get", {
|
|
select,
|
|
where: [
|
|
["activity_type_id:name", "=", activityTypeName],
|
|
["target_contact_id", "=", orgId],
|
|
],
|
|
orderBy: { activity_date_time: "DESC" },
|
|
limit: 200,
|
|
});
|
|
const rows = res.values ?? [];
|
|
|
|
const out: Record<string, unknown> = {};
|
|
for (const f of civiSelected) {
|
|
for (const row of rows) {
|
|
const v = row[f.civiField!];
|
|
if (v === null || v === undefined || v === "") continue;
|
|
if (f.type === "file") {
|
|
const fname = row[`${f.civiField!}.file_name`];
|
|
out[f.name] = {
|
|
id: v,
|
|
file_name: typeof fname === "string" ? fname : undefined,
|
|
};
|
|
} else {
|
|
out[f.name] = v;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return { values: out, activityCount: rows.length };
|
|
}
|