diff --git a/acf-hide-layout.php b/acf-hide-layout.php
index 67c2a29..19a4118 100644
--- a/acf-hide-layout.php
+++ b/acf-hide-layout.php
@@ -228,6 +228,13 @@ private function init_hooks() {
public function init() {
// Set up localisation.
$this->load_plugin_textdomain();
+ // Include and run migration script if in admin
+ if ($this->is_request('admin')) {
+ $migration_file = dirname($this->file) . '/migrate-to-acf-native.php';
+ if (file_exists($migration_file)) {
+ require_once $migration_file;
+ }
+ }
}
/**
diff --git a/migrate-to-acf-native.php b/migrate-to-acf-native.php
new file mode 100644
index 0000000..698e234
--- /dev/null
+++ b/migrate-to-acf-native.php
@@ -0,0 +1,262 @@
+';
+ echo '
Migrate ACF Hidden Layouts
';
+ echo 'This tool migrates hidden layouts created by the plugin flyntwp/acf-hide-layout to ACF\'s native "Deactivate layout" feature.
';
+
+ echo '';
+
+ if ($isSubmitted && \check_admin_referer('flynt_migrate_acf_hide_layout', 'flynt_migrate_acf_hide_layout_nonce')) {
+ $results = migrateHiddenLayouts((bool) $dryRun);
+ renderResults($results, (bool) $dryRun);
+ }
+
+ echo '';
+}
+
+/**
+ * Perform migration of hidden layouts to ACF native layout_meta disabled list
+ *
+ * @param bool $dryRun If true, do not persist changes
+ * @return array Summary of actions and per-post logs
+ */
+function migrateHiddenLayouts($dryRun = true)
+{
+ $summary = [
+ 'dryRun' => $dryRun,
+ 'checkedPosts' => 0,
+ 'updatedPosts' => 0,
+ 'errors' => [],
+ 'logs' => [],
+ ];
+
+ // Check function availability
+ $hasAcf = \function_exists('acf_get_fields') || \function_exists('get_field_objects');
+ $hasAcfMetaApi = \function_exists('acf_update_metadata_by_field');
+
+ if (!$hasAcf || !$hasAcfMetaApi) {
+ $summary['errors'][] = 'Required ACF functions are not available. Ensure ACF Pro 6.0+ is active.';
+ return $summary;
+ }
+
+ // Retrieve all public post types including custom, exclude revisions and attachments
+ $postTypes = \get_post_types(['public' => true], 'names');
+
+ $queryArgs = [
+ 'post_type' => array_values($postTypes),
+ 'post_status' => ['publish', 'draft', 'pending', 'future', 'private'],
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ 'no_found_rows' => true,
+ 'update_post_term_cache' => false,
+ 'update_post_meta_cache' => false,
+ ];
+
+ $postIds = \get_posts($queryArgs);
+
+ foreach ($postIds as $postId) {
+ $summary['checkedPosts']++;
+
+ $postLog = [
+ 'postId' => $postId,
+ 'fieldsProcessed' => 0,
+ 'layoutsFound' => 0,
+ 'layoutsHiddenDetected' => 0,
+ 'layoutsNewlyDisabled' => 0,
+ 'fieldLogs' => [],
+ ];
+
+ // Get all ACF field objects attached to this post
+ $fieldObjects = \function_exists('get_field_objects') ? \call_user_func('get_field_objects', $postId) : null;
+ if (empty($fieldObjects)) {
+ $summary['logs'][] = $postLog;
+ continue;
+ }
+
+ foreach ($fieldObjects as $fieldName => $fieldObject) {
+ if (!isset($fieldObject['type']) || $fieldObject['type'] !== 'flexible_content') {
+ continue;
+ }
+
+ $postLog['fieldsProcessed']++;
+
+ // We need to iterate rows of this flexible content
+ $hasRows = \function_exists('have_rows') ? (bool) \call_user_func('have_rows', $fieldName, $postId) : false;
+ if (!$hasRows) {
+ continue;
+ }
+
+ // Collect disabled indices for this field
+ $disabledLayouts = [];
+ $renamedLayouts = [];
+
+ // If there is existing layout_meta, preserve it
+ $existingLayoutMeta = \get_post_meta($postId, '_' . $fieldName . '_layout_meta', true);
+ if (is_array($existingLayoutMeta)) {
+ if (!empty($existingLayoutMeta['disabled']) && is_array($existingLayoutMeta['disabled'])) {
+ $disabledLayouts = array_values(array_unique(array_map('intval', $existingLayoutMeta['disabled'])));
+ }
+ if (!empty($existingLayoutMeta['renamed']) && is_array($existingLayoutMeta['renamed'])) {
+ $renamedLayouts = $existingLayoutMeta['renamed'];
+ }
+ }
+
+ // Iterate rows and detect plugin hidden flags
+ if (\call_user_func('have_rows', $fieldName, $postId)) {
+ $rowIndex = 0;
+ while (\call_user_func('have_rows', $fieldName, $postId)) {
+ \call_user_func('the_row');
+ $postLog['layoutsFound']++;
+
+ // Determine the plugin field key suffix used for hidden flag
+ // Per plugin: name is "{$field['name']}_{$key}_{$field_key}" where $field_key is acf_hide_layout
+ // Here, $key is zero-based row index in stored meta for flexible content
+ $hideFieldName = $fieldName . '_' . $rowIndex . '_acf_hide_layout';
+
+ // Build fake field array to use acf_get_value API similar to plugin's read method
+ $hideLayoutField = [
+ 'name' => $hideFieldName,
+ 'key' => 'field_acf_hide_layout',
+ ];
+
+ $isHidden = null;
+ if (\function_exists('acf_get_value')) {
+ $isHidden = \call_user_func('acf_get_value', $postId, $hideLayoutField);
+ } else {
+ $isHidden = \get_post_meta($postId, $hideFieldName, true);
+ }
+
+ if (!empty($isHidden)) {
+ $postLog['layoutsHiddenDetected']++;
+ if (!in_array($rowIndex, $disabledLayouts, true)) {
+ $disabledLayouts[] = $rowIndex;
+ $postLog['layoutsNewlyDisabled']++;
+ }
+ }
+
+ $rowIndex++;
+ }
+ }
+
+ sort($disabledLayouts);
+
+ // If changes, write layout_meta using ACF API
+ $fieldLog = [
+ 'fieldName' => $fieldName,
+ 'disabled' => $disabledLayouts,
+ 'renamed' => $renamedLayouts,
+ ];
+
+ if (!empty($disabledLayouts)) {
+ if (!$dryRun) {
+ if (\function_exists('acf_update_metadata_by_field')) {
+ \call_user_func(
+ 'acf_update_metadata_by_field',
+ $postId,
+ [
+ 'name' => '_' . $fieldName . '_layout_meta',
+ ],
+ [
+ 'disabled' => $disabledLayouts,
+ 'renamed' => $renamedLayouts,
+ ]
+ );
+ $summary['updatedPosts']++;
+ } else {
+ // Fallback to update_post_meta if ACF helper is not available
+ \update_post_meta($postId, '_' . $fieldName . '_layout_meta', [
+ 'disabled' => $disabledLayouts,
+ 'renamed' => $renamedLayouts,
+ ]);
+ $summary['updatedPosts']++;
+ }
+ }
+ }
+
+ $postLog['fieldLogs'][] = $fieldLog;
+ }
+
+ $summary['logs'][] = $postLog;
+ }
+
+ return $summary;
+}
+
+/**
+ * Render migration results in admin UI
+ *
+ * @param array $results
+ * @param bool $dryRun
+ * @return void
+ */
+function renderResults($results, $dryRun)
+{
+ if (!empty($results['errors'])) {
+ foreach ($results['errors'] as $error) {
+ echo '' . \esc_html($error) . '
';
+ }
+ return;
+ }
+
+ $summaryText = sprintf(
+ 'Processed %d posts. %s %d post(s) were updated.',
+ (int) $results['checkedPosts'],
+ $dryRun ? 'Dry run: no changes saved.' : 'Changes saved.',
+ (int) $results['updatedPosts']
+ );
+
+ echo '' . \esc_html($summaryText) . '
';
+
+ echo 'Details
';
+ echo '';
+
+ foreach ($results['logs'] as $postLog) {
+ echo '
';
+ echo '
Post ID ' . intval($postLog['postId']) . '
';
+ echo '
Fields processed: ' . intval($postLog['fieldsProcessed']) . ' | Layouts found: ' . intval($postLog['layoutsFound']) . ' | Hidden detected: ' . intval($postLog['layoutsHiddenDetected']) . ' | Newly disabled: ' . intval($postLog['layoutsNewlyDisabled']) . '
';
+
+ if (!empty($postLog['fieldLogs'])) {
+ echo '
';
+ foreach ($postLog['fieldLogs'] as $fieldLog) {
+ $disabled = !empty($fieldLog['disabled']) ? implode(', ', array_map('intval', $fieldLog['disabled'])) : 'none';
+ echo '- ' . \esc_html($fieldLog['fieldName']) . ': disabled [' . \esc_html($disabled) . ']
';
+ }
+ echo '
';
+ }
+ echo '
';
+ }
+
+ echo '
';
+}
+