From 984e35ac47ece67164404c61fda94af7e3f0c4cc Mon Sep 17 00:00:00 2001 From: Paulo Cruz Date: Mon, 11 Sep 2023 10:34:17 -0300 Subject: [PATCH] Newsletter Categories: Add Newsletter Categories to Subscription Management page (#81521) * Rename to useSubscribedNewsletterCategories * Export getCategoriesKey * Create useNewsletterCategorySubscriptionMutation * Fix Subscriber Details displayed categories * Create SubscribeToNewsletterCategories component * Fix tests * Add loading state and feature flag * Optmize mutation to send only updaded categories * Extract NewsletterCategoriesResponse * Fix repeated cache key * Await query invalidation * Optmize rendering newsletter categories --- .../reader-site-subscription/details.tsx | 9 +- .../reader-site-subscription/styles.scss | 4 + .../subscribe-to-newsletter-categories.tsx | 86 +++++++++++++++++++ client/data/newsletter-categories/index.ts | 2 + ...ibed-newsletter-categories-query.test.tsx} | 10 +-- client/data/newsletter-categories/types.ts | 5 ++ .../use-categories-query.tsx | 4 +- ...e-mark-as-newsletter-category-mutation.tsx | 4 +- .../use-newsletter-categories-query.tsx | 15 ++-- ...sletter-category-subscription-mutation.tsx | 72 ++++++++++++++++ ...ubscribed-newsletter-categories-query.tsx} | 10 +-- ...unmark-as-newsletter-category-mutation.tsx | 4 +- .../components/settings/styles.scss | 4 +- .../subscriber-details/subscriber-details.tsx | 5 +- .../subscribers/subscriber-details-page.tsx | 10 +-- 15 files changed, 210 insertions(+), 34 deletions(-) create mode 100644 client/blocks/reader-site-subscription/subscribe-to-newsletter-categories.tsx rename client/data/newsletter-categories/test/{use-subscriber-newsletter-categories-query.test.tsx => use-subscribed-newsletter-categories-query.test.tsx} (89%) create mode 100644 client/data/newsletter-categories/use-newsletter-category-subscription-mutation.tsx rename client/data/newsletter-categories/{use-subscriber-newsletter-categories-query.tsx => use-subscribed-newsletter-categories-query.tsx} (79%) diff --git a/client/blocks/reader-site-subscription/details.tsx b/client/blocks/reader-site-subscription/details.tsx index b2d8ec0dcb6f8d..9e43932757f9ec 100644 --- a/client/blocks/reader-site-subscription/details.tsx +++ b/client/blocks/reader-site-subscription/details.tsx @@ -18,6 +18,7 @@ import { getPaymentInterval, } from './helpers'; import SiteSubscriptionSettings from './settings'; +import SubscribeToNewsletterCategories from './subscribe-to-newsletter-categories'; import './styles.scss'; const SiteSubscriptionDetails = ( { @@ -221,11 +222,17 @@ const SiteSubscriptionDetails = ( { emailMeNewComments={ !! deliveryMethods.email?.send_comments } /> + { !! deliveryMethods.email?.send_posts && ( + + ) } +
{ /* TODO: Move to SiteSubscriptionInfo component when payment details are in. */ }
-

{ translate( 'Subscription' ) }

+

+ { translate( 'Subscription details' ) } +

{ translate( 'Date' ) }
diff --git a/client/blocks/reader-site-subscription/styles.scss b/client/blocks/reader-site-subscription/styles.scss index 6b296db84dba38..d1d3ae865f7085 100644 --- a/client/blocks/reader-site-subscription/styles.scss +++ b/client/blocks/reader-site-subscription/styles.scss @@ -136,6 +136,10 @@ } } } + + &__loading { + margin-bottom: 20px; + } } &__unsubscribe-button.components-button { diff --git a/client/blocks/reader-site-subscription/subscribe-to-newsletter-categories.tsx b/client/blocks/reader-site-subscription/subscribe-to-newsletter-categories.tsx new file mode 100644 index 00000000000000..b38278c501db89 --- /dev/null +++ b/client/blocks/reader-site-subscription/subscribe-to-newsletter-categories.tsx @@ -0,0 +1,86 @@ +import config from '@automattic/calypso-config'; +import { Spinner } from '@automattic/components'; +import { ToggleControl } from '@wordpress/components'; +import { useTranslate } from 'i18n-calypso'; +import { useMemo } from 'react'; +import { + useNewsletterCategorySubscriptionMutation, + useSubscribedNewsletterCategories, +} from 'calypso/data/newsletter-categories'; + +type SubscribeToNewsletterCategoriesProps = { + siteId: number; +}; + +const SubscribeToNewsletterCategories = ( { siteId }: SubscribeToNewsletterCategoriesProps ) => { + const translate = useTranslate(); + const { data: subscribedNewsletterCategoriesData, isLoading } = useSubscribedNewsletterCategories( + { siteId } + ); + const { mutate, isLoading: isSaving } = useNewsletterCategorySubscriptionMutation( siteId ); + + const subscribedCategoryIds = useMemo( + () => + subscribedNewsletterCategoriesData?.newsletterCategories + .filter( ( category ) => !! category.subscribed ) + .map( ( category ) => category.id ) || [], + [ subscribedNewsletterCategoriesData ] + ); + + const handleToggle = ( categoryId: number ) => { + mutate( [ + { + term_id: categoryId, + subscribe: ! subscribedCategoryIds.includes( categoryId ), + }, + ] ); + }; + + if ( + ! config.isEnabled( 'settings/newsletter-categories' ) || + ! subscribedNewsletterCategoriesData?.enabled + ) { + return null; + } + + return ( + <> +
+
+

{ translate( 'Subscription' ) }

+ + { isLoading ? ( +
+ +
+ ) : ( +
+
{ translate( 'Subscribed to' ) }
+
+ { subscribedNewsletterCategoriesData?.newsletterCategories.map( + ( newletterCategory ) => ( +
+ handleToggle( newletterCategory.id ) } + disabled={ isSaving } + label={ newletterCategory.name } + /> +

+ { translate( 'Receive emails for new posts in %s', { + args: [ newletterCategory.name ], + comment: 'Name of the site that the user tried to resubscribe to.', + } ) } +

+
+ ) + ) } +
+
+ ) } +
+ + ); +}; + +export default SubscribeToNewsletterCategories; diff --git a/client/data/newsletter-categories/index.ts b/client/data/newsletter-categories/index.ts index 69bca125ff9320..b1833c646b7f21 100644 --- a/client/data/newsletter-categories/index.ts +++ b/client/data/newsletter-categories/index.ts @@ -3,3 +3,5 @@ export { default as useNewsletterCategoriesFeatureEnabled } from './use-newslett export { default as useNewsletterCategoriesQuery } from './use-newsletter-categories-query'; export { default as useMarkAsNewsletterCategoryMutation } from './use-mark-as-newsletter-category-mutation'; export { default as useUnmarkAsNewsletterCategoryMutation } from './use-unmark-as-newsletter-category-mutation'; +export { default as useSubscribedNewsletterCategories } from './use-subscribed-newsletter-categories-query'; +export { default as useNewsletterCategorySubscriptionMutation } from './use-newsletter-category-subscription-mutation'; diff --git a/client/data/newsletter-categories/test/use-subscriber-newsletter-categories-query.test.tsx b/client/data/newsletter-categories/test/use-subscribed-newsletter-categories-query.test.tsx similarity index 89% rename from client/data/newsletter-categories/test/use-subscriber-newsletter-categories-query.test.tsx rename to client/data/newsletter-categories/test/use-subscribed-newsletter-categories-query.test.tsx index 89a18d3c11a640..96d607e9a58840 100644 --- a/client/data/newsletter-categories/test/use-subscriber-newsletter-categories-query.test.tsx +++ b/client/data/newsletter-categories/test/use-subscribed-newsletter-categories-query.test.tsx @@ -6,11 +6,11 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook, waitFor } from '@testing-library/react'; import React from 'react'; import request from 'wpcom-proxy-request'; -import useSubscriberNewsletterCategories from '../use-subscriber-newsletter-categories-query'; +import useSubscribedNewsletterCategories from '../use-subscribed-newsletter-categories-query'; jest.mock( 'wpcom-proxy-request', () => jest.fn() ); -describe( 'useSubscriberNewsletterCategories', () => { +describe( 'useSubscribedNewsletterCategories', () => { let queryClient: QueryClient; let wrapper: any; @@ -56,7 +56,7 @@ describe( 'useSubscriberNewsletterCategories', () => { ], } ); - const { result } = renderHook( () => useSubscriberNewsletterCategories( { siteId: 123 } ), { + const { result } = renderHook( () => useSubscribedNewsletterCategories( { siteId: 123 } ), { wrapper, } ); @@ -89,7 +89,7 @@ describe( 'useSubscriberNewsletterCategories', () => { newsletter_categories: [], } ); - const { result } = renderHook( () => useSubscriberNewsletterCategories( { siteId: 123 } ), { + const { result } = renderHook( () => useSubscribedNewsletterCategories( { siteId: 123 } ), { wrapper, } ); @@ -103,7 +103,7 @@ describe( 'useSubscriberNewsletterCategories', () => { success: true, } ); - renderHook( () => useSubscriberNewsletterCategories( { siteId: 123 } ), { + renderHook( () => useSubscribedNewsletterCategories( { siteId: 123 } ), { wrapper, } ); diff --git a/client/data/newsletter-categories/types.ts b/client/data/newsletter-categories/types.ts index 5d671728d13f98..f471b285d563d6 100644 --- a/client/data/newsletter-categories/types.ts +++ b/client/data/newsletter-categories/types.ts @@ -24,3 +24,8 @@ export type NewsletterCategory = { parent: number; subscribed?: boolean; }; + +export type NewsletterCategoriesResponse = { + enabled: boolean; + newsletter_categories: NewsletterCategory[]; +}; diff --git a/client/data/newsletter-categories/use-categories-query.tsx b/client/data/newsletter-categories/use-categories-query.tsx index e9bc786a66e5d4..9df91d234e3f84 100644 --- a/client/data/newsletter-categories/use-categories-query.tsx +++ b/client/data/newsletter-categories/use-categories-query.tsx @@ -2,9 +2,11 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; import request from 'wpcom-proxy-request'; import type { Category } from './types'; +export const getCategoriesKey = ( siteId?: string | number ) => [ 'categories', siteId ]; + const useCategoriesQuery = ( siteId?: string | number ): UseQueryResult< Category[] > => { return useQuery( { - queryKey: [ 'categories', siteId ], + queryKey: getCategoriesKey( siteId ), queryFn: () => request< Category[] >( { path: `/sites/${ siteId }/categories`, diff --git a/client/data/newsletter-categories/use-mark-as-newsletter-category-mutation.tsx b/client/data/newsletter-categories/use-mark-as-newsletter-category-mutation.tsx index 025139c64a67cd..5703e732d71b94 100644 --- a/client/data/newsletter-categories/use-mark-as-newsletter-category-mutation.tsx +++ b/client/data/newsletter-categories/use-mark-as-newsletter-category-mutation.tsx @@ -60,8 +60,8 @@ const useMarkAsNewsletterCategoryMutation = ( siteId: string | number ) => { onError: ( error, variables, context ) => { queryClient.setQueryData( cacheKey, context?.previousData ); }, - onSettled: () => { - queryClient.invalidateQueries( cacheKey ); + onSettled: async () => { + await queryClient.invalidateQueries( cacheKey ); }, } ); }; diff --git a/client/data/newsletter-categories/use-newsletter-categories-query.tsx b/client/data/newsletter-categories/use-newsletter-categories-query.tsx index 0f9740100798e1..84fcbf3d3c6768 100644 --- a/client/data/newsletter-categories/use-newsletter-categories-query.tsx +++ b/client/data/newsletter-categories/use-newsletter-categories-query.tsx @@ -1,18 +1,13 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; import request from 'wpcom-proxy-request'; -import { NewsletterCategories, NewsletterCategory } from './types'; +import { NewsletterCategories, NewsletterCategoriesResponse } from './types'; type NewsletterCategoryQueryProps = { siteId?: string | number; }; -type NewsletterCategoryResponse = { - enabled: boolean; - newsletter_categories: NewsletterCategory[]; -}; - -const convertNewsletterCategoryResponse = ( - response: NewsletterCategoryResponse +export const convertNewsletterCategoriesResponse = ( + response: NewsletterCategoriesResponse ): NewsletterCategories => { return { enabled: response.enabled, @@ -31,11 +26,11 @@ const useNewsletterCategories = ( { return useQuery( { queryKey: getNewsletterCategoriesKey( siteId ), queryFn: () => - request< NewsletterCategoryResponse >( { + request< NewsletterCategoriesResponse >( { path: `/sites/${ siteId }/newsletter-categories`, apiVersion: '2', apiNamespace: 'wpcom/v2', - } ).then( convertNewsletterCategoryResponse ), + } ).then( convertNewsletterCategoriesResponse ), enabled: !! siteId, } ); }; diff --git a/client/data/newsletter-categories/use-newsletter-category-subscription-mutation.tsx b/client/data/newsletter-categories/use-newsletter-category-subscription-mutation.tsx new file mode 100644 index 00000000000000..b0b367162d6176 --- /dev/null +++ b/client/data/newsletter-categories/use-newsletter-category-subscription-mutation.tsx @@ -0,0 +1,72 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import request from 'wpcom-proxy-request'; +import { NewsletterCategories, NewsletterCategoriesResponse } from './types'; +import { getSubscribedNewsletterCategoriesKey } from './use-subscribed-newsletter-categories-query'; + +type NewsletterCategorySubscription = { + term_id: number; + subscribe: boolean; +}; + +const useNewsletterCategorySubscriptionMutation = ( siteId: string | number ) => { + const queryClient = useQueryClient(); + const subscribedCategoriesCacheKey = getSubscribedNewsletterCategoriesKey( siteId ); + + return useMutation( { + mutationFn: async ( categorySubscriptions: NewsletterCategorySubscription[] ) => { + return await request< NewsletterCategoriesResponse >( { + path: `/sites/${ siteId }/newsletter-categories/subscriptions`, + method: 'POST', + apiVersion: '2', + apiNamespace: 'wpcom/v2', + body: { categories: categorySubscriptions }, + } ); + }, + onMutate: async ( categorySubscriptions: NewsletterCategorySubscription[] ) => { + await queryClient.cancelQueries( subscribedCategoriesCacheKey ); + + const previousData = queryClient.getQueryData< NewsletterCategories >( + subscribedCategoriesCacheKey + ); + + queryClient.setQueryData( + subscribedCategoriesCacheKey, + ( oldData?: NewsletterCategories ) => { + const newSubscribedCategories = + previousData?.newsletterCategories.map( ( category ) => { + const categorySubscription = categorySubscriptions.find( + ( subscription ) => subscription.term_id === category.id + ); + + if ( ! categorySubscription ) { + return category; + } + + return { + ...category, + subscribed: categorySubscription.subscribe, + }; + } ) || []; + + const updatedData = { + ...oldData, + enabled: oldData?.enabled || false, + newsletterCategories: newSubscribedCategories, + }; + + return updatedData; + } + ); + + return { previousData }; + }, + onError: ( error, variables, context ) => { + queryClient.setQueryData( subscribedCategoriesCacheKey, context?.previousData ); + }, + onSettled: async () => { + await queryClient.invalidateQueries( subscribedCategoriesCacheKey ); + }, + } ); +}; + +export default useNewsletterCategorySubscriptionMutation; diff --git a/client/data/newsletter-categories/use-subscriber-newsletter-categories-query.tsx b/client/data/newsletter-categories/use-subscribed-newsletter-categories-query.tsx similarity index 79% rename from client/data/newsletter-categories/use-subscriber-newsletter-categories-query.tsx rename to client/data/newsletter-categories/use-subscribed-newsletter-categories-query.tsx index 7b4891c3991210..21db3ef48190c6 100644 --- a/client/data/newsletter-categories/use-subscriber-newsletter-categories-query.tsx +++ b/client/data/newsletter-categories/use-subscribed-newsletter-categories-query.tsx @@ -12,10 +12,10 @@ type NewsletterCategoryResponse = { newsletter_categories: NewsletterCategory[]; }; -export const getSubscriberNewsletterCategoriesKey = ( +export const getSubscribedNewsletterCategoriesKey = ( siteId?: string | number, subscriptionId?: number -) => [ `newsletter-categories`, siteId, subscriptionId ]; +) => [ 'subscribed-newsletter-categories', siteId, subscriptionId ]; const convertNewsletterCategoryResponse = ( response: NewsletterCategoryResponse @@ -24,12 +24,12 @@ const convertNewsletterCategoryResponse = ( newsletterCategories: response.newsletter_categories, } ); -const useSubscriberNewsletterCategories = ( { +const useSubscribedNewsletterCategories = ( { siteId, subscriptionId, }: NewsletterCategoryQueryProps ): UseQueryResult< NewsletterCategories > => { return useQuery( { - queryKey: getSubscriberNewsletterCategoriesKey( siteId, subscriptionId ), + queryKey: getSubscribedNewsletterCategoriesKey( siteId, subscriptionId ), queryFn: () => request< NewsletterCategoryResponse >( { path: `/sites/${ siteId }/newsletter-categories/subscriptions${ @@ -42,4 +42,4 @@ const useSubscriberNewsletterCategories = ( { } ); }; -export default useSubscriberNewsletterCategories; +export default useSubscribedNewsletterCategories; diff --git a/client/data/newsletter-categories/use-unmark-as-newsletter-category-mutation.tsx b/client/data/newsletter-categories/use-unmark-as-newsletter-category-mutation.tsx index d212634fd136bf..7ba9ec00a25316 100644 --- a/client/data/newsletter-categories/use-unmark-as-newsletter-category-mutation.tsx +++ b/client/data/newsletter-categories/use-unmark-as-newsletter-category-mutation.tsx @@ -52,8 +52,8 @@ const useUnmarkAsNewsletterCategoryMutation = ( siteId: string | number ) => { onError: ( error, variables, context ) => { queryClient.setQueryData( cacheKey, context?.previousData ); }, - onSettled: () => { - queryClient.invalidateQueries( cacheKey ); + onSettled: async () => { + await queryClient.invalidateQueries( cacheKey ); }, } ); }; diff --git a/client/landing/subscriptions/components/settings/styles.scss b/client/landing/subscriptions/components/settings/styles.scss index 71e64c5fb01466..d6f78b92308323 100644 --- a/client/landing/subscriptions/components/settings/styles.scss +++ b/client/landing/subscriptions/components/settings/styles.scss @@ -1,14 +1,14 @@ @import "@automattic/color-studio/dist/color-variables"; @import "@automattic/typography/styles/variables"; -$setting-item-margin-bottom: 16px; +$setting-item-margin-bottom: 20px; .settings { .setting-item { margin-bottom: $setting-item-margin-bottom; &__hint { - margin-top: 16px; + margin-top: 8px; color: $studio-gray-50; font-size: $font-body-extra-small; text-align: left; diff --git a/client/my-sites/subscribers/components/subscriber-details/subscriber-details.tsx b/client/my-sites/subscribers/components/subscriber-details/subscriber-details.tsx index 44b76587710f3e..c06442a41d3362 100644 --- a/client/my-sites/subscribers/components/subscriber-details/subscriber-details.tsx +++ b/client/my-sites/subscribers/components/subscriber-details/subscriber-details.tsx @@ -30,7 +30,10 @@ const SubscriberDetails = ( { const translate = useTranslate(); const subscriptionPlans = useSubscriptionPlans( subscriber ); const newsletterCategoryNames = useMemo( - () => newsletterCategories?.map( ( category ) => category.name ), + () => + newsletterCategories + ?.filter( ( category ) => !! category.subscribed ) + .map( ( category ) => category.name ), [ newsletterCategories ] ); const { avatar, date_subscribed, display_name, email_address, country, url } = subscriber; diff --git a/client/my-sites/subscribers/subscriber-details-page.tsx b/client/my-sites/subscribers/subscriber-details-page.tsx index 84eec27e53644b..001f159bb0d5a8 100644 --- a/client/my-sites/subscribers/subscriber-details-page.tsx +++ b/client/my-sites/subscribers/subscriber-details-page.tsx @@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux'; import { Item } from 'calypso/components/breadcrumb'; import FixedNavigationHeader from 'calypso/components/fixed-navigation-header'; import Main from 'calypso/components/main'; -import useSubscriberNewsletterCategories from 'calypso/data/newsletter-categories/use-subscriber-newsletter-categories-query'; +import { useSubscribedNewsletterCategories } from 'calypso/data/newsletter-categories'; import { useSelector } from 'calypso/state'; import { successNotice } from 'calypso/state/notices/actions'; import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selectors'; @@ -46,8 +46,8 @@ const SubscriberDetailsPage = ( { userId ); - const { data: newsletterCategoriesData, isLoading: isLoadingNewsletterCategories } = - useSubscriberNewsletterCategories( { + const { data: subscribedNewsletterCategoriesData, isLoading: isLoadingNewsletterCategories } = + useSubscribedNewsletterCategories( { siteId: selectedSiteId as number, subscriptionId: subscriptionId || subscriber?.subscription_id, } ); @@ -114,8 +114,8 @@ const SubscriberDetailsPage = ( { siteId={ selectedSiteId } subscriptionId={ subscriptionId } userId={ userId } - newsletterCategoriesEnabled={ newsletterCategoriesData?.enabled } - newsletterCategories={ newsletterCategoriesData?.newsletterCategories } + newsletterCategoriesEnabled={ subscribedNewsletterCategoriesData?.enabled } + newsletterCategories={ subscribedNewsletterCategoriesData?.newsletterCategories } /> ) }