Skip to content

Commit

Permalink
Social: Replace can_disconnect with a store data selector (#40888)
Browse files Browse the repository at this point in the history
* Pass the connected users WPCOM data to the UI

* Augment wpcom for user object

* Create canUserManageConnection selector

* Replace can_disconnect with the new selector

* Fix unit tests

* Add changelog

* Fix user data for WPCOM sites

* Fix unit tests

* Add changelog
  • Loading branch information
manzoorwanijk authored Jan 10, 2025
1 parent e058b84 commit 6b80d99
Show file tree
Hide file tree
Showing 19 changed files with 132 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Social: Replaced can_disconnect with a store data selector
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ export function Disconnect( {

const { deleteConnectionById } = useDispatch( socialStore );

const { isDisconnecting } = useSelect(
const { isDisconnecting, canManageConnection } = useSelect(
select => {
const { getDeletingConnections } = select( socialStore );
const { getDeletingConnections, canUserManageConnection } = select( socialStore );

return {
isDisconnecting: getDeletingConnections().includes( connection.connection_id ),
canManageConnection: canUserManageConnection( connection ),
};
},
[ connection.connection_id ]
[ connection ]
);

const onClickDisconnect = useCallback( async () => {
Expand All @@ -49,7 +50,7 @@ export function Disconnect( {
} );
}, [ connection.connection_id, deleteConnectionById ] );

if ( ! connection.can_disconnect ) {
if ( ! canManageConnection ) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ export function Reconnect( { connection, service, variant = 'link' }: ReconnectP
const { deleteConnectionById, setKeyringResult, openConnectionsModal, setReconnectingAccount } =
useDispatch( socialStore );

const { isDisconnecting } = useSelect(
const { isDisconnecting, canManageConnection } = useSelect(
select => {
const { getDeletingConnections } = select( socialStore );
const { getDeletingConnections, canUserManageConnection } = select( socialStore );

return {
isDisconnecting: getDeletingConnections().includes( connection.connection_id ),
canManageConnection: canUserManageConnection( connection ),
};
},
[ connection.connection_id ]
[ connection ]
);

const onConfirm = useCallback(
Expand Down Expand Up @@ -80,7 +81,7 @@ export function Reconnect( { connection, service, variant = 'link' }: ReconnectP
setReconnectingAccount,
] );

if ( ! connection.can_disconnect ) {
if ( ! canManageConnection ) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ describe( 'Disconnecting a connection', () => {
service_name: 'facebook',
connection_id: '2',
display_name: 'Facebook',
can_disconnect: true,
} }
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ describe( 'Marking a connection as shared', () => {
service_name: 'facebook',
connection_id: '2',
display_name: 'Facebook',
can_disconnect: true,
} }
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ describe( 'Reconnect', () => {

const mockConnection = {
connection_id: '123',
can_disconnect: true,
external_display: 'mockDisplay',
display_name: 'mockDisplay',
};

beforeEach( () => {
Expand Down Expand Up @@ -58,12 +57,8 @@ describe( 'Reconnect', () => {
} );

test( 'does not render the button if connection cannot be disconnected', () => {
const nonDisconnectableConnection = {
...mockConnection,
can_disconnect: false,
};

render( <Reconnect connection={ nonDisconnectableConnection } service={ mockService } /> );
setup( { canUserManageConnection: false } );
render( <Reconnect connection={ mockConnection } service={ mockService } /> );

expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument();
} );
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { IconTooltip, Text } from '@automattic/jetpack-components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { store as socialStore } from '../../social-store';
import { Connection } from '../../social-store/types';
import { ConnectionName } from '../connection-management/connection-name';
import { ConnectionStatus } from '../connection-management/connection-status';
Expand All @@ -19,6 +21,11 @@ export const ServiceConnectionInfo = ( {
service,
isAdmin,
}: ServiceConnectionInfoProps ) => {
const canManageConnection = useSelect(
select => select( socialStore ).canUserManageConnection( connection ),
[ connection ]
);

return (
<div className={ styles[ 'service-connection' ] }>
<div>
Expand All @@ -40,7 +47,7 @@ export const ServiceConnectionInfo = ( {
* if the user can disconnect the connection.
* Otherwise, non-admin authors will see only the status without any further context.
*/
if ( conn.status === 'broken' && conn.can_disconnect ) {
if ( conn.status === 'broken' && canManageConnection ) {
return <ConnectionStatus connection={ conn } service={ service } />;
}

Expand All @@ -63,7 +70,7 @@ export const ServiceConnectionInfo = ( {
* Now if the user is not an admin, we tell them that the connection
* was added by an admin and show the connection status if it's broken.
*/
return ! conn.can_disconnect ? (
return ! canManageConnection ? (
<>
<Text className={ styles.description }>
{ __(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Button, useBreakpointMatch } from '@automattic/jetpack-components';
import { Panel, PanelBody } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useEffect, useReducer, useRef } from '@wordpress/element';
import { __, _x } from '@wordpress/i18n';
import { Icon, chevronDown, chevronUp } from '@wordpress/icons';
import { store as socialStore } from '../../social-store';
import { ConnectForm } from './connect-form';
import { ServiceItemDetails, ServicesItemDetailsProps } from './service-item-details';
import { ServiceStatus } from './service-status';
Expand Down Expand Up @@ -40,9 +42,11 @@ export function ServiceItem( {

const brokenConnections = serviceConnections.filter( ( { status } ) => status === 'broken' );

const hasOwnBrokenConnections = brokenConnections.some(
( { can_disconnect } ) => can_disconnect
);
const hasOwnBrokenConnections = useSelect( select => {
const { canUserManageConnection, getBrokenConnections } = select( socialStore );

return getBrokenConnections().some( canUserManageConnection );
}, [] );

const hideInitialConnectForm =
// For services with custom inputs, the initial Connect button opens the panel,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Alert } from '@automattic/jetpack-components';
import { useSelect } from '@wordpress/data';
import { __, _n, sprintf } from '@wordpress/i18n';
import { store as socialStore } from '../../social-store';
import { Connection } from '../../social-store/types';
import styles from './style.module.scss';

Expand All @@ -16,13 +18,16 @@ export type ServiceStatusProps = {
* @return {import('react').ReactNode} Service status component
*/
export function ServiceStatus( { serviceConnections, brokenConnections }: ServiceStatusProps ) {
const canFix = useSelect(
select => brokenConnections.some( select( socialStore ).canUserManageConnection ),
[ brokenConnections ]
);

if ( ! serviceConnections.length ) {
return null;
}

if ( brokenConnections.length > 0 ) {
const canFix = brokenConnections.some( ( { can_disconnect } ) => can_disconnect );

return (
<Alert
level={ canFix ? 'error' : 'warning' }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
import { setup } from '../../../utils/test-factory';
import { ServiceConnectionInfo } from '../service-connection-info';

jest.mock( '../../connection-management/connection-name', () => ( {
Expand All @@ -19,13 +20,22 @@ describe( 'ServiceConnectionInfo', () => {
profile_picture: 'https://example.com/profile.jpg',
display_name: 'Example User',
status: 'connected',
can_disconnect: true,
};

const service = {
icon: () => <svg aria-label="test-svg"></svg>,
};

beforeAll( () => {
global.JetpackScriptData = {
user: {
current_user: {
id: 123,
},
},
};
} );

const renderComponent = ( connOverrides = {}, serviceOverrides = {}, props = {} ) => {
render(
<ServiceConnectionInfo
Expand All @@ -36,6 +46,10 @@ describe( 'ServiceConnectionInfo', () => {
);
};

afterEach( () => {
jest.clearAllMocks();
} );

test( 'renders profile picture if available', () => {
renderComponent();
const profilePic = screen.getByAltText( 'Example User' );
Expand Down Expand Up @@ -70,7 +84,9 @@ describe( 'ServiceConnectionInfo', () => {
} );

test( 'displays description if connection cannot be disconnected', () => {
renderComponent( { can_disconnect: false } );
setup( { canUserManageConnection: false } );
renderComponent();

expect(
screen.getByText( 'This connection is added by a site administrator.' )
).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,6 @@ export function createConnection( data, optimisticData = {} ) {
// Updating the connection will also override the connection_id.
updateConnection( tempId, {
...connection,
can_disconnect: true,
// For editor, we always enable the connection by default.
enabled: true,
} )
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { getScriptData } from '@automattic/jetpack-script-data';
import { store as coreStore } from '@wordpress/core-data';
import { createRegistrySelector } from '@wordpress/data';
import { REQUEST_TYPE_DEFAULT } from '../actions/constants';
import { SocialStoreState } from '../types';
import { Connection, SocialStoreState } from '../types';

/**
* Returns the connections list from the store.
Expand Down Expand Up @@ -227,3 +230,32 @@ export function getKeyringResult( state ) {
export function isConnectionsModalOpen( state ) {
return state.connectionData?.isConnectionsModalOpen ?? false;
}

/**
* Whether the current user can manage the connection.
*/
export const canUserManageConnection = createRegistrySelector(
select =>
( state: SocialStoreState, connectionOrId: Connection | string ): boolean => {
const connection =
typeof connectionOrId === 'string'
? getConnectionById( state, connectionOrId )
: connectionOrId;

const { current_user } = getScriptData().user;

// If the current user is the connection owner.
if ( current_user.wpcom?.ID === connection.user_id ) {
return true;
}

const {
// @ts-expect-error getUser exists but `core-data` entities are not typed properly.
// Should work fine after https://github.com/WordPress/gutenberg/pull/67668 is released to npm.
getUser,
} = select( coreStore );

// The user has to be at least an editor to manage the connection.
return getUser( current_user.id )?.capabilities?.edit_others_posts ?? false;
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import * as utmSelectors from './utm-settings';

/**
* Returns whether the site settings are being saved.
*
* @type {() => boolean} Whether the site settings are being saved.
*/
export const isSavingSiteSettings = createRegistrySelector( select => () => {
return select( coreStore ).isSavingEntityRecord( 'root', 'site', undefined );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ export type Connection = {
user_id: number;

/* DEPRECATED FIELDS */
/**
* @deprecated
*/
can_disconnect?: boolean;
/**
* @deprecated
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ jest.mock( '../hooks/use-social-media-connections', () => ( {

export const setup = ( {
connections = [
{ service_name: 'twitter', connection_id: '1', display_name: 'Twitter', can_disconnect: true },
{ service_name: 'twitter', connection_id: '1', display_name: 'Twitter' },
{
service_name: 'facebook',
connection_id: '2',
display_name: 'Facebook',
can_disconnect: true,
},
],
getDeletingConnections = [],
getUpdatingConnections = [],
canUserManageConnection = true,
} = {} ) => {
let storeSelect;
renderHook( () => useSelect( select => ( storeSelect = select( store ) ) ) );
Expand All @@ -36,6 +36,10 @@ export const setup = ( {
.mockReset()
.mockReturnValue( getUpdatingConnections );
const stubGetKeyringResult = jest.spyOn( storeSelect, 'getKeyringResult' ).mockReset();
jest
.spyOn( storeSelect, 'canUserManageConnection' )
.mockReset()
.mockReturnValue( canUserManageConnection );

const { result: dispatch } = renderHook( () => useDispatch( store ) );
const stubDeleteConnectionById = jest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Added wpcom data for current user
4 changes: 4 additions & 0 deletions projects/js-packages/script-data/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface SiteData extends PublicSiteData, Partial< AdminSiteData > {}
export interface CurrentUserData {
id: number;
display_name: string;
wpcom?: {
ID: number;
login: string;
};
}

export interface UserData {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Social: Replaced can_disconnect with a store data selector
Loading

0 comments on commit 6b80d99

Please sign in to comment.