diff --git a/docs/screenshots/legacy-pages-transition/desktop.jpg b/docs/screenshots/legacy-pages-transition/desktop.jpg new file mode 100644 index 0000000..98f62dd Binary files /dev/null and b/docs/screenshots/legacy-pages-transition/desktop.jpg differ diff --git a/docs/screenshots/legacy-pages-transition/mobile.jpg b/docs/screenshots/legacy-pages-transition/mobile.jpg new file mode 100644 index 0000000..97ea51c Binary files /dev/null and b/docs/screenshots/legacy-pages-transition/mobile.jpg differ diff --git a/src/assets/js/simply-rets-admin.js b/src/assets/js/simply-rets-admin.js index 496e986..f9cf1bc 100644 --- a/src/assets/js/simply-rets-admin.js +++ b/src/assets/js/simply-rets-admin.js @@ -97,9 +97,20 @@ jQuery(function($) { } try { active = window.sessionStorage.getItem(storageKey) || active; } catch (error) { /* Use the account section. */ } + function linkedSection() { + var prefix = '#sr-section-'; + if (window.location.hash.indexOf(prefix) !== 0) return null; + var section = window.location.hash.slice(prefix.length); + return panels.filter(function() { return $(this).attr('data-sr-panel') === section; }).length ? section : null; + } + active = linkedSection() || active; root.addClass('sr-settings-enhanced'); root.find('.sr-settings-toolbar, .sr-settings-nav, .sr-settings-savebar-info, .sr-reveal-credentials').prop('hidden', false); showSection(active, false); + $(window).on('hashchange', function() { + var section = linkedSection(); + if (section) showSection(section, true); + }); nav.on('click', 'a', function(event) { event.preventDefault(); diff --git a/src/simply-rets-admin.php b/src/simply-rets-admin.php index 0195b01..950b3e4 100644 --- a/src/simply-rets-admin.php +++ b/src/simply-rets-admin.php @@ -14,9 +14,63 @@ add_action("wp_loaded", array("SrAdminSettings", "createDemoPage")); add_action("admin_notices", array("SrAdminSettings", "adminMessages")); +add_action('admin_notices', array('SrAdminSettings', 'legacyPagesNotice')); +add_action('admin_post_sr_dismiss_legacy_pages_notice', array('SrAdminSettings', 'dismissLegacyPagesNotice')); class SrAdminSettings { + public static function canManageLegacyPages() { + $post_type = get_post_type_object('sr-listings'); + return $post_type && current_user_can('manage_options') && current_user_can($post_type->cap->edit_posts); + } + + public static function hasLegacyPages() { + global $wpdb; + // Look for persisted posts only, including drafts, private pages and trash. + // Automatic drafts are editor placeholders; dynamic routes are not stored. + return $wpdb->get_var($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status NOT IN (%s, %s) LIMIT 1", + 'sr-listings', 'auto-draft', 'inherit' + )) !== null; + } + + public static function legacyPagesSettingsUrl() { + return admin_url('options-general.php?page=simplyrets-admin.php') . '#sr-section-legacy-pages'; + } + + public static function legacyPagesNotice() { + if ((defined('DOING_AJAX') && DOING_AJAX) || + (defined('IFRAME_REQUEST') && IFRAME_REQUEST) || + (function_exists('is_network_admin') && is_network_admin()) || + !self::canManageLegacyPages() || !self::hasLegacyPages() || + get_user_option('sr_legacy_pages_transition_dismissed')) { + return; + } + ?> +
+

SimplyRETS Pages are now legacy pages. Your existing pages continue to work. For new pages, use SimplyRETS shortcodes on ordinary WordPress pages.

+

Find your existing pages in Settings → SimplyRETS → Legacy Pages.

+
+ + +

+
+
+ array('Analytics', 'ListHub tracking', 'chart-bar'), 'messages' => array('Custom messages', 'Disclaimers & empty results', 'editor-alignleft') ); + $show_legacy_pages = self::canManageLegacyPages() && self::hasLegacyPages(); + if ($show_legacy_pages) { + $sections['legacy-pages'] = array('Legacy Pages', 'Manage existing pages', 'admin-page'); + } // If meta data refresh action was posted, verify nonce and perform action $update_meta_nonce_field = 'sr_update_meta_data_nonce_field'; @@ -859,6 +917,15 @@ public static function sr_admin_page() { + +
+

Legacy Pages

+

The custom SimplyRETS page editor is deprecated. Your existing pages are still supported and continue to work, including their filters, templates, and URLs.

+

For new listing pages, add SimplyRETS shortcodes to ordinary WordPress pages. For example, use [sr_map_search search_form="true" list_view="true"] for a map and listing search.

+

Explore shortcode examples (opens in a new tab)

+

Manage existing pages

+
+
@@ -869,4 +936,4 @@ public static function sr_admin_page() {
\ No newline at end of file +} ?> diff --git a/src/simply-rets-setup.php b/src/simply-rets-setup.php index 2ca7160..f0a899d 100644 --- a/src/simply-rets-setup.php +++ b/src/simply-rets-setup.php @@ -94,6 +94,8 @@ public static function srRegisterPostType() { ); $args = array( 'public' => true, + // Keep saved pages and their editor/routes available through settings. + 'show_in_menu' => false, 'has_archive' => false, 'labels' => $labels, 'description' => 'SimplyRETS property listings pages', diff --git a/src/tests/test-legacy-pages.php b/src/tests/test-legacy-pages.php new file mode 100644 index 0000000..9b0d202 --- /dev/null +++ b/src/tests/test-legacy-pages.php @@ -0,0 +1,168 @@ +factory->user->create(array('role' => $role)); + wp_set_current_user($user_id); + return $user_id; + } + + private function noticeMarkup() { + ob_start(); + SrAdminSettings::legacyPagesNotice(); + return ob_get_clean(); + } + + private function dismissalIsRejected() { + $handler = function() { + return function() { throw new RuntimeException('Dismissal rejected'); }; + }; + add_filter('wp_die_handler', $handler); + try { + SrAdminSettings::dismissLegacyPagesNotice(); + } catch (RuntimeException $error) { + return $error->getMessage() === 'Dismissal rejected'; + } finally { + remove_filter('wp_die_handler', $handler); + } + return false; + } + + public function testUnauthorizedUsersCannotDismissNotice() { + $user_id = $this->signIn('editor'); + $original_request = $_REQUEST; + try { + $_REQUEST['_wpnonce'] = wp_create_nonce('sr_dismiss_legacy_pages_notice'); + $this->assertTrue($this->dismissalIsRejected()); + $this->assertFalse(get_user_option('sr_legacy_pages_transition_dismissed', $user_id)); + } finally { + $_REQUEST = $original_request; + } + } + + public function testMissingAndInvalidNoncesCannotDismissNotice() { + $user_id = $this->signIn('administrator'); + $original_request = $_REQUEST; + try { + unset($_REQUEST['_wpnonce']); + $this->assertTrue($this->dismissalIsRejected()); + $_REQUEST['_wpnonce'] = 'invalid'; + $this->assertTrue($this->dismissalIsRejected()); + $this->assertFalse(get_user_option('sr_legacy_pages_transition_dismissed', $user_id)); + } finally { + $_REQUEST = $original_request; + } + } + + public function testOrdinaryPagesAndAutomaticDraftsDoNotTriggerTransition() { + $this->signIn('administrator'); + $this->factory->post->create(array('post_type' => 'page')); + $this->factory->post->create(array('post_type' => 'sr-listings', 'post_status' => 'auto-draft')); + $this->assertFalse(SrAdminSettings::hasLegacyPages()); + $this->assertSame('', $this->noticeMarkup()); + ob_start(); + SrAdminSettings::sr_admin_page(); + $this->assertFalse(strpos(ob_get_clean(), 'data-sr-panel="legacy-pages"')); + } + + public function testSavedStatusesIncludingPrivateDraftAndTrashTriggerTransition() { + foreach (array('publish', 'draft', 'private', 'pending', 'future', 'trash') as $status) { + $post_id = $this->factory->post->create(array( + 'post_type' => 'sr-listings', + 'post_status' => $status, + 'post_date' => $status === 'future' ? '2099-01-01 00:00:00' : '2020-01-01 00:00:00' + )); + $this->assertTrue(SrAdminSettings::hasLegacyPages(), $status); + wp_delete_post($post_id, true); + $this->assertFalse(SrAdminSettings::hasLegacyPages(), $status . ' removed'); + } + } + + public function testSynthesizedListingRoutesDoNotTriggerTransition() { + global $wp_query; + $original_query = $wp_query; + try { + foreach (array('sr-single', 'sr-search', 'sr-openhouses') as $route) { + $wp_query = new WP_Query(); + $wp_query->query = array('sr-listings' => $route); + $posts = SimplyRetsCustomPostPages::srCreateDynamicPost(array()); + $this->assertCount(1, $posts); + $this->assertSame('sr-listings', $posts[0]->post_type); + $this->assertFalse(SrAdminSettings::hasLegacyPages()); + } + } finally { + $wp_query = $original_query; + } + } + + public function testNoticeAndTabRequireSettingsAndPageEditingPermissions() { + $this->factory->post->create(array('post_type' => 'sr-listings')); + foreach (array('subscriber', 'editor') as $role) { + $this->signIn($role); + $this->assertFalse(SrAdminSettings::canManageLegacyPages()); + $this->assertSame('', $this->noticeMarkup()); + } + $this->signIn('subscriber'); + $user = wp_get_current_user(); + $user->add_cap('manage_options'); + $this->assertFalse(SrAdminSettings::canManageLegacyPages()); + ob_start(); + SrAdminSettings::sr_admin_page(); + $this->assertFalse(strpos(ob_get_clean(), 'data-sr-panel="legacy-pages"')); + $user->add_cap('edit_pages'); + $this->assertTrue(SrAdminSettings::canManageLegacyPages()); + } + + public function testNoticeDismissalIsPerUserAndSurvivesNewPageVisits() { + $this->factory->post->create(array('post_type' => 'sr-listings')); + $user_id = $this->signIn('administrator'); + $markup = $this->noticeMarkup(); + $this->assertNotFalse(strpos($markup, '#sr-section-legacy-pages')); + $this->assertNotFalse(strpos($markup, 'name="_wpnonce"')); + $this->assertNotFalse(strpos($markup, 'sr_dismiss_legacy_pages_notice')); + update_user_option($user_id, 'sr_legacy_pages_transition_dismissed', 1, false); + global $wpdb; + $this->assertSame('1', (string) get_user_meta($user_id, $wpdb->prefix . 'sr_legacy_pages_transition_dismissed', true)); + $this->assertSame('', get_user_meta($user_id, 'sr_legacy_pages_transition_dismissed', true)); + $this->assertSame('', $this->noticeMarkup()); + $this->signIn('administrator'); + $this->assertNotSame('', $this->noticeMarkup()); + wp_set_current_user($user_id); + $this->assertSame('', $this->noticeMarkup()); + } + + public function testLegacyPanelKeepsManagementWithinSettings() { + $this->signIn('administrator'); + $this->factory->post->create(array('post_type' => 'sr-listings', 'post_status' => 'trash')); + ob_start(); + SrAdminSettings::sr_admin_page(); + $markup = ob_get_clean(); + $this->assertNotFalse(strpos($markup, 'data-sr-section="legacy-pages"')); + $this->assertNotFalse(strpos($markup, 'id="sr-section-legacy-pages"')); + $this->assertFalse(strpos($markup, 'data-sr-panel="legacy-pages" hidden')); + $this->assertNotFalse(strpos($markup, esc_url(admin_url('edit.php?post_type=sr-listings')))); + $this->assertTrue(strpos($markup, 'data-sr-section="messages"') < strpos($markup, 'data-sr-section="legacy-pages"')); + $this->assertTrue(strpos($markup, 'id="sr-section-legacy-pages"') < strpos($markup, 'Manage existing pages')); + } + + public function testPostTypeKeepsEditorAndRoutingWithoutSidebarEntry() { + $post_type = get_post_type_object('sr-listings'); + $this->assertFalse($post_type->show_in_menu); + $this->assertTrue($post_type->show_ui); + $this->assertTrue($post_type->public); + $this->assertSame('sr-listings', $post_type->query_var); + $this->assertSame('edit_pages', $post_type->cap->edit_posts); + $this->assertTrue(post_type_supports('sr-listings', 'editor')); + $post_id = $this->factory->post->create(array('post_type' => 'sr-listings', 'post_status' => 'publish')); + $this->assertNotEmpty(get_permalink($post_id)); + $this->assertNotFalse(has_filter('the_posts', array('SimplyRetsCustomPostPages', 'srCreateDynamicPost'))); + $this->assertNotFalse(has_action('add_meta_boxes', array('SimplyRetsCustomPostPages', 'postFilterMetaBox'))); + $this->assertNotFalse(has_filter('single_template', array('SimplyRetsCustomPostPages', 'srLoadPostTemplate'))); + } +}