diff --git a/js/command-palette.js b/js/command-palette.js new file mode 100644 index 0000000..a270192 --- /dev/null +++ b/js/command-palette.js @@ -0,0 +1,142 @@ +/** + * Command Palette integration for User Switching. + */ + +( function () { + const commandsStore = wp.commands.store; + const coreStore = wp.coreData.store; + const dispatch = wp.data.dispatch; + const useSelect = wp.data.useSelect; + const useState = wp.element.useState; + const useEffect = wp.element.useEffect; + const useMemo = wp.element.useMemo; + const useDebounce = wp.compose.useDebounce; + const __ = wp.i18n.__; + const sprintf = wp.i18n.sprintf; + const el = wp.element.createElement; + const Dashicon = wp.components.Dashicon; + + function useDebouncedValue( value ) { + const [ debouncedValue, setDebouncedValue ] = useState( '' ); + const debounced = useDebounce( setDebouncedValue, 250 ); + useEffect( function () { + debounced( value ); + return function () { + debounced.cancel(); + }; + }, [ debounced, value ] ); + return debouncedValue; + } + + const settings = window.userSwitchingCommands; + + if ( ! settings ) { + return; + } + + const switchIcon = el( Dashicon, { icon: 'admin-users' } ); + + // Static command: Switch Back. + if ( settings.switchBackUrl ) { + dispatch( commandsStore ).registerCommand( { + name: 'user-switching/switch-back', + label: settings.switchBackLabel, + searchLabel: `Switch back ${ settings.switchBackLabel }`, + icon: settings.switchBackAvatar ? el( 'img', { + src: settings.switchBackAvatar, + alt: '', + } ) : switchIcon, + callback: function ( args ) { + document.location.href = settings.switchBackUrl; + args.close(); + }, + } ); + } + + // Static command: Switch Off. + if ( settings.switchOffUrl ) { + dispatch( commandsStore ).registerCommand( { + name: 'user-switching/switch-off', + label: settings.switchOffLabel, + // This facilitates searching either in English or the localized language + searchLabel: `Switch off ${ settings.switchOffLabel }`, + icon: switchIcon, + callback: function ( args ) { + document.location.href = settings.switchOffUrl; + args.close(); + }, + } ); + } + + // Dynamic command loader for searching users to switch to. + if ( settings.canSwitchUsers ) { + dispatch( commandsStore ).registerCommandLoader( { + name: 'user-switching/switch-to-user', + hook: function useSwitchToUserLoader( options ) { + // This facilitates searching either in English or the localized language + // and trims the search query to just the name. + const trimmedSearch = options.search + .replace( new RegExp( settings.switchToLabel, 'i' ), '' ) + .replace( /Switch to/i, '' ) + .trim(); + const search = useDebouncedValue( trimmedSearch ); + + const { users, isLoading } = useSelect( function ( select ) { + if ( ! search ) { + return { users: [], isLoading: false }; + } + + const query = { + search: search, + per_page: 10, + context: 'view', + }; + const core = select( coreStore ); + + return { + users: core.getEntityRecords( 'root', 'user', query ) || [], + isLoading: ! core.hasFinishedResolution( 'getEntityRecords', [ 'root', 'user', query ] ), + }; + }, [ search ] ); + + const commands = useMemo( function () { + return users + .filter( function ( user ) { + return user.user_switching_url; + } ) + .map( function ( user ) { + const avatarUrl = user.avatar_urls && ( user.avatar_urls[ '48' ] || user.avatar_urls[ '24' ] ); + + return { + name: `user-switching/switch-to-${ user.id }`, + label: sprintf( + /* translators: %s: User's display name. */ + __( 'Switch to %s', 'user-switching' ), + user.name, + ), + // This facilitates searching either in English or the localized language + searchLabel: sprintf( + /* translators: %s: User's display name. */ + __( 'Switch to %s', 'user-switching' ), + `${ user.name } ${ user.slug } Switch to ${ user.name } ${ user.slug }`, + ), + icon: avatarUrl ? el( 'img', { + src: avatarUrl, + alt: '', + } ) : switchIcon, + callback: function ( args ) { + document.location.href = user.user_switching_url; + args.close(); + }, + }; + } ); + }, [ users ] ); + + return { + commands: commands, + isLoading: isLoading, + }; + }, + } ); + } +} )(); diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 9903923..b6b722c 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -1,6 +1,7 @@ + diff --git a/readme.txt b/readme.txt index 86daebe..0a0943b 100644 --- a/readme.txt +++ b/readme.txt @@ -258,7 +258,7 @@ This feature is useful if you have multiple users on your site who may be switch ### Can I switch users directly from the admin toolbar? -Yes, there's a third party add-on plugin for this: [Admin Bar User Switching](https://wordpress.org/plugins/admin-bar-user-switching/). +Yes, in the [WordPress Command Palette](https://wordpress.org/documentation/article/site-editor-command-palette/) you can search for a user and immediately switch into their account. You can switch off and switch back from there too. ### Are any plugin actions called when a user switches account? diff --git a/user-switching.php b/user-switching.php index 2d09c50..a1cbada 100644 --- a/user-switching.php +++ b/user-switching.php @@ -36,6 +36,8 @@ exit; } +define( 'USER_SWITCHING_VERSION', '1.11.2' ); + /** * Main singleton class for the User Switching plugin. */ @@ -80,6 +82,10 @@ public function init_hooks(): void { add_action( 'admin_bar_menu', [ $this, 'action_admin_bar_menu' ], 11 ); add_action( 'shutdown', [ $this, 'action_shutdown_for_wp_die' ], 1, 0 ); + // Command Palette integration: + add_action( 'rest_api_init', [ $this, 'action_rest_api_init' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'action_enqueue_command_palette_script' ] ); + // BuddyPress integration: add_action( 'bp_member_header_actions', [ $this, 'action_bp_button' ], 11 ); add_action( 'bp_directory_members_actions', [ $this, 'action_bp_button' ], 11 ); @@ -1144,6 +1150,96 @@ public static function secure_auth_cookie(): bool { return ( is_ssl() && ( 'https' === wp_parse_url( wp_login_url(), PHP_URL_SCHEME ) ) ); } + /** + * Registers a REST API field on the users endpoint containing the "Switch to" URL. + */ + public function action_rest_api_init(): void { + register_rest_field( 'user', 'user_switching_url', [ + 'get_callback' => function ( array $user ): ?string { + if ( ! current_user_can( 'switch_to_user', $user['id'] ) ) { + return null; + } + + $target = get_userdata( $user['id'] ); + + if ( ! $target ) { + return null; + } + + return wp_specialchars_decode( self::switch_to_url( $target ) ); + }, + 'schema' => [ + 'type' => [ + 'string', + 'null', + ], + 'description' => __( 'The URL to switch to this user.', 'user-switching' ), + 'readonly' => true, + ], + ] ); + } + + /** + * Enqueues the command palette integration script. + */ + public function action_enqueue_command_palette_script(): void { + if ( ! wp_script_is( 'wp-commands', 'enqueued' ) ) { + return; + } + + $old_user = self::get_old_user(); + $can_switch_off = current_user_can( 'switch_off' ); + + if ( ! $can_switch_off && ! $old_user ) { + return; + } + + $settings = [ + 'canSwitchUsers' => $can_switch_off, + 'switchToLabel' => str_replace( ' ', ' ', __( 'Switch To', 'user-switching' ) ), + ]; + + if ( $old_user instanceof WP_User ) { + $switch_back_url = add_query_arg( [ + 'redirect_to' => rawurlencode( self::current_url() ), + ], self::switch_back_url( $old_user ) ); + + $settings['switchBackUrl'] = wp_specialchars_decode( $switch_back_url ); + $settings['switchBackLabel'] = self::switch_back_message( $old_user ); + $settings['switchBackAvatar'] = get_avatar_url( $old_user->ID, [ 'size' => 48 ] ); + } + + if ( $can_switch_off ) { + $switch_off_url = self::switch_off_url(); + $redirect_to = is_admin() ? self::get_admin_redirect_to() : [ + 'redirect_to' => rawurlencode( self::current_url() ), + ]; + + if ( is_array( $redirect_to ) ) { + $switch_off_url = add_query_arg( $redirect_to, $switch_off_url ); + } + + $settings['switchOffUrl'] = wp_specialchars_decode( $switch_off_url ); + $settings['switchOffLabel'] = __( 'Switch Off', 'user-switching' ); + } + + wp_enqueue_script( + 'user-switching', + plugins_url( 'js/command-palette.js', __FILE__ ), + [ 'wp-commands', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n' ], + USER_SWITCHING_VERSION, + true + ); + + wp_set_script_translations( 'user-switching', 'user-switching' ); + + wp_localize_script( + 'user-switching', + 'userSwitchingCommands', + $settings + ); + } + /** * Adds a 'Switch back to {user}' link to the WooCommerce login screen. */