Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions inc/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,25 +158,31 @@ public function register_routes() {
],
'threads' => [
[
'methods' => \WP_REST_Server::READABLE,
'args' => [
'methods' => \WP_REST_Server::READABLE,
'args' => [
'offset' => [
'required' => false,
'type' => 'integer',
'default' => 0,
],
],
'callback' => [ $this, 'get_threads' ],
'callback' => [ $this, 'get_threads' ],
'permission_callback' => function () {
return current_user_can( 'hyve_read_messages' );
},
],
[
'methods' => \WP_REST_Server::DELETABLE,
'args' => [
'methods' => \WP_REST_Server::DELETABLE,
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
],
],
'callback' => [ $this, 'delete_thread' ],
'callback' => [ $this, 'delete_thread' ],
'permission_callback' => function () {
return current_user_can( 'hyve_manage_messages' );
},
],
],
'qdrant' => [
Expand Down
104 changes: 101 additions & 3 deletions inc/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public function __construct() {
new Threads();

add_action( 'admin_menu', [ $this, 'register_menu_page' ] );
add_filter( 'user_has_cap', [ $this, 'grant_message_capabilities' ] );
add_action( 'save_post', [ $this, 'update_meta' ], 10, 3 );
add_action( 'delete_post', [ $this, 'delete_post' ] );
add_filter( 'themeisle_sdk_enable_telemetry', '__return_true' );
Expand Down Expand Up @@ -132,6 +133,34 @@ public function add_privacy_policy_content() {
wp_add_privacy_policy_content( 'Hyve', wp_kses_post( $content ) );
}

/**
* Grant the Messages capabilities to administrators.
*
* The two capabilities are custom, so nobody has them by default. Granting
* them to anyone who can `manage_options` keeps the Messages submenu and its
* REST endpoints working for admins. Doing it here, instead of persisting to
* the role, means there is nothing to clean up on uninstall.
*
* To give access to other roles, add the capabilities to them with a
* role-editor plugin or WP_Role::add_cap():
* - `hyve_read_messages` view the Messages page and read conversations.
* - `hyve_manage_messages` delete conversations and export them.
*
* @since 1.5.0
*
* @param array<string, bool> $allcaps All capabilities of the current user.
*
* @return array<string, bool>
*/
public function grant_message_capabilities( $allcaps ) {
if ( ! empty( $allcaps['manage_options'] ) ) {
$allcaps['hyve_read_messages'] = true;
$allcaps['hyve_manage_messages'] = true;
}

return $allcaps;
}

/**
* Register menu page.
*
Expand All @@ -140,7 +169,7 @@ public function add_privacy_policy_content() {
* @return void
*/
public function register_menu_page() {
$page_hook_suffix = add_menu_page(
add_menu_page(
__( 'Hyve', 'hyve-lite' ),
__( 'Hyve', 'hyve-lite' ),
'manage_options',
Expand All @@ -150,7 +179,62 @@ public function register_menu_page() {
99
);

add_action( "admin_print_scripts-$page_hook_suffix", [ $this, 'enqueue_options_assets' ] );
foreach ( $this->get_submenu_pages() as $slug => $submenu ) {
$hook = add_submenu_page(
'hyve',
$submenu['label'],
$submenu['label'],
$submenu['capability'],
$slug,
[ $this, 'menu_page' ]
);

if ( $hook ) {
add_action( "admin_print_scripts-$hook", [ $this, 'enqueue_options_assets' ] );
}
}
}

/**
* Get the Hyve submenu pages.
*
* Each entry mirrors a top-level section of the dashboard app and deep-links
* into it. The `route` is passed to the app so the matching screen opens.
* Messages is gated on `hyve_read_messages` so support staff can reach it
* without seeing the rest of the dashboard.
*
* @since 1.5.0
*
* @return array<string, array{label: string, capability: string, route: string}>
*/
public function get_submenu_pages() {
return [
'hyve' => [
'label' => __( 'Dashboard', 'hyve-lite' ),
'capability' => 'manage_options',
'route' => 'home',
],
'hyve-knowledge-base' => [
'label' => __( 'Knowledge Base', 'hyve-lite' ),
'capability' => 'manage_options',
'route' => 'data',
],
'hyve-messages' => [
'label' => __( 'Messages', 'hyve-lite' ),
'capability' => 'hyve_read_messages',
'route' => 'messages',
],
'hyve-integrations' => [
'label' => __( 'Integrations', 'hyve-lite' ),
'capability' => 'manage_options',
'route' => 'integrations',
],
'hyve-settings' => [
'label' => __( 'Settings', 'hyve-lite' ),
'capability' => 'manage_options',
'route' => 'settings',
],
];
}

/**
Expand Down Expand Up @@ -184,12 +268,26 @@ public function admin_init() {
];
}

$submenu_pages = $this->get_submenu_pages();

// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the current admin page to pick the initial app screen, no state change.
$current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : 'hyve';
$current_view = isset( $submenu_pages[ $current_page ] ) ? $submenu_pages[ $current_page ]['route'] : 'home';

add_filter(
'hyve_options_data',
function ( $data ) use ( $settings, $post_types_for_js ) {
function ( $data ) use ( $settings, $post_types_for_js, $current_view ) {
/**
* PHPStan false positive: the return type is an array, but PHPStan cannot infer it because of the dynamic nature of the filter.
*
* @phpstan-ignore return.type
*/
return array_merge(
$data,
[
'view' => $current_view,
'canManage' => current_user_can( 'manage_options' ),
'canManageMessages' => current_user_can( 'hyve_manage_messages' ),
'api' => $this->api->get_endpoint(),
'rest_url' => rest_url( $this->api->get_endpoint() ),
'postTypes' => $post_types_for_js,
Expand Down
9 changes: 9 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@

<rule ref="Generic.Arrays.DisallowLongArraySyntax.Found" />

<rule ref="WordPress.WP.Capabilities">
<properties>
<property name="custom_capabilities" type="array">
<element value="hyve_read_messages" />
<element value="hyve_manage_messages" />
</property>
</properties>
</rule>

<arg name="extensions" value="php" />
<arg value="s" />

Expand Down
18 changes: 16 additions & 2 deletions src/backend/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ const App = () => {
document.dispatchEvent( sdkEvent );
};

fetchData();
// Support users without full access cannot read settings; skip the
// request so the app finishes loading instead of hanging on a 403.
if ( window.hyve?.canManage ) {
fetchData();
} else {
setLoading();
}

const urlParams = new URLSearchParams( window.location.search );
const nav = urlParams.get( 'nav' );
Expand All @@ -52,7 +58,15 @@ const App = () => {
}
}, [ setSettings, setLoading, setRoute ] );

const ROUTE_TREE = applyFilters( 'hyve.route', ROUTE );
let ROUTE_TREE = applyFilters( 'hyve.route', ROUTE );

// Restrict the app to Messages for users who can only read messages, so a
// support user never reaches a settings screen or an admin-only request.
if ( ! window.hyve?.canManage ) {
ROUTE_TREE = ROUTE_TREE.messages
? { messages: ROUTE_TREE.messages }
: {};
}

const ROUTE_COMPONENTS = Object.keys( ROUTE_TREE ).reduce( ( acc, key ) => {
if ( ROUTE_TREE[ key ].component ) {
Expand Down
28 changes: 16 additions & 12 deletions src/backend/parts/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,15 @@ const MessageThreadView = ( { selectedPost, onDelete } ) => {
<p className="text-xs text-gray-500">
{ selectedPost?.thread_id?.replace( 'thread_', '' ) }
</p>
<Button
isDestructive={ true }
aria-label={ __( 'Delete conversation', 'hyve-lite' ) }
onClick={ () => onDelete( selectedPost?.ID ) }
>
<Icon icon={ 'trash' } />
</Button>
{ window.hyve?.canManageMessages && (
<Button
isDestructive={ true }
aria-label={ __( 'Delete conversation', 'hyve-lite' ) }
onClick={ () => onDelete( selectedPost?.ID ) }
>
<Icon icon={ 'trash' } />
</Button>
) }
</div>
<div className="overflow-scroll pl-4 grow">
{ selectedPost?.thread?.map( ( message, index ) => (
Expand Down Expand Up @@ -333,11 +335,13 @@ const Messages = () => {
/>
</div>
</div>
<div className="flex justify-end mt-1">
<ExportMessagesAction
onClick={ () => setUpsellOpen( true ) }
/>
</div>
{ window.hyve?.canManageMessages && (
<div className="flex justify-end mt-1">
<ExportMessagesAction
onClick={ () => setUpsellOpen( true ) }
/>
</div>
) }
</>
) }
</PanelRow>
Expand Down
9 changes: 8 additions & 1 deletion src/backend/parts/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ const Sidebar = () => {

const { setRoute } = useDispatch( 'hyve' );

const MENU_ITEMS = applyFilters( 'hyve.route', ROUTE_TREE );
let MENU_ITEMS = applyFilters( 'hyve.route', ROUTE_TREE );

// Support users who can only read messages see just the Messages entry.
if ( ! window.hyve?.canManage ) {
MENU_ITEMS = MENU_ITEMS.messages
? { messages: MENU_ITEMS.messages }
: {};
}

return (
<div className="col-span-6 xl:col-span-2">
Expand Down
1 change: 1 addition & 0 deletions src/backend/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const ROUTE_TREE = {
label: __( 'Messages', 'hyve-lite' ),
icon: comment,
component: Messages,
disabled: false,
},
integrations: {
label: __( 'Integrations', 'hyve-lite' ),
Expand Down
2 changes: 1 addition & 1 deletion src/backend/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { createReduxStore, register } from '@wordpress/data';

const DEFAULT_STATE = {
route: 'home',
route: window.hyve?.view || 'home',
hasLoaded: false,
settings: {},
processed: [],
Expand Down
Loading