` primitive shown with
* `showModal()`: the browser gives the focus trap, the Escape handling and the
* `role="dialog"` for free, and the screen already had one for the Revoke
* confirmation.
*
* @since 1.4.0
*/
final class UserPickerModal {
/**
* The dialog element's id, shared by both entry points.
*
* @since 1.4.0
* @var string
*/
public const DIALOG_ID = 'albert-adduser-dialog';
/**
* The `admin-post.php` action the dialog submits to.
*
* @since 1.4.0
* @var string
*/
public const ACTION = 'albert_add_allowed_users';
/**
* The capability a user must have to be offered in the picker.
*
* A user who cannot edit content could authorise an assistant that then has
* nothing it may do, so offering them is noise, and on a shop it is twenty
* thousand rows of noise.
*
* @return string A capability name, or '' to offer every user.
* @since 1.4.0
*/
public static function candidate_capability(): string {
/**
* Filters the capability a user needs to appear in the allowed-users picker.
*
* Return an empty string to offer every user, or another capability to
* widen or narrow the candidate list. This only changes who is *offered*:
* the authorisation gate itself is the stored allowed-users list.
*
* @since 1.4.0
*
* @param string $capability Default `edit_posts`.
*/
return (string) apply_filters( 'albert/connections/allowed_user_capability', 'edit_posts' );
}
/**
* Register the picker's script and styles for the current screen.
*
* Both screens already load the primitives stylesheet, which is where the
* dialog shell lives; only the behaviour has to be added here.
*
* @param string $return_to Which screen the form should come back to.
*
* @return void
* @since 1.4.0
*/
public static function enqueue( string $return_to ): void {
wp_enqueue_script(
'albert-admin-utils',
ALBERT_PLUGIN_URL . 'assets/js/albert-admin-utils.js',
[],
Assets::version( 'assets/js/albert-admin-utils.js' ),
true
);
wp_enqueue_script(
'albert-connections',
ALBERT_PLUGIN_URL . 'assets/js/admin-connections.js',
[ 'albert-admin-utils' ],
Assets::version( 'assets/js/admin-connections.js' ),
true
);
wp_localize_script(
'albert-connections',
'albertConnections',
[
// Core's own users route. `search`, `roles` and `capabilities` all
// require `list_users`; these screens are manage_options, so an
// administrator is fine. If a narrower capability is ever allowed
// to manage connections, the picker degrades and needs revisiting.
'usersUrl' => rest_url( 'wp/v2/users' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'capability' => self::candidate_capability(),
'roles' => self::role_names(),
'allowed' => self::allowed_user_ids(),
'returnTo' => $return_to,
'i18n' => [
// The connected-assistants list shares this object: it is
// the same screen, the same script file, and one localised
// blob is one thing to keep translated.
'selectedNone' => __( 'Nothing selected', 'albert-ai-butler' ),
/* translators: %d: number of selected connections. */
'selectedCount' => __( '%d selected', 'albert-ai-butler' ),
/* translators: %d: number of connections matching the current filter. */
'selectAllFiltered' => __( 'Select all %d that match this filter', 'albert-ai-butler' ),
/* translators: 1: number of selected connections, 2: number of connections currently shown. */
'selectionStatus' => __( '%1$d of %2$d connections selected', 'albert-ai-butler' ),
'selectModeOn' => __( 'Selection mode on, nothing selected', 'albert-ai-butler' ),
'selectModeOff' => __( 'Selection mode off', 'albert-ai-butler' ),
'searching' => __( 'Searching…', 'albert-ai-butler' ),
'noResults' => __( 'Nobody matched that.', 'albert-ai-butler' ),
'failed' => __( 'Could not search users. Reload the page and try again.', 'albert-ai-butler' ),
'prompt' => __( 'Type a name, email address or user ID to search.', 'albert-ai-butler' ),
'loadingDefaults' => __( 'Loading…', 'albert-ai-butler' ),
// Both plural forms, chosen in the browser: the count is only
// known there, and _n() cannot be called after the page renders.
/* translators: %s: the search term. */
'matchesOne' => __( 'One match for “%s”, among users who can edit content.', 'albert-ai-butler' ),
/* translators: 1: number of matches, 2: the search term. */
'matchesMany' => __( '%1$d matches for “%2$s”, among users who can edit content.', 'albert-ai-butler' ),
'defaultSuggestionOne' => __( 'One person you can add. Search to find someone else.', 'albert-ai-butler' ),
/* translators: %d: number of suggested people. */
'defaultSuggestionsMany' => __( '%d people you can add. Search to find someone else.', 'albert-ai-butler' ),
'defaultSuggestionsEmpty' => __( 'Everyone on this site can already approve an assistant. Search to find someone else.', 'albert-ai-butler' ),
'alreadyAllowed' => __( 'Already allowed', 'albert-ai-butler' ),
/* translators: %s: the person's name. */
'removeChip' => __( 'Remove %s from the selection', 'albert-ai-butler' ),
'addNone' => __( 'Add user', 'albert-ai-butler' ),
'addOne' => __( 'Add 1 user', 'albert-ai-butler' ),
/* translators: %d: number of selected people. */
'addMany' => __( 'Add %d users', 'albert-ai-butler' ),
'addFailed' => __( 'Could not add the selected people. Reload the page and try again.', 'albert-ai-butler' ),
'emailSending' => __( 'Sending…', 'albert-ai-butler' ),
'emailSent' => __( 'Email sent', 'albert-ai-butler' ),
'emailFailed' => __( 'The email couldn’t be sent. Copy the link and send it another way.', 'albert-ai-butler' ),
'emailFailedShort' => __( 'Couldn’t send, copy the link instead', 'albert-ai-butler' ),
'copied' => __( 'Copied', 'albert-ai-butler' ),
'copiedAnnounce' => __( 'Setup link copied', 'albert-ai-butler' ),
'copyFailed' => __( 'Couldn’t copy the link, try again', 'albert-ai-butler' ),
'noRole' => __( 'No role on this site', 'albert-ai-butler' ),
/* translators: %s: the person's name. */
'chosen' => __( '%s added to the selection.', 'albert-ai-butler' ),
/* translators: %s: the person's name. */
'unchosen' => __( '%s removed from the selection.', 'albert-ai-butler' ),
],
]
);
}
/**
* Render the dialog.
*
* Rendered once per screen, next to whichever button opens it. Everything
* inside is a real form posting to `admin-post.php`: the script fills the
* hidden field with the chosen ids, it does not submit anything itself.
*
* @param string $return_to Which screen the form should come back to.
*
* @return void
* @since 1.4.0
*/
public static function render( string $return_to ): void {
?>
* @since 1.4.0
*/
private static function allowed_user_ids(): array {
return AllowedUsers::ids();
}
/**
* Role slug to translated name, for labelling picker results.
*
* @return array
* @since 1.4.0
*/
private static function role_names(): array {
$labels = [];
foreach ( wp_roles()->get_names() as $slug => $name ) {
$labels[ $slug ] = translate_user_role( $name );
}
return $labels;
}
}