diff --git a/README.md b/README.md index 525c35e..0ff5e5d 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,12 @@ this adapter builds from the raw files, so its reader needs no assembler of its own. The admin screen's export, its ZIP and GitHub delivery never carry it: the site's repository holds the store, not a raw copy of every record. +ACF Options Pages reach RawIR as `acf_options` (from `bridge/acf-options.json`): +each page's slug, title, storage `post_id` and fields — the same +secret-filtered, default-language values its own `acf-options-*` singleton +holds, keyed by field name like a post's `acf`, with each field's ACF type and +label. + ## REST export API (for Migrate) An administrator's application password is enough to start, advance and read an diff --git a/includes/class-contentrain-bridge-jobs.php b/includes/class-contentrain-bridge-jobs.php index 1867a18..5e006ab 100644 --- a/includes/class-contentrain-bridge-jobs.php +++ b/includes/class-contentrain-bridge-jobs.php @@ -61,8 +61,12 @@ public static function create( $input, $remote = false ) { foreach ( $menus as $menu ) { Coverage::tally( $job, array( 'menu_items' ), 'exported', count( $menu['items'] ) ); } + $acf_options = array(); foreach ( Source::options_pages() as $options_page ) { - Models::options_page( $job, $options_page ); + $acf_options[] = Models::options_page( $job, $options_page ); + } + if ( $acf_options ) { + Models::file( $job, 'bridge/acf-options.json', Policy::json( $acf_options ) ); } self::warning( $job, array( 'source' => 'rendered-states', 'reason' => 'Source scan does not execute dynamic WordPress/plugin states. Rendered coverage requires the Migrate capture adapter.' ) ); self::save( $job ); diff --git a/includes/class-contentrain-bridge-models.php b/includes/class-contentrain-bridge-models.php index cf4b30f..7edb6b4 100644 --- a/includes/class-contentrain-bridge-models.php +++ b/includes/class-contentrain-bridge-models.php @@ -473,6 +473,9 @@ private static function menu_url( $url ) { * title field regardless of what those configured fields are named; ACF * fields are prefixed the same way a post's own are, so a field legitimately * named `page_title` cannot collide with it. + * + * Returns the page as RawIR carries it (`RawAcfOptionsPage`): the same + * secret-filtered values, keyed by field name, for the REST-path conversion. */ public static function options_page( &$job, $page ) { $post_id = $page['post_id'] ?: 'options'; @@ -522,6 +525,7 @@ public static function options_page( &$job, $page ) { } elseif ( defined( 'BEA_ACF_OPTIONS_FOR_POLYLANG_VERSION' ) ) { Jobs::warning( $job, array( 'source' => 'acf-options/' . $slug, 'reason' => 'acf-options-translation-plugin-detected: ACF Options for Polylang can translate this Options Page; only its default-language values are exported' ) ); } + return array( 'slug' => $slug, 'title' => $page['page_title'] ?: $page['menu_slug'] ?: $slug, 'post_id' => (string) $post_id, 'fields' => (object) $raw ); } /** Check short writes so disk exhaustion cannot produce a successful truncated table. */ diff --git a/includes/class-contentrain-bridge-rawir.php b/includes/class-contentrain-bridge-rawir.php index 4204d85..37b7af1 100644 --- a/includes/class-contentrain-bridge-rawir.php +++ b/includes/class-contentrain-bridge-rawir.php @@ -52,6 +52,11 @@ public static function write( &$job ) { $field( 'language_pairs' ); self::rows( $stream, $job, 'bridge/language-pairs.json', false ); $field( 'options', $file( 'bridge/options.json' ) ?? '{}' ); + // RawAcfOptionsPage[]: each ACF Options Page's site-wide fields, default language only. + $acf_options = $file( 'bridge/acf-options.json' ); + if ( null !== $acf_options ) { + $field( 'acf_options', $acf_options ); + } // RawIR.redirects is RawRedirect[]: what the site serves; redirects_excluded what it holds but does not // serve as a plain redirect, each with its reason. Sources and counts stay in bridge/redirects.json. $redirects = json_decode( $file( 'bridge/redirects.json' ) ?? '{}' ); diff --git a/includes/class-contentrain-bridge-source.php b/includes/class-contentrain-bridge-source.php index e4897fe..27325cf 100644 --- a/includes/class-contentrain-bridge-source.php +++ b/includes/class-contentrain-bridge-source.php @@ -230,7 +230,7 @@ public static function acf_fields_for_options_page( $post_id, $slug, $source_pre $excluded[] = array( 'source' => $source_prefix . '/' . $name, 'reason' => 'sensitive-field' ); continue; } - $raw[ $name ] = array( 'value' => self::acf_value( $loaded, $loaded['value'], $excluded, $source_prefix . '/' . $name ), 'field_key' => $loaded['key'] ); + $raw[ $name ] = array( 'value' => self::acf_value( $loaded, $loaded['value'], $excluded, $source_prefix . '/' . $name ), 'field_key' => $loaded['key'], 'type' => $loaded['type'], 'label' => $loaded['label'] ); $schema[ $name ] = self::schema( $loaded ); } } diff --git a/tests/integration.php b/tests/integration.php index 40197c8..bed2cde 100644 --- a/tests/integration.php +++ b/tests/integration.php @@ -930,6 +930,16 @@ function rejects( $callback, $message ) { $locale_options_id = 'acf-options-bridge-locale-options'; $locale_entry = json_decode( Files::read( $dir, Models::content_path( $job, $locale_options_id, $job['default_locale'] ) ), true ); check( 'Default language value' === $locale_entry['acf_locale_note'], 'an Options Page with a detected per-language copy still exports only its default-language value' ); + + // RawIR carries every Options Page too (`acf_options`), for the REST-path conversion: the same values, keyed by + // field name like a post's `acf`, each page with its own fields only. + $raw_options = array_column( json_decode( Files::read( $dir, 'bridge/acf-options.json' ), true ), null, 'slug' ); + check( array( 'bridge-footer', 'bridge-header', 'bridge-locale-options', 'bridge-options' ) === array_values( array_intersect( array( 'bridge-footer', 'bridge-header', 'bridge-locale-options', 'bridge-options' ), array_keys( $raw_options ) ) ), 'RawIR acf_options carries every Options Page' ); + $raw_site_options = $raw_options['bridge-options'] ?? array(); + check( 'Bridge Options' === ( $raw_site_options['title'] ?? null ) && 'options' === ( $raw_site_options['post_id'] ?? null ), 'an Options Page in RawIR keeps its title and storage post_id' ); + check( 'Owns its content' === ( $raw_site_options['fields']['site_wide_tagline']['value'] ?? null ) && 'field_bridge_opt_tagline' === ( $raw_site_options['fields']['site_wide_tagline']['field_key'] ?? null ) && 'text' === ( $raw_site_options['fields']['site_wide_tagline']['type'] ?? null ), 'an Options Page field in RawIR has its value, field key and type' ); + check( 2 === count( $raw_site_options['fields']['social_links']['value'] ?? array() ), 'a repeater on an Options Page keeps its rows in RawIR' ); + check( array( 'header_text' ) === array_keys( $raw_options['bridge-header']['fields'] ?? array() ) && 'bridge_locale_options' === ( $raw_options['bridge-locale-options']['post_id'] ?? null ), 'each Options Page in RawIR carries only its own fields, and a custom post_id as set' ); } if ( $has_polylang ) { diff --git a/tools/prepare-migrate.mjs b/tools/prepare-migrate.mjs index 67e55b2..c53896a 100644 --- a/tools/prepare-migrate.mjs +++ b/tools/prepare-migrate.mjs @@ -50,6 +50,9 @@ export function prepareMigrate(source, destination) { if (seo) raw.seo = { ...seo, entries: json('bridge/seo-entries.json', {}) } const routing = json('bridge/routing.json', null) if (routing) raw.routing = routing + // RawAcfOptionsPage[]: each ACF Options Page's site-wide fields, default language only. + const acfOptions = json('bridge/acf-options.json', null) + if (acfOptions) raw.acf_options = acfOptions // B-08: every interface-text candidate with its one outcome; not yet a RawIR field (proposed). // B-07: services to reconnect; not yet a RawIR field (proposed). const integrations = json('bridge/integrations.json', null)