Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

My Jetpack: Update data handling - Implement request status in Product Card #22475

Merged
merged 16 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const renderActionButton = ( {
onManage,
onFixConnection,
onActivate,
isFetching,
} ) => {
if ( ! admin ) {
return (
Expand All @@ -57,6 +58,11 @@ const renderActionButton = ( {
);
}

const buttonState = {
isPressed: ! isFetching,
disabled: isFetching,
};

switch ( status ) {
case PRODUCT_STATUSES.ABSENT:
return (
Expand All @@ -69,27 +75,27 @@ const renderActionButton = ( {
);
case PRODUCT_STATUSES.ACTIVE:
return (
<Button isPressed onClick={ onManage }>
<Button { ...buttonState } onClick={ onManage }>
{ __( 'Manage', 'jetpack-my-jetpack' ) }
</Button>
);
case PRODUCT_STATUSES.ERROR:
return (
<Button isPressed onClick={ onFixConnection }>
<Button { ...buttonState } onClick={ onFixConnection }>
{ __( 'Fix connection', 'jetpack-my-jetpack' ) }
</Button>
);
case PRODUCT_STATUSES.INACTIVE:
return (
<Button isPressed onClick={ onActivate }>
<Button { ...buttonState } onClick={ onActivate }>
{ __( 'Activate', 'jetpack-my-jetpack' ) }
</Button>
);
}
};

const ProductCard = props => {
const { name, admin, description, icon, status, onDeactivate } = props;
const { name, admin, description, icon, status, onDeactivate, isFetching } = props;
const isActive = status === PRODUCT_STATUSES.ACTIVE;
const isError = status === PRODUCT_STATUSES.ERROR;
const isInactive = status === PRODUCT_STATUSES.INACTIVE;
Expand All @@ -105,6 +111,7 @@ const ProductCard = props => {
[ styles.active ]: isActive,
[ styles.inactive ]: isInactive,
[ styles.error ]: isError,
[ styles[ 'is-fetching' ] ]: isFetching,
} );

return (
Expand All @@ -120,9 +127,10 @@ const ProductCard = props => {
{ renderActionButton( props ) }
<DropdownMenu
className={ styles.dropdown }
toggleProps={ { isPressed: true } }
toggleProps={ { isPressed: true, disabled: isFetching } }
popoverProps={ { noArrow: false } }
icon={ DownIcon }
disableOpenOnArrowDown={ true }
controls={ [
{
title: __( 'Deactivate', 'jetpack-my-jetpack' ),
Expand All @@ -146,6 +154,7 @@ ProductCard.propTypes = {
description: PropTypes.string.isRequired,
icon: PropTypes.element,
admin: PropTypes.bool.isRequired,
isFetching: PropTypes.bool,
onDeactivate: PropTypes.func,
onManage: PropTypes.func,
onFixConnection: PropTypes.func,
Expand All @@ -162,6 +171,7 @@ ProductCard.propTypes = {

ProductCard.defaultProps = {
icon: null,
isFetching: false,
onDeactivate: () => {},
onManage: () => {},
onFixConnection: () => {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,23 @@
}
}
}

&.is-fetching {
&:before {
animation: blink-animation 0.5s linear infinite;
}
}
}

@keyframes blink-animation{
0%{
opacity: 0;
}
50%{
opacity: 0.5;
}
100%{
opacity: 0;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const BackupIcon = () => (
);

const BackupCard = ( { admin } ) => {
const { status, activate, deactivate, detail } = useProduct( 'backup' );
const { status, activate, deactivate, detail, isFetching } = useProduct( 'backup' );
const { name, description } = detail;

return (
Expand All @@ -31,6 +31,7 @@ const BackupCard = ( { admin } ) => {
description={ description }
status={ status }
icon={ <BackupIcon /> }
isFetching={ isFetching }
admin={ admin }
onDeactivate={ deactivate }
onActivate={ activate }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import ProductCard, { PRODUCT_STATUSES } from '../product-card';
import ProductCard from '../product-card';
import { useProduct } from '../../hooks/use-product';

const BoostIcon = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
Expand All @@ -16,14 +17,19 @@ const BoostIcon = () => (
);

const BoostCard = ( { admin } ) => {
// @todo: implement action handlers
const { status, activate, deactivate, detail, isFetching } = useProduct( 'boost' );
const { name, description } = detail;

return (
<ProductCard
name="Boost"
description="Instant speed and SEO"
status={ PRODUCT_STATUSES.ABSENT }
name={ name }
description={ description }
status={ status }
icon={ <BoostIcon /> }
admin={ admin }
isFetching={ isFetching }
onDeactivate={ deactivate }
onActivate={ activate }
/>
);
};
Expand Down
2 changes: 0 additions & 2 deletions projects/packages/my-jetpack/_inc/hooks/use-product/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ An object with details about the product.

Returns the current products list of My Jetpack.

An array with all products list.

### activate( <productSlug> )
A helper function to activate a product.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function useProduct( productId ) {
productsList: useSelect( select => select( STORE_ID ).getProducts() ),
detail,
isActive: detail.status === 'active',
isFetching: useSelect( select => select( STORE_ID ).isFetching( productId ) ),
status: detail.status, // shorthand. Consider to remove.
isFetching: !! detail.isFetching,
};
}
62 changes: 43 additions & 19 deletions projects/packages/my-jetpack/_inc/state/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import { REST_API_SITE_PRODUCTS_ENDPOINT } from './constants';
const SET_PURCHASES_IS_FETCHING = 'SET_PURCHASES_IS_FETCHING';
const FETCH_PURCHASES = 'FETCH_PURCHASES';
const SET_PURCHASES = 'SET_PURCHASES';
const SET_PRODUCT_ACTION_ERROR = 'SET_PRODUCT_ACTION_ERROR';
const SET_IS_FETCHING_PRODUCT = 'SET_IS_FETCHING_PRODUCT';
const SET_PRODUCT = 'SET_PRODUCT';
const SET_PRODUCT_REQUEST_ERROR = 'SET_PRODUCT_REQUEST_ERROR';
const ACTIVATE_PRODUCT = 'ACTIVATE_PRODUCT';
const DEACTIVATE_PRODUCT = 'DEACTIVATE_PRODUCT';
const SET_FETCHING_PRODUCT_STATUS = 'SET_FETCHING_PRODUCT_STATUS';
const SET_PRODUCT_STATUS = 'SET_PRODUCT_STATUS';

const setPurchasesIsFetching = isFetching => {
Expand All @@ -33,17 +34,33 @@ const setPurchases = purchases => {
return { type: SET_PURCHASES, purchases };
};

const setProduct = product => ( { type: SET_PRODUCT, product } );

const setRequestProductError = ( productId, error ) => ( {
type: SET_PRODUCT_REQUEST_ERROR,
productId,
error,
} );

const setProductStatus = ( productId, status ) => {
return { type: SET_PRODUCT_STATUS, productId, status };
};

const setIsFetchingProductStatus = ( productId, isFetching ) => {
return { type: SET_FETCHING_PRODUCT_STATUS, productId, isFetching };
};

const setProductActionError = error => {
return { type: SET_PRODUCT_ACTION_ERROR, error };
};
/**
* Action that set the `isFetching` state of the product,
* when the client hits the server.
*
* @param {string} productId - My Jetpack product ID.
* @param {boolean} isFetching - True if the product is being fetched. Otherwise, False.
* @returns {object} - Redux action.
*/
function setIsFetchingProduct( productId, isFetching ) {
return {
type: SET_IS_FETCHING_PRODUCT,
productId,
isFetching,
};
}

/**
* Side effect action which will sync
Expand All @@ -61,31 +78,34 @@ function requestProductStatus( productId, data, { select, dispatch } ) {
// Check valid product.
const isValid = select.isValidProduct( productId );
if ( ! isValid ) {
dispatch( setProductStatus( productId, { status: 'error' } ) );
return dispatch(
setProductActionError( {
setRequestProductError( productId, {
code: 'invalid_product',
message: __( 'Invalid product name', 'jetpack-my-jetpack' ),
} )
);
}

dispatch( setIsFetchingProductStatus( productId, true ) );
const method = data.activate ? 'POST' : 'DELETE';
dispatch( setIsFetchingProduct( productId, true ) );

// Activate/deactivate product.
return apiFetch( {
path: `${ REST_API_SITE_PRODUCTS_ENDPOINT }/${ productId }`,
method: 'POST',
method,
data,
} )
.then( status => {
dispatch( setIsFetchingProductStatus( productId, false ) );
dispatch( setProductStatus( productId, status ) );
.then( freshProduct => {
dispatch( setIsFetchingProduct( productId, false ) );
dispatch( setProduct( freshProduct ) );
resolve( status );
} )
.catch( error => {
dispatch( setProductActionError( error ) );
dispatch( setProductStatus( productId, { status: 'error' } ) );
dispatch( setRequestProductError( productId, error ) );
reject( error );
dispatch( setIsFetchingProductStatus( productId, false ) );
dispatch( setIsFetchingProduct( productId, false ) );
} );
} );
}
Expand Down Expand Up @@ -113,8 +133,11 @@ const deactivateProduct = productId => async store => {
};

const productActions = {
setProduct,
activateProduct,
deactivateProduct,
setIsFetchingProduct,
setRequestProductError,
};

const actions = {
Expand All @@ -128,10 +151,11 @@ export {
SET_PURCHASES_IS_FETCHING,
FETCH_PURCHASES,
SET_PURCHASES,
SET_PRODUCT_ACTION_ERROR,
SET_PRODUCT,
SET_PRODUCT_REQUEST_ERROR,
ACTIVATE_PRODUCT,
DEACTIVATE_PRODUCT,
SET_FETCHING_PRODUCT_STATUS,
SET_IS_FETCHING_PRODUCT,
SET_PRODUCT_STATUS,
actions as default,
};
45 changes: 31 additions & 14 deletions projects/packages/my-jetpack/_inc/state/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,29 @@ import { combineReducers } from '@wordpress/data';
import {
SET_PURCHASES,
SET_PURCHASES_IS_FETCHING,
SET_PRODUCT_ACTION_ERROR,
SET_PRODUCT,
SET_PRODUCT_STATUS,
SET_FETCHING_PRODUCT_STATUS,
SET_IS_FETCHING_PRODUCT,
SET_PRODUCT_REQUEST_ERROR,
} from './actions';

const products = ( state = {}, action ) => {
switch ( action.type ) {
case SET_IS_FETCHING_PRODUCT: {
const { productId, isFetching } = action;
return {
...state,
isFetching: {
...state.isFetching,
[ productId ]: isFetching,
},
errors: {
...state.errors,
[ productId ]: isFetching ? undefined : state.errors[ productId ],
},
renatoagds marked this conversation as resolved.
Show resolved Hide resolved
};
}

case SET_PRODUCT_STATUS: {
const { productId, status } = action;
return {
Expand All @@ -27,28 +43,29 @@ const products = ( state = {}, action ) => {
...status,
},
},
error: {},
};
}

case SET_PRODUCT_ACTION_ERROR:
case SET_PRODUCT: {
const { product } = action;
const { slug: productId } = product;
return {
...state,
error: action.error,
items: {
...state.items,
[ productId ]: product,
},
};
}

case SET_FETCHING_PRODUCT_STATUS: {
const { productId, isFetching } = action;
case SET_PRODUCT_REQUEST_ERROR: {
const { productId, error } = action;
return {
...state,
items: {
...state.items,
[ productId ]: {
...state.items[ productId ],
isFetching,
},
errors: {
...state.errors,
[ productId ]: error,
},
error: isFetching ? {} : state.error,
};
}

Expand Down
Loading