import { useCallback, useEffect, useState } from "react"; import { useQuery, useMutation, useQueryClient, } from "@tanstack/react-query"; import { Button, SectionMessage, Tabs, useToast, ToastType, } from "@pantheon-systems/pds-toolkit-react"; import { apiClient } from "../api/client"; import { getErrorMessage } from "../lib/errors"; interface AcfMapping { post_type: string; acf_field: string; cpub_field: string; } interface AcfField { key: string; label: string; name: string; type: string; group: string; } interface AcfMappingsResponse { acf_active: boolean; mappings: AcfMapping[]; user_match_by: "login" | "email"; errors: string[]; post_types_with_fields: PostTypeOption[]; } interface FieldRowProps { field: AcfField; cpubValue: string; onChange: (_acfFieldName: string, _cpubField: string) => void; } function FieldRow({ field, cpubValue, onChange }: FieldRowProps) { return (
{field.label || field.name}
{field.key}
{ onChange(field.name, e.target.value); }} placeholder={`e.g. ${field.label || field.name}`} className="w-full border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500" /> ); } interface FieldTableProps { postType: string; cpubValues: Record; onChangeField: (_acfName: string, _cpubField: string) => void; onFieldsLoaded: (_postType: string, _fields: AcfField[]) => void; } function FieldTable({ postType, cpubValues, onChangeField, onFieldsLoaded }: FieldTableProps) { const { data: fields = [], isLoading } = useQuery({ queryKey: ["acf-fields", postType], queryFn: async () => { const res = await apiClient.get<{ fields: AcfField[] }>( `/acf-fields?post_type=${encodeURIComponent(postType)}` ); return res.data.fields; }, enabled: Boolean(postType), }); // Notify parent of loaded fields for user-type detection useEffect(() => { if (fields.length > 0) { onFieldsLoaded(postType, fields); } }, [fields, onFieldsLoaded, postType]); if (isLoading) { return

Loading ACF fields…

; } if (fields.length === 0) { return (

No ACF fields found for post type {postType}. Create an ACF field group assigned to this post type first.

); } return ( {fields.map((field) => ( ))}
Advanced Custom Fields Content Publisher Metadata Field
); } export default function AcfMappings() { const [addToast] = useToast(); const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery({ queryKey: ["acf-mappings"], queryFn: async () => { const res = await apiClient.get("/acf-mappings"); return res.data; }, }); const saveMutation = useMutation({ mutationFn: async (payload: { mappings: AcfMapping[]; user_match_by: "login" | "email" }) => { const res = await apiClient.put("/acf-mappings", payload); return res.data; }, onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["acf-mappings"] }); }, }); const acfActive = window.CPUB_BOOTSTRAP.acf_active; const postTypesWithFields = data?.post_types_with_fields ?? []; // Local state: post_type → acf_field_name → cpub_field const [localMappings, setLocalMappings] = useState>>({}); const [userMatchBy, setUserMatchBy] = useState<"login" | "email">( data?.user_match_by ?? "login" ); // Track loaded ACF fields per post type for user-type detection const [loadedFieldsByType, setLoadedFieldsByType] = useState>({}); // Sync remote data into local state once loaded const [syncedFromServer, setSyncedFromServer] = useState(false); useEffect(() => { if (data && !syncedFromServer) { setSyncedFromServer(true); setUserMatchBy(data.user_match_by); const grouped: Record> = {}; data.mappings.forEach((mapping) => { grouped[mapping.post_type] ??= {}; grouped[mapping.post_type][mapping.acf_field] = mapping.cpub_field; }); setLocalMappings((prev) => { const merged: Record> = { ...prev }; for (const [pt, fields] of Object.entries(grouped)) { const existing = merged[pt] ?? {}; merged[pt] = { ...existing, ...fields }; } return merged; }); } }, [data, syncedFromServer]); const handleFieldChange = useCallback((postType: string, acfName: string, cpubField: string) => { setLocalMappings((prev) => { const existing = prev[postType] ?? {}; return { ...prev, [postType]: { ...existing, [acfName]: cpubField } }; }); }, []); const handleFieldsLoaded = useCallback((postType: string, fields: AcfField[]) => { setLoadedFieldsByType((prev) => ({ ...prev, [postType]: fields })); }, []); const handleSave = () => { // Build flat mappings array across all post types, omitting empty cpub fields const mappings: AcfMapping[] = []; for (const [postType, fields] of Object.entries(localMappings)) { for (const [acfField, cpubField] of Object.entries(fields)) { if (cpubField.trim()) { mappings.push({ post_type: postType, acf_field: acfField, cpub_field: cpubField.trim(), }); } } } saveMutation.mutate( { mappings, user_match_by: userMatchBy }, { onSuccess: () => { addToast(ToastType.Success, "Metadata mapping saved."); }, onError: (err) => { addToast( ToastType.Critical, getErrorMessage(err, "Failed to save mappings.") ?? "Failed to save mappings." ); }, } ); }; if (isLoading) { return

Loading…

; } if (error) { return ( ); } // ACF not active: show install prompt if (!acfActive) { return (

Advanced Custom Fields

Metadata fields defined in the Pantheon Content Publisher collection can be synced to Advanced Custom Fields.

); } const hasUserFields = Object.values(loadedFieldsByType).some( (fields) => fields.some((f) => f.type === "user") ); const renderFieldTable = (pt: PostTypeOption) => ( { handleFieldChange(pt.name, acfName, cpubField); }} onFieldsLoaded={handleFieldsLoaded} /> ); return (

Advanced Custom Fields

Metadata fields defined in the Pantheon Content Publisher collection can be synced to Advanced Custom Fields.

{/* Recent sync errors from server */} {data?.errors && data.errors.length > 0 && ( `• ${e}`).join("\n") } /> )} {/* Field mapping tables per post type */} {postTypesWithFields.length === 0 ? (

No post types have ACF field groups configured. Create an ACF field group and assign it to a post type first.

) : ( ({ tabLabel: pt.label, panelContent: renderFieldTable(pt), }))} /> )} {/* User field matching — only when user-type ACF fields exist */} {hasUserFields && (

Map ACF user fields by:

)}

Enter the metadata field name exactly as defined in your PCC collection. Leave blank to skip that field. Field names are case-sensitive. Mappings are applied on every publish; renaming a field requires updating the mapping manually.

); }