diff --git a/.yarn/patches/@wordpress-dataviews-npm-0.4.1-2c01fa0792.patch b/.yarn/patches/@wordpress-dataviews-npm-0.4.1-2c01fa0792.patch deleted file mode 100644 index ac1aef83914985..00000000000000 --- a/.yarn/patches/@wordpress-dataviews-npm-0.4.1-2c01fa0792.patch +++ /dev/null @@ -1,87 +0,0 @@ -diff --git a/build/lock-unlock.js b/build/lock-unlock.js -index b1e3c1e3b950c7d3095876fdf32dc9d0094a8f7a..3885591aaed7c99d345b7428a57b9b7dcbb982dd 100644 ---- a/build/lock-unlock.js -+++ b/build/lock-unlock.js -@@ -1,7 +1,7 @@ - "use strict"; - - Object.defineProperty(exports, "__esModule", { -- value: true -+ value: true, - }); - exports.unlock = exports.lock = void 0; - var _privateApis = require("@wordpress/private-apis"); -@@ -9,10 +9,11 @@ var _privateApis = require("@wordpress/private-apis"); - * WordPress dependencies - */ - --const { -- lock, -- unlock --} = (0, _privateApis.__dangerousOptInToUnstableAPIsOnlyForCoreModules)('I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', '@wordpress/dataviews'); -+const { lock, unlock } = (0, -+_privateApis.__dangerousOptInToUnstableAPIsOnlyForCoreModules)( -+ "I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.", -+ "@wordpress/dataviews" -+); - exports.unlock = unlock; - exports.lock = lock; - //# sourceMappingURL=lock-unlock.js.map -diff --git a/build/lock-unlock.js.map b/build/lock-unlock.js.map -index b20c8e5e5cc50b108035dbfb4c765b354835476a..3edcce8eed204c1da1a55175bb617fb66c89d8a9 100644 ---- a/build/lock-unlock.js.map -+++ b/build/lock-unlock.js.map -@@ -1 +1 @@ --{"version":3,"names":["_privateApis","require","lock","unlock","__dangerousOptInToUnstableAPIsOnlyForCoreModules","exports"],"sources":["@wordpress/dataviews/src/lock-unlock.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';\n\nexport const { lock, unlock } =\n\t__dangerousOptInToUnstableAPIsOnlyForCoreModules(\n\t\t'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.',\n\t\t'@wordpress/dataviews'\n\t);\n"],"mappings":";;;;;;AAGA,IAAAA,YAAA,GAAAC,OAAA;AAHA;AACA;AACA;;AAGO,MAAM;EAAEC,IAAI;EAAEC;AAAO,CAAC,GAC5B,IAAAC,6DAAgD,EAC/C,iHAAiH,EACjH,sBACD,CAAC;AAACC,OAAA,CAAAF,MAAA,GAAAA,MAAA;AAAAE,OAAA,CAAAH,IAAA,GAAAA,IAAA"} -\ No newline at end of file -+{"version":3,"names":["_privateApis","require","lock","unlock","__dangerousOptInToUnstableAPIsOnlyForCoreModules","exports"],"sources":["@wordpress/dataviews/src/lock-unlock.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';\n\nexport const { lock, unlock } =\n\t__dangerousOptInToUnstableAPIsOnlyForCoreModules(\n\t\t'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',\n\t\t'@wordpress/dataviews'\n\t);\n"],"mappings":";;;;;;AAGA,IAAAA,YAAA,GAAAC,OAAA;AAHA;AACA;AACA;;AAGO,MAAM;EAAEC,IAAI;EAAEC;AAAO,CAAC,GAC5B,IAAAC,6DAAgD,EAC/C,iHAAiH,EACjH,sBACD,CAAC;AAACC,OAAA,CAAAF,MAAA,GAAAA,MAAA;AAAAE,OAAA,CAAAH,IAAA,GAAAA,IAAA"} -\ No newline at end of file -diff --git a/build-module/lock-unlock.js b/build-module/lock-unlock.js -index 79b912f8d2976acba70c34235d856368bf906425..0c778415d2bcf2ee21fab94d5518d123730c6623 100644 ---- a/build-module/lock-unlock.js -+++ b/build-module/lock-unlock.js -@@ -1,9 +1,10 @@ - /** - * WordPress dependencies - */ --import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; --export const { -- lock, -- unlock --} = __dangerousOptInToUnstableAPIsOnlyForCoreModules('I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', '@wordpress/dataviews'); -+import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from "@wordpress/private-apis"; -+export const { lock, unlock } = -+ __dangerousOptInToUnstableAPIsOnlyForCoreModules( -+ "I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.", -+ "@wordpress/dataviews" -+ ); - //# sourceMappingURL=lock-unlock.js.map -diff --git a/build-module/lock-unlock.js.map b/build-module/lock-unlock.js.map -index 36173786489d0182174357e2b57e4e3351f50055..28dc0b6ae24f362442a98877134784a19bc2fc7f 100644 ---- a/build-module/lock-unlock.js.map -+++ b/build-module/lock-unlock.js.map -@@ -1 +1 @@ --{"version":3,"names":["__dangerousOptInToUnstableAPIsOnlyForCoreModules","lock","unlock"],"sources":["@wordpress/dataviews/src/lock-unlock.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';\n\nexport const { lock, unlock } =\n\t__dangerousOptInToUnstableAPIsOnlyForCoreModules(\n\t\t'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.',\n\t\t'@wordpress/dataviews'\n\t);\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,gDAAgD,QAAQ,yBAAyB;AAE1F,OAAO,MAAM;EAAEC,IAAI;EAAEC;AAAO,CAAC,GAC5BF,gDAAgD,CAC/C,iHAAiH,EACjH,sBACD,CAAC"} -\ No newline at end of file -+{"version":3,"names":["__dangerousOptInToUnstableAPIsOnlyForCoreModules","lock","unlock"],"sources":["@wordpress/dataviews/src/lock-unlock.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';\n\nexport const { lock, unlock } =\n\t__dangerousOptInToUnstableAPIsOnlyForCoreModules(\n\t\t'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',\n\t\t'@wordpress/dataviews'\n\t);\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,gDAAgD,QAAQ,yBAAyB;AAE1F,OAAO,MAAM;EAAEC,IAAI;EAAEC;AAAO,CAAC,GAC5BF,gDAAgD,CAC/C,iHAAiH,EACjH,sBACD,CAAC"} -\ No newline at end of file -diff --git a/src/lock-unlock.js b/src/lock-unlock.js -index 18318773cefefee8becd93b68574d2b8659b5707..bf7fc262ddb2b241de42ab70ab207c34ccf487a6 100644 ---- a/src/lock-unlock.js -+++ b/src/lock-unlock.js -@@ -1,10 +1,10 @@ - /** - * WordPress dependencies - */ --import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; -+import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from "@wordpress/private-apis"; - - export const { lock, unlock } = -- __dangerousOptInToUnstableAPIsOnlyForCoreModules( -- 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', -- '@wordpress/dataviews' -- ); -+ __dangerousOptInToUnstableAPIsOnlyForCoreModules( -+ "I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.", -+ "@wordpress/dataviews" -+ ); diff --git a/apps/happy-blocks/package.json b/apps/happy-blocks/package.json index 80489eb44d4fa4..cc488ce06f67ff 100644 --- a/apps/happy-blocks/package.json +++ b/apps/happy-blocks/package.json @@ -29,7 +29,7 @@ "@automattic/calypso-build": "workspace:^", "@automattic/calypso-config": "workspace:^", "@automattic/calypso-products": "workspace:^", - "@automattic/color-studio": "2.6.0", + "@automattic/color-studio": "^3.0.1", "@automattic/components": "workspace:^", "@automattic/format-currency": "workspace:^", "@automattic/typography": "workspace:^", diff --git a/apps/wpcom-block-editor/src/calypso/features/iframe-bridge-server.js b/apps/wpcom-block-editor/src/calypso/features/iframe-bridge-server.js index 822d4c6777a7e9..00afb81810c5f0 100644 --- a/apps/wpcom-block-editor/src/calypso/features/iframe-bridge-server.js +++ b/apps/wpcom-block-editor/src/calypso/features/iframe-bridge-server.js @@ -1075,16 +1075,16 @@ function handleAppBannerShowing( calypsoPort ) { }; } -function handlePatterns( calypsoPort ) { - const selector = `[data-value="${ __( 'Patterns' ) }"]`; +function handleWpAdminRedirect( { calypsoPort, path, title } ) { + const selector = `[data-value="${ title }"]`; const callback = ( e ) => { e.preventDefault(); calypsoPort.postMessage( { - action: 'goToPatterns', + action: 'wpAdminRedirect', payload: { - destinationUrl: '/wp-admin/site-editor.php?postType=wp_block', + destinationUrl: `/wp-admin/${ path }`, unsavedChanges: select( 'core/editor' ).isEditedPostDirty(), }, } ); @@ -1094,6 +1094,26 @@ function handlePatterns( calypsoPort ) { addCommandsInputListener( selector, callback ); } +function handlePatterns( calypsoPort ) { + handleWpAdminRedirect( { + calypsoPort, + path: 'site-editor.php?postType=wp_block', + title: __( 'Patterns' ), + } ); +} + +function handleAddPage( calypsoPort ) { + handleWpAdminRedirect( { + calypsoPort, + path: 'post-new.php?post_type=page', + title: __( 'Add new page' ), + } ); +} + +function handleAddPost( calypsoPort ) { + handleWpAdminRedirect( { calypsoPort, path: 'post-new.php', title: __( 'Add new post' ) } ); +} + function initPort( message ) { if ( 'initPort' !== message.data.action ) { return; @@ -1196,6 +1216,10 @@ function initPort( message ) { handleAppBannerShowing( calypsoPort ); handlePatterns( calypsoPort ); + + handleAddPage( calypsoPort ); + + handleAddPost( calypsoPort ); } window.removeEventListener( 'message', initPort, false ); diff --git a/apps/wpcom-block-editor/src/wpcom/features/redirect-onboarding-user-after-publishing-post.js b/apps/wpcom-block-editor/src/wpcom/features/redirect-onboarding-user-after-publishing-post.js index 0e6f613d3195a6..16e77249a32c0a 100644 --- a/apps/wpcom-block-editor/src/wpcom/features/redirect-onboarding-user-after-publishing-post.js +++ b/apps/wpcom-block-editor/src/wpcom/features/redirect-onboarding-user-after-publishing-post.js @@ -1,4 +1,4 @@ -import { dispatch, select, subscribe } from '@wordpress/data'; +import { dispatch, select, subscribe, useSelect } from '@wordpress/data'; import { getQueryArg } from '@wordpress/url'; import { useEffect } from 'react'; import useLaunchpadScreen from './use-launchpad-screen'; @@ -11,34 +11,31 @@ export function RedirectOnboardingUserAfterPublishingPost() { const { siteIntent: intent } = useSiteIntent(); const { launchpad_screen: launchpadScreen } = useLaunchpadScreen(); - useEffect( () => { - // We check the URL param along with site intent because the param loads faster and prevents element flashing. - const hasStartWritingFlowQueryArg = - getQueryArg( window.location.search, START_WRITING_FLOW ) === 'true'; + const currentPostType = useSelect( + ( localSelect ) => localSelect( 'core/editor' ).getCurrentPostType(), + [] + ); - if ( - intent === START_WRITING_FLOW || - intent === DESIGN_FIRST_FLOW || - hasStartWritingFlowQueryArg - ) { - dispatch( 'core/edit-post' ).closeGeneralSidebar(); - document.documentElement.classList.add( 'blog-onboarding-hide' ); - } - }, [ intent ] ); - - // Check the URL parameter first so we can skip later processing ASAP. + // Check the URL parameter first so we can skip later processing ASAP and avoid flashing. const hasStartWritingFlowQueryArg = getQueryArg( window.location.search, START_WRITING_FLOW ) === 'true'; - if ( ! hasStartWritingFlowQueryArg ) { - return false; - } + const shouldShowMinimalUIAndRedirectToFullscreenLaunchpad = + ( intent === START_WRITING_FLOW || intent === DESIGN_FIRST_FLOW ) && + hasStartWritingFlowQueryArg && + 'full' === launchpadScreen && + currentPostType === 'post'; - if ( intent !== START_WRITING_FLOW && intent !== DESIGN_FIRST_FLOW ) { - return false; - } + useEffect( () => { + if ( shouldShowMinimalUIAndRedirectToFullscreenLaunchpad ) { + dispatch( 'core/edit-post' ).closeGeneralSidebar(); + document.documentElement.classList.add( 'blog-onboarding-hide' ); + } else { + document.documentElement.classList.remove( 'blog-onboarding-hide' ); + } + }, [ shouldShowMinimalUIAndRedirectToFullscreenLaunchpad ] ); - if ( 'full' !== launchpadScreen ) { + if ( ! shouldShowMinimalUIAndRedirectToFullscreenLaunchpad ) { return false; } @@ -56,13 +53,6 @@ export function RedirectOnboardingUserAfterPublishingPost() { const isCurrentPostPublished = select( 'core/editor' ).isCurrentPostPublished(); const isCurrentPostScheduled = select( 'core/editor' ).isCurrentPostScheduled(); const getCurrentPostRevisionsCount = select( 'core/editor' ).getCurrentPostRevisionsCount(); - const currentPostType = select( 'core/editor' ).getCurrentPostType(); - - // If we're editing anything that is not a post, including pages, templates, and navigation, nothing further needed. - if ( currentPostType && currentPostType !== 'post' ) { - unsubscribe(); - return; - } if ( ! isSavingPost && diff --git a/apps/wpcom-block-editor/src/wpcom/features/test/redirect-onboarding-user-after-publishing-post.test.js b/apps/wpcom-block-editor/src/wpcom/features/test/redirect-onboarding-user-after-publishing-post.test.js index b0c320c1bc6892..927363af67de83 100644 --- a/apps/wpcom-block-editor/src/wpcom/features/test/redirect-onboarding-user-after-publishing-post.test.js +++ b/apps/wpcom-block-editor/src/wpcom/features/test/redirect-onboarding-user-after-publishing-post.test.js @@ -9,15 +9,15 @@ const mockUnSubscribe = jest.fn(); const mockClosePublishSidebar = jest.fn(); const mockCloseSidebar = jest.fn(); const mockSubscribeFunction = {}; -const mockUseEffectFunctions = []; -let mockSubscribeFunctionDescriptor = ''; +const mockUseEffectFunctions = {}; +let mockFunctionDescriptor = ''; let mockIsSaving = false; let mockPostType = 'post'; let mockLaunchpadScreen = 'full'; jest.mock( 'react', () => ( { useEffect: ( userFunction ) => { - mockUseEffectFunctions.push( userFunction ); + mockUseEffectFunctions[ mockFunctionDescriptor ] = userFunction; }, } ) ); @@ -39,29 +39,31 @@ jest.mock( '../use-site-intent', () => ( { }, } ) ); +const stubSelect = ( item ) => { + if ( item === 'core/editor' ) { + return { + isSavingPost: () => mockIsSaving, + isCurrentPostPublished: () => true, + getCurrentPostRevisionsCount: () => 1, + isCurrentPostScheduled: () => true, + getCurrentPostType: () => mockPostType, + }; + } + + if ( item === 'core/preferences' ) { + return { + get: () => true, + }; + } +}; + jest.mock( '@wordpress/data', () => ( { subscribe: ( userFunction ) => { - mockSubscribeFunction[ mockSubscribeFunctionDescriptor ] = userFunction; + mockSubscribeFunction[ mockFunctionDescriptor ] = userFunction; return mockUnSubscribe; }, - select: ( item ) => { - if ( item === 'core/editor' ) { - return { - isSavingPost: () => mockIsSaving, - isCurrentPostPublished: () => true, - getCurrentPostRevisionsCount: () => 1, - isCurrentPostScheduled: () => true, - getCurrentPostType: () => mockPostType, - }; - } - - if ( item === 'core/preferences' ) { - return { - get: () => true, - }; - } - }, + select: stubSelect, dispatch: () => { return { closePublishSidebar: () => { @@ -72,12 +74,13 @@ jest.mock( '@wordpress/data', () => ( { }, }; }, + useSelect: ( selector ) => selector( stubSelect ), } ) ); describe( 'RedirectOnboardingUserAfterPublishingPost', () => { it( 'should NOT redirect while saving the post', () => { mockIsSaving = true; - mockSubscribeFunctionDescriptor = 'no_redirect_while_saving_post'; + mockFunctionDescriptor = 'no_redirect_while_saving_post'; delete global.window; global.window = { sessionStorage: { @@ -94,8 +97,8 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { RedirectOnboardingUserAfterPublishingPost(); - expect( mockSubscribeFunction[ mockSubscribeFunctionDescriptor ] ).not.toBe( undefined ); - mockSubscribeFunction[ mockSubscribeFunctionDescriptor ](); + expect( mockSubscribeFunction[ mockFunctionDescriptor ] ).not.toBe( undefined ); + mockSubscribeFunction[ mockFunctionDescriptor ](); expect( mockUnSubscribe ).not.toHaveBeenCalled(); expect( global.window.location.href ).toBe( undefined ); @@ -104,7 +107,7 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { it( 'should redirect the user to the launchpad when a post is published and the start-writing query parameter is "true"', () => { jest.clearAllMocks(); mockIsSaving = false; - mockSubscribeFunctionDescriptor = 'redirect_to_launchpad_post'; + mockFunctionDescriptor = 'redirect_to_launchpad_post'; delete global.window; global.window = { @@ -122,8 +125,8 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { RedirectOnboardingUserAfterPublishingPost(); - expect( mockSubscribeFunction[ mockSubscribeFunctionDescriptor ] ).not.toBe( null ); - mockSubscribeFunction[ mockSubscribeFunctionDescriptor ](); + expect( mockSubscribeFunction[ mockFunctionDescriptor ] ).not.toBe( null ); + mockSubscribeFunction[ mockFunctionDescriptor ](); expect( mockUnSubscribe ).toHaveBeenCalledTimes( 1 ); expect( mockClosePublishSidebar ).toHaveBeenCalledTimes( 1 ); @@ -135,7 +138,7 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { it( 'should NOT redirect the user to the launchpad when a post is published and the start-writing query parameter is present but not "true"', () => { jest.clearAllMocks(); mockIsSaving = false; - mockSubscribeFunctionDescriptor = 'no_redirect_to_launchpad_bad_start_writing_param'; + mockFunctionDescriptor = 'no_redirect_to_launchpad_bad_start_writing_param'; delete global.window; global.window = { @@ -153,7 +156,7 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { RedirectOnboardingUserAfterPublishingPost(); - expect( mockSubscribeFunction[ mockSubscribeFunctionDescriptor ] ).toBe( undefined ); + expect( mockSubscribeFunction[ mockFunctionDescriptor ] ).toBe( undefined ); expect( mockClosePublishSidebar ).not.toHaveBeenCalled(); expect( global.window.location.href ).toBe( undefined ); } ); @@ -162,7 +165,7 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { jest.clearAllMocks(); mockIsSaving = false; mockLaunchpadScreen = 'skipped'; - mockSubscribeFunctionDescriptor = 'no_redirect_to_launchpad_skipped_launchpad_screen'; + mockFunctionDescriptor = 'no_redirect_to_launchpad_skipped_launchpad_screen'; delete global.window; global.window = { @@ -180,7 +183,7 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { RedirectOnboardingUserAfterPublishingPost(); - expect( mockSubscribeFunction[ mockSubscribeFunctionDescriptor ] ).toBe( undefined ); + expect( mockSubscribeFunction[ mockFunctionDescriptor ] ).toBe( undefined ); expect( mockClosePublishSidebar ).not.toHaveBeenCalled(); expect( global.window.location.href ).toBe( undefined ); @@ -191,7 +194,7 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { jest.clearAllMocks(); mockIsSaving = false; mockPostType = 'page'; - mockSubscribeFunctionDescriptor = 'no_redirect_page_published'; + mockFunctionDescriptor = 'no_redirect_page_published'; delete global.window; global.window = { @@ -209,11 +212,8 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { RedirectOnboardingUserAfterPublishingPost(); - expect( mockSubscribeFunction[ mockSubscribeFunctionDescriptor ] ).not.toBe( null ); - - mockSubscribeFunction[ mockSubscribeFunctionDescriptor ](); + expect( mockSubscribeFunction[ mockFunctionDescriptor ] ).toBe( undefined ); - expect( mockUnSubscribe ).toHaveBeenCalledTimes( 1 ); expect( mockClosePublishSidebar ).not.toHaveBeenCalled(); expect( global.window.location.href ).toBe( undefined ); } ); @@ -222,7 +222,7 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { jest.clearAllMocks(); mockIsSaving = false; mockPostType = 'template'; - mockSubscribeFunctionDescriptor = 'no_redirect_template_published'; + mockFunctionDescriptor = 'no_redirect_template_published'; delete global.window; global.window = { @@ -240,11 +240,8 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { RedirectOnboardingUserAfterPublishingPost(); - expect( mockSubscribeFunction[ mockSubscribeFunctionDescriptor ] ).not.toBe( null ); + expect( mockSubscribeFunction[ mockFunctionDescriptor ] ).toBe( undefined ); - mockSubscribeFunction[ mockSubscribeFunctionDescriptor ](); - - expect( mockUnSubscribe ).toHaveBeenCalledTimes( 1 ); expect( mockClosePublishSidebar ).not.toHaveBeenCalled(); expect( global.window.location.href ).toBe( undefined ); } ); @@ -252,7 +249,8 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { it( 'should close the sidebar once isComplementaryAreaVisible === true', () => { jest.clearAllMocks(); mockIsSaving = false; - mockSubscribeFunctionDescriptor = 'close_sidebar'; + mockPostType = 'post'; + mockFunctionDescriptor = 'close_sidebar'; delete global.window; global.window = { sessionStorage: { @@ -269,10 +267,10 @@ describe( 'RedirectOnboardingUserAfterPublishingPost', () => { RedirectOnboardingUserAfterPublishingPost(); - expect( mockSubscribeFunction[ mockSubscribeFunctionDescriptor ] ).not.toBe( null ); + expect( mockSubscribeFunction[ mockFunctionDescriptor ] ).not.toBe( undefined ); + expect( mockUseEffectFunctions[ mockFunctionDescriptor ] ).not.toBe( undefined ); - mockSubscribeFunction[ mockSubscribeFunctionDescriptor ](); - mockUseEffectFunctions[ 0 ](); + mockUseEffectFunctions[ mockFunctionDescriptor ](); expect( mockCloseSidebar ).toHaveBeenCalledTimes( 1 ); } ); diff --git a/client/a8c-for-agencies/components/a4a-confirmation-dialog/index.tsx b/client/a8c-for-agencies/components/a4a-confirmation-dialog/index.tsx new file mode 100644 index 00000000000000..aa1fd330b29d80 --- /dev/null +++ b/client/a8c-for-agencies/components/a4a-confirmation-dialog/index.tsx @@ -0,0 +1,62 @@ +import { Dialog } from '@automattic/components'; +import { Button } from '@wordpress/components'; +import { clsx } from 'clsx'; +import { useTranslate } from 'i18n-calypso'; +import { ReactNode } from 'react'; + +import './style.scss'; + +export type Props = { + className?: string; + title: string; + children: ReactNode; + onClose: () => void; + onConfirm?: () => void; + ctaLabel?: string; + closeLabel?: string; + isLoading?: boolean; + isDestructive?: boolean; +}; + +export function A4AConfirmationDialog( { + className, + title, + children, + onConfirm, + ctaLabel, + closeLabel, + onClose, + isLoading, + isDestructive, +}: Props ) { + const translate = useTranslate(); + + return ( + + { closeLabel ?? translate( 'Cancel' ) } + , + + , + ] } + > +

{ title }

+ + { children } +
+ ); +} diff --git a/client/a8c-for-agencies/components/a4a-confirmation-dialog/style.scss b/client/a8c-for-agencies/components/a4a-confirmation-dialog/style.scss new file mode 100644 index 00000000000000..abb089bec37955 --- /dev/null +++ b/client/a8c-for-agencies/components/a4a-confirmation-dialog/style.scss @@ -0,0 +1,11 @@ +.a4a-confirmation-dialog__heading { + padding-block-end: 16px; + @include a4a-font-heading-lg; +} + +.a4a-confirmation-dialog .dialog__action-buttons { + display: flex; + flex-direction: row; + gap: 8px; + justify-content: flex-end; +} diff --git a/client/a8c-for-agencies/components/a4a-request-wp-admin-access/index.tsx b/client/a8c-for-agencies/components/a4a-request-wp-admin-access/index.tsx new file mode 100644 index 00000000000000..74e4995a54553b --- /dev/null +++ b/client/a8c-for-agencies/components/a4a-request-wp-admin-access/index.tsx @@ -0,0 +1,45 @@ +import { Button } from '@wordpress/components'; +import { Icon, external } from '@wordpress/icons'; +import { useTranslate } from 'i18n-calypso'; +import illustration from 'calypso/assets/images/a8c-for-agencies/request-wp-admin-access-illustration.svg'; + +import './style.scss'; + +export function A4ARequestWPAdminAccess() { + const translate = useTranslate(); + + const kbArticleUrl = '#'; // FIXME: Add the correct URL + + return ( +
+
+
+ { translate( 'Request WP-Admin access' ) } +
+
+ { translate( + 'Ask the Agency owner to provide you WP-Admin access to this site in order to manage its plugins and features.' + ) } +
+
+ { translate( 'Learn more about team member permissions' ) } +
+ + +
+
+ illustration +
+
+ ); +} diff --git a/client/a8c-for-agencies/components/a4a-request-wp-admin-access/style.scss b/client/a8c-for-agencies/components/a4a-request-wp-admin-access/style.scss new file mode 100644 index 00000000000000..c3ee9568250f7a --- /dev/null +++ b/client/a8c-for-agencies/components/a4a-request-wp-admin-access/style.scss @@ -0,0 +1,64 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.a4a-request-wp-admin-access { + display: flex; + flex-direction: column; + + @include break-xlarge { + flex-direction: row; + background: var(--color-neutral-0); + border-radius: 4px; + } +} + +.a4a-request-wp-admin-access__content { + display: flex; + flex-direction: column; + justify-content: center; + padding: 32px 8px; + + @include break-xlarge { + padding: 32px; + } + + .a4a-request-wp-admin-access__heading { + @include a4a-font-heading-lg; + padding-block-end: 16px; + } + + .a4a-request-wp-admin-access__description { + @include a4a-font-body-lg; + padding-block-end: 24px; + } + + .a4a-request-wp-admin-access__learn-more { + @include a4a-font-body-md($font-weight: 600); + padding-block-end: 8px; + } +} + +.a4a-request-wp-admin-access__illustration { + display: flex; + + @include break-xlarge { + justify-content: flex-end; + width: 50%; + } +} + +a.components-button.a4a-request-wp-admin-access__learn-more-button { + @include a4a-font-body-md; + color: var(--color-primary); + justify-content: flex-start; + background: none; + text-decoration: none; + + &:hover { + color: var(--color-primary-60); + } + + svg { + margin-inline-start: 4px; + } +} diff --git a/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/hooks/use-managed-sites-map.ts b/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/hooks/use-managed-sites-map.ts index 14a42f526230a9..adeaa27b17d5b6 100644 --- a/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/hooks/use-managed-sites-map.ts +++ b/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/hooks/use-managed-sites-map.ts @@ -14,10 +14,6 @@ export default function useManagedSitesMap( { size = 100 }: Props ) { isPartnerOAuthTokenLoaded: false, searchQuery: '', currentPage: 1, - sort: { - field: '', - direction: '', - }, perPage: size, agencyId, filter: { diff --git a/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/table-content.tsx b/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/table-content.tsx index e4d5a9282e345e..8c5ce203b0e4c7 100644 --- a/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/table-content.tsx +++ b/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/table-content.tsx @@ -1,19 +1,24 @@ -import { useState, useEffect } from 'react'; +import { filterSortAndPaginate } from '@wordpress/dataviews'; +import { useMemo, useState, useEffect } from 'react'; import { initialDataViewsState } from 'calypso/a8c-for-agencies/components/items-dashboard/constants'; import ItemsDataViews from 'calypso/a8c-for-agencies/components/items-dashboard/items-dataviews'; import { SiteItem } from './wpcom-sites-table'; -import type { DataViewsColumn } from '../../items-dashboard/items-dataviews/interfaces'; +import type { Field } from '@wordpress/dataviews'; interface Props { items: SiteItem[]; - fields: DataViewsColumn[]; + fields: Field< any >[]; } export default function WPCOMSitesTableContent( { items, fields }: Props ) { const [ dataViewsState, setDataViewsState ] = useState( initialDataViewsState ); + const { data, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( items, dataViewsState, fields ); + }, [ items, dataViewsState, fields ] ); + useEffect( () => { - if ( items.length ) { + if ( data.length ) { const handleRowClick = ( event: Event ) => { const target = event.target as HTMLElement; @@ -51,22 +56,20 @@ export default function WPCOMSitesTableContent( { items, fields }: Props ) { } }; } - }, [ dataViewsState, items ] ); + }, [ dataViewsState, data ] ); return ( `${ item.id }`, - pagination: { - totalItems: 1, - totalPages: 1, - }, + pagination: paginationInfo, enableSearch: false, actions: [], dataViewsState: dataViewsState, setDataViewsState: setDataViewsState, + defaultLayouts: { table: {} }, } } /> ); diff --git a/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/wpcom-sites-table.tsx b/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/wpcom-sites-table.tsx index c90731128f4fae..9b4a298921eb99 100644 --- a/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/wpcom-sites-table.tsx +++ b/client/a8c-for-agencies/components/add-new-site-button/import-from-wpcom-modal/wpcom-sites-table.tsx @@ -55,10 +55,6 @@ export default function WPCOMSitesTable( { isPartnerOAuthTokenLoaded: false, searchQuery: '', currentPage: 1, - sort: { - field: '', - direction: '', - }, perPage: 1, agencyId, filter: { @@ -120,7 +116,7 @@ export default function WPCOMSitesTable( { ? [ { id: 'site', - header: ( + label: (
{ item.site }
), - width: '100%', enableHiding: false, enableSorting: false, }, @@ -154,7 +149,7 @@ export default function WPCOMSitesTable( { : [ { id: 'site', - header: ( + label: (
), - width: '100%', enableHiding: false, enableSorting: false, }, { id: 'date', - header: translate( 'Date' ).toUpperCase(), + label: translate( 'Date' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: SiteItem } ) => new Date( item.date ).toLocaleDateString(), - width: '100%', enableHiding: false, enableSorting: false, }, { id: 'type', - header: translate( 'Type' ).toUpperCase(), + label: translate( 'Type' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: SiteItem } ) => , - width: '100%', enableHiding: false, enableSorting: false, }, @@ -207,6 +199,8 @@ export default function WPCOMSitesTable( { { isPending ? ( ) : ( + // @ts-expect-error the error is because field.label types do not admit JSX.Elements. + // To remove when this is using dataviews@4.2.0 ) }
diff --git a/client/a8c-for-agencies/components/items-dashboard/constants.ts b/client/a8c-for-agencies/components/items-dashboard/constants.ts index 4a2938ac09a329..af095d7e9c8525 100644 --- a/client/a8c-for-agencies/components/items-dashboard/constants.ts +++ b/client/a8c-for-agencies/components/items-dashboard/constants.ts @@ -14,6 +14,5 @@ export const initialDataViewsState: DataViewsState = { perPage: 50, page: 1, search: '', - hiddenFields: [], layout: {}, }; diff --git a/client/a8c-for-agencies/components/items-dashboard/item-preview-pane/index.tsx b/client/a8c-for-agencies/components/items-dashboard/item-preview-pane/index.tsx index 8eac93573f993f..dc92efcfc4c918 100644 --- a/client/a8c-for-agencies/components/items-dashboard/item-preview-pane/index.tsx +++ b/client/a8c-for-agencies/components/items-dashboard/item-preview-pane/index.tsx @@ -4,6 +4,7 @@ import { GuidedTourStep } from 'calypso/a8c-for-agencies/components/guided-tour- import SectionNav from 'calypso/components/section-nav'; import NavItem from 'calypso/components/section-nav/item'; import NavTabs from 'calypso/components/section-nav/tabs'; +import { isWpMobileApp } from 'calypso/lib/mobile-app'; import ItemPreviewPaneContent from './item-preview-pane-content'; import ItemPreviewPaneHeader from './item-preview-pane-header'; import { FeaturePreviewInterface, PreviewPaneProps } from './types'; @@ -79,7 +80,9 @@ export default function ItemPreviewPane( { ); } ); - const shouldHideNav = hideNavIfSingleTab && featureTabs.length <= 1; + const isMobileApp = isWpMobileApp(); + + const shouldHideNav = ( hideNavIfSingleTab && featureTabs.length <= 1 ) || isMobileApp; return (
diff --git a/client/a8c-for-agencies/components/items-dashboard/item-preview-pane/item-preview-pane-header/index.tsx b/client/a8c-for-agencies/components/items-dashboard/item-preview-pane/item-preview-pane-header/index.tsx index b229840fe774e0..2c8d9ac3698bf8 100644 --- a/client/a8c-for-agencies/components/items-dashboard/item-preview-pane/item-preview-pane-header/index.tsx +++ b/client/a8c-for-agencies/components/items-dashboard/item-preview-pane/item-preview-pane-header/index.tsx @@ -7,6 +7,7 @@ import { translate } from 'i18n-calypso'; import { useEffect, useRef } from 'react'; import QuerySitePhpVersion from 'calypso/components/data/query-site-php-version'; import QuerySiteWpVersion from 'calypso/components/data/query-site-wp-version'; +import { isWpMobileApp } from 'calypso/lib/mobile-app'; import { useSelector, useDispatch } from 'calypso/state'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; import { getAtomicHostingPhpVersion } from 'calypso/state/selectors/get-atomic-hosting-php-version'; @@ -47,6 +48,7 @@ export default function ItemPreviewPaneHeader( { const wpVersion = selectedSite?.options?.software_version?.match( /^\d+(\.\d+){0,2}/ )?.[ 0 ]; // Some times it can be `6.6.1-alpha-58760`, so we strip the `-alpha-58760` part const isAtomic = useSelector( ( state ) => isSiteWpcomAtomic( state, siteId ) ); const isStagingSite = useSelector( ( state ) => isSiteWpcomStaging( state, siteId ) ); + const isMobileApp = isWpMobileApp(); const focusRef = useRef< HTMLButtonElement >( null ); @@ -77,101 +79,105 @@ export default function ItemPreviewPaneHeader( { { isAtomic && }
- { !! itemData?.withIcon && ( - - ) } -
-
-
{ itemData.title }
-
- { itemData?.url ? ( - - ) : ( - itemData.subtitle - ) } + { ! isMobileApp && ( + <> + { !! itemData?.withIcon && ( + + ) } +
+
+
{ itemData.title }
+
+ { itemData?.url ? ( + + ) : ( + itemData.subtitle + ) } - { extraProps && extraProps.subtitleExtra ? ( - - - - ) : ( - '' - ) } -
+ { extraProps && extraProps.subtitleExtra ? ( + + + + ) : ( + '' + ) } +
- { shouldDisplayVersionNumbers && ( -
- { wpVersion && ( - - ) } + { shouldDisplayVersionNumbers && ( +
+ { wpVersion && ( + + ) } - { phpVersion && ( -
- PHP{ ' ' } - - { phpVersion } - + { phpVersion && ( +
+ PHP{ ' ' } + + { phpVersion } + +
+ ) }
) }
- ) } -
- { isPreviewLoaded && ( -
- { extraProps?.headerButtons ? ( - {} ) } - /> - ) : ( - + { isPreviewLoaded && ( +
+ { extraProps?.headerButtons ? ( + {} ) } + /> + ) : ( + + ) } +
) }
- ) } -
+ + ) }
diff --git a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/index.tsx b/client/a8c-for-agencies/components/items-dashboard/items-dataviews/index.tsx index d7b4307dc7c6ab..0c1f45c1baa157 100644 --- a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/index.tsx +++ b/client/a8c-for-agencies/components/items-dashboard/items-dataviews/index.tsx @@ -4,7 +4,8 @@ import { useTranslate } from 'i18n-calypso'; import { ReactNode, useRef, useLayoutEffect } from 'react'; import ReactDOM from 'react-dom'; import { DataViews } from 'calypso/components/dataviews'; -import { ItemsDataViewsType, DataViewsColumn } from './interfaces'; +import { ItemsDataViewsType } from './interfaces'; +import type { Field } from '@wordpress/dataviews'; import './style.scss'; @@ -23,7 +24,7 @@ const getIdByPath = ( item: object, path: string ) => { /** * Create an item column for the DataViews component * @param id - * @param header + * @param label * @param displayField * @param getValue * @param isSortable @@ -31,18 +32,19 @@ const getIdByPath = ( item: object, path: string ) => { */ export const createItemColumn = ( id: string, - header: ReactNode, + label: ReactNode, displayField: () => ReactNode, getValue: () => undefined, isSortable: boolean = false, canHide: boolean = false -): DataViewsColumn => { +): Field< any > => { return { id, enableSorting: isSortable, enableHiding: canHide, getValue, - header, + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label, render: displayField, }; }; @@ -64,9 +66,7 @@ const ItemsDataViews = ( { data, isLoading = false, className }: ItemsDataViewsP ! scrollContainerRef.current || previousDataViewsState?.type !== data.dataViewsState.type ) { - scrollContainerRef.current = document.querySelector( - '.dataviews-view-list, .dataviews-view-table-wrapper' - ) as HTMLElement; + scrollContainerRef.current = document.querySelector( '.dataviews-view-list' ) as HTMLElement; } if ( ! previousDataViewsState?.selectedItem && data.dataViewsState.selectedItem ) { @@ -85,12 +85,13 @@ const ItemsDataViews = ( { data, isLoading = false, className }: ItemsDataViewsP return (
data.setDataViewsState( () => newView ) } + fields={ data.fields } search={ data?.enableSearch ?? true } searchLabel={ data.searchLabel ?? translate( 'Search' ) } + actions={ data.actions } getItemId={ data.getItemId ?? ( ( item: any ) => { @@ -99,11 +100,12 @@ const ItemsDataViews = ( { data, isLoading = false, className }: ItemsDataViewsP return item.id; } ) } - onSelectionChange={ data.onSelectionChange } - onChangeView={ data.setDataViewsState } - supportedLayouts={ [ 'table' ] } - actions={ data.actions } isLoading={ isLoading } + paginationInfo={ data.pagination } + defaultLayouts={ data.defaultLayouts } + selection={ data.selection } + onChangeSelection={ data.onSelectionChange } + header={ data.header } /> { dataviewsWrapper && ReactDOM.createPortal( diff --git a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces.ts b/client/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces.ts index 2825c947a60b63..628d6368cb103f 100644 --- a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces.ts +++ b/client/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces.ts @@ -1,47 +1,21 @@ -import { ReactNode } from 'react'; +import type { View, Field, Action, SortDirection } from '@wordpress/dataviews'; +import type { ReactNode } from 'react'; export interface ItemsDataViewsType< T > { items: T[] | undefined; pagination: DataViewsPaginationInfo; enableSearch?: boolean; searchLabel?: string; - fields: DataViewsColumn[]; - actions?: DataViewsAction[]; + fields: Field< T >[]; + actions?: Action< T >[]; getItemId?: ( item: T ) => string; itemFieldId?: string; // The field path to get the item id. Examples `id` or `site.blog_id` setDataViewsState: ( callback: ( prevState: DataViewsState ) => DataViewsState ) => void; dataViewsState: DataViewsState; - onSelectionChange?: ( item: T[] ) => void; -} - -export interface DataViewsColumn { - id: string; - enableHiding?: boolean; - enableSorting?: boolean; - elements?: { - value: number; - label: string; - }[]; - filterBy?: { - operators: string[]; - isPrimary?: boolean; - }; - type?: string; - header: ReactNode; - getValue?: ( item: any ) => string | boolean | number | undefined; - render?: ( item: any ) => ReactNode | null; -} - -export interface DataViewsAction { - id: string; - label: string; - isPrimary?: boolean; - icon?: string; - isEligible?: ( record: any ) => boolean; - isDestructive?: boolean; - callback?: () => void; - RenderModal?: ReactNode; - hideModalHeader?: boolean; + selection?: string[]; + onSelectionChange?: ( items: string[] ) => void; + defaultLayouts?: any; // TODO: improve this type + header?: ReactNode; } export interface DataViewsPaginationInfo { @@ -51,23 +25,10 @@ export interface DataViewsPaginationInfo { export interface DataViewsSort { field: string; - direction: 'asc' | 'desc' | ''; + direction: SortDirection; } -export interface DataViewsFilter { - field: string; - operator: string; - value: number; -} - -export interface DataViewsState { - type: 'table' | 'list' | 'grid'; - search: string; - filters: DataViewsFilter[]; - perPage: number; - page: number; - sort: DataViewsSort; - hiddenFields?: string[]; - layout: object; +export type DataViewsState = View & { selectedItem?: any | undefined; -} + layout?: any; // TODO: improve this type. +}; diff --git a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/site-sort/index.tsx b/client/a8c-for-agencies/components/items-dashboard/items-dataviews/site-sort/index.tsx deleted file mode 100644 index c511c47daacb5a..00000000000000 --- a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/site-sort/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Icon } from '@wordpress/icons'; -import clsx from 'clsx'; -import { useContext } from 'react'; -// todo: Extract the context -import SitesDashboardContext from 'calypso/a8c-for-agencies/sections/sites/sites-dashboard-context'; -// todo: Copy the sort icons from Jetpack Cloud -import { - defaultSortIcon, - ascendingSortIcon, - descendingSortIcon, -} from 'calypso/jetpack-cloud/sections/agency-dashboard/icons'; - -import './style.scss'; - -const SORT_DIRECTION_ASC = 'asc'; -const SORT_DIRECTION_DESC = 'desc'; - -// Mapping the columns to the site data keys -const SITE_COLUMN_KEY_MAP: { [ key: string ]: string } = { - site: 'url', -}; - -export default function SiteSort( { - columnKey, - isLargeScreen, - children, - isSortable, -}: { - columnKey: string; - isLargeScreen?: boolean; - children?: React.ReactNode; - isSortable?: boolean; -} ) { - const { dataViewsState, setDataViewsState } = useContext( SitesDashboardContext ); - - const { field, direction } = dataViewsState.sort; - - const isDefault = field !== SITE_COLUMN_KEY_MAP?.[ columnKey ] || ! field || ! direction; - - const setSort = () => { - const updatedSort = { ...dataViewsState.sort }; - if ( isDefault ) { - updatedSort.field = SITE_COLUMN_KEY_MAP?.[ columnKey ]; - updatedSort.direction = SORT_DIRECTION_ASC; - } else if ( direction === SORT_DIRECTION_ASC ) { - updatedSort.direction = SORT_DIRECTION_DESC; - } else if ( direction === SORT_DIRECTION_DESC ) { - updatedSort.field = ''; - updatedSort.direction = ''; - } - - setDataViewsState( ( sitesViewState ) => ( { - ...sitesViewState, - sort: updatedSort, - } ) ); - }; - - const getSortIcon = () => { - if ( isDefault ) { - return defaultSortIcon; - } else if ( direction === SORT_DIRECTION_ASC ) { - return ascendingSortIcon; - } else if ( direction === SORT_DIRECTION_DESC ) { - return descendingSortIcon; - } - return defaultSortIcon; - }; - - if ( ! isSortable ) { - return { children }; - } - - const handleOnKeyDown = ( event: React.KeyboardEvent< HTMLDivElement > ) => { - if ( event.key === 'Enter' || event.key === ' ' ) { - setSort(); - } - }; - - return ( - - { children } - { isSortable && ( - - ) } - - ); -} diff --git a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/site-sort/style.scss b/client/a8c-for-agencies/components/items-dashboard/items-dataviews/site-sort/style.scss deleted file mode 100644 index d54e0243a2cb8c..00000000000000 --- a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/site-sort/style.scss +++ /dev/null @@ -1,43 +0,0 @@ -.site-sort__icon { - position: relative; - inset-block-start: 3px; - inset-inline-start: 8px; -} - -.site-sort__icon-large_screen { - &.site-sort { - position: relative; - width: 100%; - height: 100%; - display: inline-flex; - align-items: center; - margin-inline-start: -16px; - padding-inline: 16px; - } - - .site-sort__icon { - position: absolute; - inset-inline-end: 16px; - inset-block-start: unset; - inset-inline-start: unset; - } -} - -.site-sort__icon-hidden { - visibility: hidden; -} - -.site-sort__clickable { - cursor: pointer; - // Inline block to ensure focus outline is rectangular. - display: inline-block; - // 5px right padding so focus border doesnt overlap icon. - padding-right: 5px; - border-radius: 2px; - .accessible-focus &:focus { - outline-style: solid; - outline-color: var(--color-primary-light); - outline-width: 2px; - outline-offset: 2px; - } -} diff --git a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/style.scss b/client/a8c-for-agencies/components/items-dashboard/items-dataviews/style.scss index a948592130a18c..7753fd32a54b42 100644 --- a/client/a8c-for-agencies/components/items-dashboard/items-dataviews/style.scss +++ b/client/a8c-for-agencies/components/items-dashboard/items-dataviews/style.scss @@ -22,6 +22,7 @@ justify-content: space-between !important; padding: 12px 16px 12px 16px; margin-top: auto; + z-index: 1; .components-input-control__backdrop { border-color: var(--Gray-Gray-5, #dcdcde); @@ -35,3 +36,16 @@ bottom: 0; } } + +/* + * This is a hotfix for the the button disabled state. + * The button is getting a color that is almost identical to the active state. + * It needs to be looked at and fixed separately. + * See https://github.com/Automattic/wp-calypso/pull/93503#issuecomment-2304078241 + */ +.dataviews-wrapper .components-button.is-tertiary:visited, +.dataviews-wrapper .components-button.is-tertiary[disabled], +.dataviews-wrapper .components-button.is-tertiary:disabled, +.dataviews-wrapper .components-button.is-tertiary.disabled { + color: #949494; // Same as core https://github.com/WordPress/gutenberg/blob/2cbba93a29600f09f6f95c09f690576b90e79e9f/packages/components/src/button/style.scss#L125 +} diff --git a/client/a8c-for-agencies/components/page-placeholder/index.tsx b/client/a8c-for-agencies/components/page-placeholder/index.tsx new file mode 100644 index 00000000000000..f6ed62ddd3e63e --- /dev/null +++ b/client/a8c-for-agencies/components/page-placeholder/index.tsx @@ -0,0 +1,35 @@ +import clsx from 'clsx'; +import Layout from 'calypso/a8c-for-agencies/components/layout'; +import LayoutBody from 'calypso/a8c-for-agencies/components/layout/body'; +import LayoutHeader, { + LayoutHeaderTitle as Title, +} from 'calypso/a8c-for-agencies/components/layout/header'; +import LayoutTop from 'calypso/a8c-for-agencies/components/layout/top'; + +import './style.scss'; + +type Props = { + title?: string; + className?: string; +}; + +export default function PagePlaceholder( { title, className }: Props ) { + return ( + + + + + <div className="a4a-page-placeholder__title-placeholder"></div> + + + + +
+
+
+
+
+
+
+ ); +} diff --git a/client/a8c-for-agencies/sections/landing/style.scss b/client/a8c-for-agencies/components/page-placeholder/style.scss similarity index 64% rename from client/a8c-for-agencies/sections/landing/style.scss rename to client/a8c-for-agencies/components/page-placeholder/style.scss index 83e1b6bdfa9669..fdc55a94783aa2 100644 --- a/client/a8c-for-agencies/sections/landing/style.scss +++ b/client/a8c-for-agencies/components/page-placeholder/style.scss @@ -3,20 +3,20 @@ background: var(--color-neutral-10); } -.a4a-landing__title-placeholder { +.a4a-page-placeholder__title-placeholder { @include loading-effect; width: 300px; height: 43px; } -.a4a-landing__section-placeholder { +.a4a-page-placeholder__section-placeholder { display: flex; flex-direction: column; gap: 16px; } -.a4a-landing__section-placeholder-title { +.a4a-page-placeholder__section-placeholder-title { @include loading-effect; width: 100%; @@ -24,14 +24,14 @@ } -.a4a-landing__section-placeholder-body { +.a4a-page-placeholder__section-placeholder-body { @include loading-effect; width: 100%; height: 200px; } -.a4a-landing__section-placeholder-footer { +.a4a-page-placeholder__section-placeholder-footer { @include loading-effect; width: 20%; diff --git a/client/a8c-for-agencies/data/team/use-activate-member.ts b/client/a8c-for-agencies/data/team/use-activate-member.ts index 7e80f12be1c06d..36d5df3a7e420c 100644 --- a/client/a8c-for-agencies/data/team/use-activate-member.ts +++ b/client/a8c-for-agencies/data/team/use-activate-member.ts @@ -1,10 +1,15 @@ import { useMutation, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; import wpcom from 'calypso/lib/wp'; +import { Agency } from 'calypso/state/a8c-for-agencies/types'; -interface APIError { +export interface APIError { status: number; code: string | null; message: string; + data?: { + user_agencies?: Agency[]; + target_agency?: Agency; + }; } interface Params { diff --git a/client/a8c-for-agencies/data/team/use-fetch-active-members.ts b/client/a8c-for-agencies/data/team/use-fetch-active-members.ts index ebe276486920a0..86340dd47cd6a0 100644 --- a/client/a8c-for-agencies/data/team/use-fetch-active-members.ts +++ b/client/a8c-for-agencies/data/team/use-fetch-active-members.ts @@ -10,6 +10,7 @@ type MemberAPIResponse = { email: string; avatar_url: string; role: string; + joined_timestamp: string; }; export default function useFetchActiveMembers(): UseQueryResult< TeamMember[], unknown > { @@ -30,6 +31,7 @@ export default function useFetchActiveMembers(): UseQueryResult< TeamMember[], u avatar: member.avatar_url, role: member.role, status: 'active', + dateAdded: member.joined_timestamp, } ) ); }, enabled: !! agencyId, diff --git a/client/a8c-for-agencies/data/team/use-remove-member.ts b/client/a8c-for-agencies/data/team/use-remove-member.ts new file mode 100644 index 00000000000000..97eabef755eb42 --- /dev/null +++ b/client/a8c-for-agencies/data/team/use-remove-member.ts @@ -0,0 +1,41 @@ +import { useMutation, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import wpcom from 'calypso/lib/wp'; +import { useSelector } from 'calypso/state'; +import { getActiveAgencyId } from 'calypso/state/a8c-for-agencies/agency/selectors'; + +interface APIError { + status: number; + code: string | null; + message: string; +} + +export interface Params { + id: number; +} + +interface APIResponse { + success: boolean; +} + +function removeMemberMutation( params: Params, agencyId?: number ): Promise< APIResponse > { + if ( ! agencyId ) { + throw new Error( 'Agency ID is required to remove a team member' ); + } + + return wpcom.req.post( { + apiNamespace: 'wpcom/v2', + path: `/agency/${ agencyId }/users/${ params.id }`, + method: 'DELETE', + } ); +} + +export default function useRemoveMemberMutation< TContext = unknown >( + options?: UseMutationOptions< APIResponse, APIError, Params, TContext > +): UseMutationResult< APIResponse, APIError, Params, TContext > { + const agencyId = useSelector( getActiveAgencyId ); + + return useMutation< APIResponse, APIError, Params, TContext >( { + ...options, + mutationFn: ( args ) => removeMemberMutation( args, agencyId ), + } ); +} diff --git a/client/a8c-for-agencies/hooks/use-no-active-site.ts b/client/a8c-for-agencies/hooks/use-no-active-site.ts index d8bd5d90322019..b17668ddd6f104 100644 --- a/client/a8c-for-agencies/hooks/use-no-active-site.ts +++ b/client/a8c-for-agencies/hooks/use-no-active-site.ts @@ -14,10 +14,6 @@ export default function useNoActiveSite() { showOnlyFavorites: false, showOnlyDevelopmentSites: false, }, - sort: { - field: '', - direction: '', - }, agencyId, } ); diff --git a/client/a8c-for-agencies/hooks/use-wp-admin-access-control.ts b/client/a8c-for-agencies/hooks/use-wp-admin-access-control.ts new file mode 100644 index 00000000000000..1825362f01c92e --- /dev/null +++ b/client/a8c-for-agencies/hooks/use-wp-admin-access-control.ts @@ -0,0 +1,20 @@ +import isA8CForAgencies from 'calypso/lib/a8c-for-agencies/is-a8c-for-agencies'; +import { useSelector } from 'calypso/state'; +import { getActiveAgency } from 'calypso/state/a8c-for-agencies/agency/selectors'; +import type { AppState } from 'calypso/types'; + +export default function useWPAdminAccessControl( { siteId }: { siteId: number } ) { + const agency = useSelector( getActiveAgency ); + + const role = agency?.user?.role; + + const userCapabilities = useSelector( ( state: AppState ) => + siteId ? state?.currentUser?.capabilities?.[ siteId ] : [] + ); + + const hasCapability = userCapabilities?.[ 'manage_options' ]; + + return { + noWPAdminAccess: isA8CForAgencies() && role === 'a4a_manager' && ! hasCapability, + }; +} diff --git a/client/a8c-for-agencies/lib/permission.ts b/client/a8c-for-agencies/lib/permission.ts index 9bce8e5215b577..0fe5f593f9d34f 100644 --- a/client/a8c-for-agencies/lib/permission.ts +++ b/client/a8c-for-agencies/lib/permission.ts @@ -30,13 +30,13 @@ import { A4A_REFERRALS_FAQ, A4A_PARTNER_DIRECTORY_LINK, A4A_PURCHASES_LINK, - A4A_LICENSES_LINK, - A4A_UNASSIGNED_LICENSES_LINK, A4A_BILLING_LINK, A4A_INVOICES_LINK, A4A_PAYMENT_METHODS_LINK, A4A_PAYMENT_METHODS_ADD_LINK, A4A_MIGRATIONS_LINK, + A4A_TEAM_LINK, + A4A_TEAM_INVITE_LINK, } from '../components/sidebar-menu/lib/constants'; import type { Agency } from 'calypso/state/a8c-for-agencies/types'; @@ -72,13 +72,25 @@ const MEMBER_ACCESSIBLE_PATHS: Record< string, string[] > = { [ A4A_PARTNER_DIRECTORY_AGENCY_DETAILS_LINK ]: [ 'a4a_read_partner_directory' ], [ A4A_PARTNER_DIRECTORY_AGENCY_EXPERTISE_LINK ]: [ 'a4a_read_partner_directory' ], [ A4A_PURCHASES_LINK ]: [ 'a4a_jetpack_licensing' ], - [ A4A_LICENSES_LINK ]: [ 'a4a_jetpack_licensing' ], - [ A4A_UNASSIGNED_LICENSES_LINK ]: [ 'a4a_jetpack_licensing' ], [ A4A_BILLING_LINK ]: [ 'a4a_jetpack_licensing' ], [ A4A_INVOICES_LINK ]: [ 'a4a_jetpack_licensing' ], [ A4A_PAYMENT_METHODS_LINK ]: [ 'a4a_jetpack_licensing' ], [ A4A_PAYMENT_METHODS_ADD_LINK ]: [ 'a4a_jetpack_licensing' ], [ A4A_MIGRATIONS_LINK ]: [ 'a4a_read_migrations' ], + [ A4A_TEAM_LINK ]: [ 'a4a_read_users' ], + [ A4A_TEAM_INVITE_LINK ]: [ 'a4a_edit_user_invites' ], +}; + +const MEMBER_ACCESSIBLE_DYNAMIC_PATHS: Record< string, string[] > = { + 'sites-overview': [ 'a4a_read_managed_sites' ], + marketplace: [ 'a4a_read_marketplace' ], + licenses: [ 'a4a_jetpack_licensing' ], +}; + +const DYNAMIC_PATH_PATTERNS: Record< string, RegExp > = { + 'sites-overview': /^\/sites\/overview\/[^/]+(\/.*)?$/, + marketplace: /^\/marketplace\/[^/]+\/[^/]+(\/.*)?$/, + licenses: /^\/purchases\/licenses(\/.*)?$/, }; export const isPathAllowed = ( pathname: string, agency: Agency | null ) => { @@ -103,8 +115,20 @@ export const isPathAllowed = ( pathname: string, agency: Agency | null ) => { const capabilities = agency?.user?.capabilities; if ( capabilities ) { const permissions = MEMBER_ACCESSIBLE_PATHS?.[ pathname ]; - return permissions - ? capabilities.some( ( capability: string ) => permissions.includes( capability ) ) - : false; + if ( permissions ) { + return capabilities.some( ( capability: string ) => permissions?.includes( capability ) ); + } + + // Check dynamic path patterns + for ( const [ key, pattern ] of Object.entries( DYNAMIC_PATH_PATTERNS ) ) { + if ( pattern.test( pathname ) ) { + const dynamicPermissions = MEMBER_ACCESSIBLE_DYNAMIC_PATHS[ key ]; + return capabilities.some( + ( capability: string ) => dynamicPermissions?.includes( capability ) + ); + } + } } + + return false; }; diff --git a/client/a8c-for-agencies/sections/client/cancel-subscription-confirmation-dialog/index.tsx b/client/a8c-for-agencies/sections/client/cancel-subscription-confirmation-dialog/index.tsx index 0cd1796bad6e95..66e28c7a6c1bca 100644 --- a/client/a8c-for-agencies/sections/client/cancel-subscription-confirmation-dialog/index.tsx +++ b/client/a8c-for-agencies/sections/client/cancel-subscription-confirmation-dialog/index.tsx @@ -1,6 +1,7 @@ -import { Button, Dialog } from '@automattic/components'; +import { Button } from '@automattic/components'; import { useTranslate } from 'i18n-calypso'; import { useState } from 'react'; +import { A4AConfirmationDialog } from 'calypso/a8c-for-agencies/components/a4a-confirmation-dialog'; import TextPlaceholder from 'calypso/a8c-for-agencies/components/text-placeholder'; import useCancelClientSubscription from 'calypso/a8c-for-agencies/data/client/use-cancel-client-subscription'; import useFetchClientProducts from 'calypso/a8c-for-agencies/data/client/use-fetch-client-products'; @@ -20,7 +21,7 @@ export default function CancelSubscriptionAction( { subscription, onCancelSubscr const [ isVisible, setIsVisible ] = useState( false ); - const { data: products, isFetching: isFetchingProductInfo } = useFetchClientProducts(); + const { data: products, isFetching: isFetchingProductInfo } = useFetchClientProducts( false ); const { mutate: cancelSubscription, isPending } = useCancelClientSubscription( { onSuccess: () => { @@ -59,47 +60,35 @@ export default function CancelSubscriptionAction( { subscription, onCancelSubscr { translate( 'Cancel the subscription' ) } - - { translate( 'Keep the subscription' ) } - , - , - ] } - shouldCloseOnEsc - onClose={ handleClose } - > -

- { translate( 'Are you sure you want to cancel this subscription?' ) } -

- { isFetchingProductInfo ? ( - - ) : ( - translate( - '{{b}}%(productName)s{{/b}} will stop recommending products to your customers. This action cannot be undone.', - { - args: { - productName, - }, - components: { - b: , - }, - comment: - '%(productName)s is the name of the product that the user is about to cancel.', - } - ) - ) } -
+ { isVisible && ( + + { isFetchingProductInfo ? ( + + ) : ( + translate( + '{{b}}%(productName)s{{/b}} will stop recommending products to your customers. This action cannot be undone.', + { + args: { + productName, + }, + components: { + b: , + }, + comment: + '%(productName)s is the name of the product that the user is about to cancel.', + } + ) + ) } + + ) } ); } diff --git a/client/a8c-for-agencies/sections/client/primary/subscriptions-list/index.tsx b/client/a8c-for-agencies/sections/client/primary/subscriptions-list/index.tsx index bd2c0adf2b86c2..4726a9318a72ce 100644 --- a/client/a8c-for-agencies/sections/client/primary/subscriptions-list/index.tsx +++ b/client/a8c-for-agencies/sections/client/primary/subscriptions-list/index.tsx @@ -1,4 +1,5 @@ import { useDesktopBreakpoint } from '@automattic/viewport-react'; +import { filterSortAndPaginate } from '@wordpress/dataviews'; import clsx from 'clsx'; import { useTranslate } from 'i18n-calypso'; import { useMemo, ReactNode, useState, useCallback } from 'react'; @@ -44,7 +45,7 @@ export default function SubscriptionsList() { () => [ { id: 'purchase', - header: translate( 'Purchase' ).toUpperCase(), + label: translate( 'Purchase' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: Subscription } ): ReactNode => { const product = products?.find( ( product ) => product.product_id === item.product_id ); @@ -55,7 +56,7 @@ export default function SubscriptionsList() { }, { id: 'price', - header: translate( 'Price' ).toUpperCase(), + label: translate( 'Price' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: Subscription } ): ReactNode => { const product = products?.find( ( product ) => product.product_id === item.product_id ); @@ -66,7 +67,7 @@ export default function SubscriptionsList() { }, { id: 'subscription-status', - header: translate( 'Subscription Status' ).toUpperCase(), + label: translate( 'Subscription Status' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: Subscription } ): ReactNode => { return ; @@ -76,7 +77,7 @@ export default function SubscriptionsList() { }, { id: 'actions', - header: translate( 'Actions' ).toUpperCase(), + label: translate( 'Actions' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: Subscription } ): ReactNode => { return ( @@ -92,6 +93,9 @@ export default function SubscriptionsList() { ], [ isFetchingProducts, onCancelSubscription, products, translate ] ); + const { data: items, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( data ?? [], dataViewsState, fields ); + }, [ data, dataViewsState, fields ] ); return ( diff --git a/client/a8c-for-agencies/sections/landing/landing.tsx b/client/a8c-for-agencies/sections/landing/landing.tsx index 2b4d763ec1a4da..551df76342c846 100644 --- a/client/a8c-for-agencies/sections/landing/landing.tsx +++ b/client/a8c-for-agencies/sections/landing/landing.tsx @@ -2,12 +2,7 @@ import page from '@automattic/calypso-router'; import { addQueryArgs, getQueryArg, getQueryArgs } from '@wordpress/url'; import { useTranslate } from 'i18n-calypso'; import { useEffect } from 'react'; -import Layout from 'calypso/a8c-for-agencies/components/layout'; -import LayoutBody from 'calypso/a8c-for-agencies/components/layout/body'; -import LayoutHeader, { - LayoutHeaderTitle as Title, -} from 'calypso/a8c-for-agencies/components/layout/header'; -import LayoutTop from 'calypso/a8c-for-agencies/components/layout/top'; +import PagePlaceholder from 'calypso/a8c-for-agencies/components/page-placeholder'; import { A4A_OVERVIEW_LINK, A4A_SIGNUP_LINK, @@ -20,8 +15,6 @@ import { isAgencyClientUser, } from 'calypso/state/a8c-for-agencies/agency/selectors'; -import './style.scss'; - /** * Redirect with Current Query * Adds all of the current location's query parameters to the provided URL before redirecting. @@ -62,22 +55,5 @@ export default function Landing() { redirectWithCurrentQuery( A4A_SIGNUP_LINK ); }, [ agency, hasFetched, isClientUser ] ); - return ( - - - - - <div className="a4a-landing__title-placeholder"></div> - - - - -
-
-
-
-
-
-
- ); + return ; } diff --git a/client/a8c-for-agencies/sections/marketplace/controller.tsx b/client/a8c-for-agencies/sections/marketplace/controller.tsx index 4fae59602d8e2d..6e9512ec16c7cd 100644 --- a/client/a8c-for-agencies/sections/marketplace/controller.tsx +++ b/client/a8c-for-agencies/sections/marketplace/controller.tsx @@ -9,7 +9,7 @@ import { import MarketplaceSidebar from '../../components/sidebar-menu/marketplace'; import AssignLicense from './assign-license'; import Checkout from './checkout'; -import { MARKETPLACE_TYPE_REFERRAL, MARKETPLACE_TYPE_REGULAR } from './hoc/with-marketplace-type'; +import { MARKETPLACE_TYPE_REFERRAL } from './hoc/with-marketplace-type'; import HostingOverview from './hosting-overview'; import { getValidHostingSection } from './lib/hosting'; import { getValidBrand } from './lib/product-brand'; @@ -98,9 +98,7 @@ export const checkoutContext: Callback = ( context, next ) => { ); diff --git a/client/a8c-for-agencies/sections/marketplace/lib/hosting.tsx b/client/a8c-for-agencies/sections/marketplace/lib/hosting.tsx index 4b66c7f46feb66..c20ed4b1b3bf1a 100644 --- a/client/a8c-for-agencies/sections/marketplace/lib/hosting.tsx +++ b/client/a8c-for-agencies/sections/marketplace/lib/hosting.tsx @@ -82,7 +82,7 @@ export function getHostingLogo( slug: string, showText = true ) { */ export function isPressableHostingProduct( keyOrSlug: string ) { return ( - keyOrSlug.startsWith( 'pressable-wp' ) || + keyOrSlug.startsWith( 'pressable-' ) || keyOrSlug.startsWith( 'pressable-hosting' ) || keyOrSlug.startsWith( 'jetpack-pressable' ) ); diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/hooks/use-existing-pressable-plan.ts b/client/a8c-for-agencies/sections/marketplace/pressable-overview/hooks/use-existing-pressable-plan.ts index 3b9aa5348ed06b..0dcd792d62714e 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/hooks/use-existing-pressable-plan.ts +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/hooks/use-existing-pressable-plan.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import useFetchLicenseCounts from 'calypso/a8c-for-agencies/data/purchases/use-fetch-license-counts'; import { APIProductFamilyProduct } from 'calypso/state/partner-portal/types'; +import getPressablePlan from '../lib/get-pressable-plan'; type Props = { plans: APIProductFamilyProduct[]; @@ -11,7 +12,7 @@ export default function useExistingPressablePlan( { plans }: Props ) { return useMemo( () => { const pressablePlans = Object.keys( data?.products ?? {} ).filter( ( slug ) => - slug.startsWith( 'pressable-wp' ) + slug.startsWith( 'pressable-' ) ); const existingPlan = pressablePlans.find( ( slug ) => { @@ -20,6 +21,7 @@ export default function useExistingPressablePlan( { plans }: Props ) { return { existingPlan: plans.find( ( plan ) => plan.slug === existingPlan ) ?? null, + pressablePlan: existingPlan ? getPressablePlan( existingPlan ) : null, isReady, }; }, [ data?.products, isReady, plans ] ); diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-pressable-plan.ts b/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-pressable-plan.ts index 14ef13670ac95b..78e2cf203e5e00 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-pressable-plan.ts +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/lib/get-pressable-plan.ts @@ -48,6 +48,68 @@ const PLAN_DATA: Record< string, PressablePlan > = { visits: 2000000, storage: 500, }, + + // New pressable plans + 'pressable-build': { + slug: 'pressable-build', + install: 1, + visits: 30000, + storage: 20, + }, + 'pressable-growth': { + slug: 'pressable-growth', + install: 3, + visits: 50000, + storage: 30, + }, + 'pressable-advanced': { + slug: 'pressable-advanced', + install: 5, + visits: 75000, + storage: 35, + }, + 'pressable-pro': { + slug: 'pressable-pro', + install: 10, + visits: 150000, + storage: 50, + }, + 'pressable-premium': { + slug: 'pressable-premium', + install: 20, + visits: 400000, + storage: 80, + }, + 'pressable-business': { + slug: 'pressable-business', + install: 50, + visits: 1000000, + storage: 200, + }, + 'pressable-business-80': { + slug: 'pressable-business-80', + install: 80, + visits: 1600000, + storage: 275, + }, + 'pressable-business-100': { + slug: 'pressable-business-100', + install: 100, + visits: 2000000, + storage: 325, + }, + 'pressable-business-120': { + slug: 'pressable-business-120', + install: 120, + visits: 2400000, + storage: 375, + }, + 'pressable-business-150': { + slug: 'pressable-business-150', + install: 150, + visits: 3000000, + storage: 450, + }, }; export default function getPressablePlan( slug: string ) { diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/filter.tsx b/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/filter.tsx index 6fafa93a03ab82..7b6d378dee0ce5 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/filter.tsx +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/filter.tsx @@ -8,7 +8,7 @@ import { useDispatch } from 'calypso/state'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; import { APIProductFamilyProduct } from 'calypso/state/partner-portal/types'; import { FILTER_TYPE_INSTALL, FILTER_TYPE_VISITS } from '../constants'; -import getPressablePlan from '../lib/get-pressable-plan'; +import getPressablePlan, { PressablePlan } from '../lib/get-pressable-plan'; import getSliderOptions from '../lib/get-slider-options'; import { FilterType } from '../types'; @@ -16,6 +16,7 @@ type Props = { selectedPlan: APIProductFamilyProduct | null; plans: APIProductFamilyProduct[]; existingPlan?: APIProductFamilyProduct | null; + pressablePlan?: PressablePlan | null; onSelectPlan: ( plan: APIProductFamilyProduct | null ) => void; isLoading?: boolean; }; @@ -25,6 +26,7 @@ export default function PlanSelectionFilter( { plans, onSelectPlan, existingPlan, + pressablePlan, isLoading, }: Props ) { const translate = useTranslate(); @@ -83,9 +85,22 @@ export default function PlanSelectionFilter( { : 'a4a-pressable-filter-wrapper-visits'; const wrapperClass = clsx( additionalWrapperClass, 'pressable-overview-plan-selection__filter' ); - const minimum = existingPlan - ? options.findIndex( ( { value } ) => value === existingPlan.slug ) + 1 - : 0; + const minimum = useMemo( () => { + if ( ! pressablePlan ) { + return 0; + } + + const allAvailablePlans = plans + .map( ( plan ) => getPressablePlan( plan.slug ) ) + .sort( ( a, b ) => a.install - b.install ); + + for ( let i = 0; i < allAvailablePlans.length; i++ ) { + if ( pressablePlan.install < allAvailablePlans[ i ].install ) { + return i; + } + } + return allAvailablePlans.length; + }, [ plans, pressablePlan ] ); if ( isLoading ) { return ( diff --git a/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/index.tsx b/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/index.tsx index 79f3103fe3f4a2..424d60382ede37 100644 --- a/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/index.tsx +++ b/client/a8c-for-agencies/sections/marketplace/pressable-overview/plan-selection/index.tsx @@ -40,7 +40,11 @@ export default function PressableOverviewPlanSelection( { onAddToCart }: Props ) productSearchQuery: '', } ); - const { existingPlan, isReady: isExistingPlanFetched } = useExistingPressablePlan( { + const { + existingPlan, + pressablePlan, + isReady: isExistingPlanFetched, + } = useExistingPressablePlan( { plans: pressablePlans, } ); @@ -82,6 +86,7 @@ export default function PressableOverviewPlanSelection( { onAddToCart }: Props ) plans={ pressablePlans } onSelectPlan={ onSelectPlan } existingPlan={ existingPlan } + pressablePlan={ pressablePlan } isLoading={ ! isExistingPlanFetched } /> ) } diff --git a/client/a8c-for-agencies/sections/marketplace/products-overview/hooks/use-issue-and-assign-licenses.tsx b/client/a8c-for-agencies/sections/marketplace/products-overview/hooks/use-issue-and-assign-licenses.tsx index 25d2e9d70be4e4..5f3f6d20988acc 100644 --- a/client/a8c-for-agencies/sections/marketplace/products-overview/hooks/use-issue-and-assign-licenses.tsx +++ b/client/a8c-for-agencies/sections/marketplace/products-overview/hooks/use-issue-and-assign-licenses.tsx @@ -38,7 +38,7 @@ const useGetLicenseIssuedMessage = () => { const productName = products?.data?.find?.( ( p ) => p.slug === licenses[ 0 ].slug )?.name ?? ''; - if ( licenses[ 0 ].slug.startsWith( 'pressable-wp' ) ) { + if ( licenses[ 0 ].slug.startsWith( 'pressable-' ) ) { return translate( 'Thanks for your purchase! Below you can view and manage your new {{strong}}%(productName)s{{/strong}}', { diff --git a/client/a8c-for-agencies/sections/partner-directory/agency-details/index.tsx b/client/a8c-for-agencies/sections/partner-directory/agency-details/index.tsx index e71338645409b1..67745830ae072b 100644 --- a/client/a8c-for-agencies/sections/partner-directory/agency-details/index.tsx +++ b/client/a8c-for-agencies/sections/partner-directory/agency-details/index.tsx @@ -228,6 +228,17 @@ const AgencyDetailsForm = ( { initialFormData }: Props ) => { + ), + }, + } ) } sub={ translate( 'Upload your agency logo sized at 800px by 320px. Format allowed: JPG, PNG' ) } diff --git a/client/a8c-for-agencies/sections/partner-directory/agency-details/logo-picker.tsx b/client/a8c-for-agencies/sections/partner-directory/agency-details/logo-picker.tsx index f2db01a1900ad0..3a6c06ea013111 100644 --- a/client/a8c-for-agencies/sections/partner-directory/agency-details/logo-picker.tsx +++ b/client/a8c-for-agencies/sections/partner-directory/agency-details/logo-picker.tsx @@ -4,6 +4,7 @@ import A4AImagePicker from 'calypso/a8c-for-agencies/components/a4a-image-picker const LOGO_SIZE_WIDTH = 800; const LOGO_SIZE_HEIGHT = 320; +const LOGO_SIZE_TOLERANCE = 5; type Props = { logo?: string | null; @@ -37,7 +38,11 @@ const LogoPicker = ( { logo, onPick }: Props ) => { setError( null ); getImage( file ).then( ( img ) => { - if ( img.width !== LOGO_SIZE_WIDTH || img.height !== LOGO_SIZE_HEIGHT ) { + // Check against the allowed deviation in pixels from the required logo dimensions + const isWidthValid = Math.abs( img.width - LOGO_SIZE_WIDTH ) <= LOGO_SIZE_TOLERANCE; + const isHeightValid = Math.abs( img.height - LOGO_SIZE_HEIGHT ) <= LOGO_SIZE_TOLERANCE; + + if ( ! isWidthValid || ! isHeightValid ) { setError( translate( 'Company logo must have 800px width and 320px height.' ) ); return; } diff --git a/client/a8c-for-agencies/sections/partner-directory/dashboard/index.tsx b/client/a8c-for-agencies/sections/partner-directory/dashboard/index.tsx index 7108d6c57607a8..7fc3bbd15445ce 100644 --- a/client/a8c-for-agencies/sections/partner-directory/dashboard/index.tsx +++ b/client/a8c-for-agencies/sections/partner-directory/dashboard/index.tsx @@ -294,11 +294,8 @@ const PartnerDirectoryDashboard = () => {
{ translate( - 'Thank you! You’ll be notified when the partner directory is live.', - 'Thank you! You’ll be notified when the partner directories are live.', - // todo: Once the partner directory are live use the copy below: - //'Congratulations! Your agency is now listed in our partner directory.', - //'Congratulations! Your agency is now listed in our partner directories.', + 'Congratulations! Your agency is now listed in our partner directory.', + 'Congratulations! Your agency is now listed in our partner directories.', { count: directoryApplicationStatuses.filter( ( { key } ) => key === 'approved' ) .length, diff --git a/client/a8c-for-agencies/sections/partner-directory/lib/get-brand-meta.tsx b/client/a8c-for-agencies/sections/partner-directory/lib/get-brand-meta.tsx index 7ca4821ca7a8ed..5f708e01c2f7a3 100644 --- a/client/a8c-for-agencies/sections/partner-directory/lib/get-brand-meta.tsx +++ b/client/a8c-for-agencies/sections/partner-directory/lib/get-brand-meta.tsx @@ -37,7 +37,7 @@ export const getBrandMeta = ( brand: string, agency?: Agency | null ): BrandMeta className: 'partner-directory-dashboard__woo-icon', url: 'https://woocommerce.com/development-services/', urlProfile: `https://woocommerce.com/development-services/${ agencySlug }/${ agencyId }`, - isAvailable: false, + isAvailable: true, }; case 'Pressable.com': return { @@ -45,7 +45,7 @@ export const getBrandMeta = ( brand: string, agency?: Agency | null ): BrandMeta icon: , url: 'https://pressable.com/development-services/', urlProfile: `https://pressable.com/development-services/${ agencySlug }/${ agencyId }`, - isAvailable: false, + isAvailable: true, }; case 'Jetpack.com': return { diff --git a/client/a8c-for-agencies/sections/purchases/licenses/license-details/actions.tsx b/client/a8c-for-agencies/sections/purchases/licenses/license-details/actions.tsx index f9518b06d16fce..ddc135fc01f208 100644 --- a/client/a8c-for-agencies/sections/purchases/licenses/license-details/actions.tsx +++ b/client/a8c-for-agencies/sections/purchases/licenses/license-details/actions.tsx @@ -9,7 +9,9 @@ import { LicenseType, } from 'calypso/jetpack-cloud/sections/partner-portal/types'; import { addQueryArgs } from 'calypso/lib/url'; -import { useDispatch } from 'calypso/state'; +import { useDispatch, useSelector } from 'calypso/state'; +import { hasAgencyCapability } from 'calypso/state/a8c-for-agencies/agency/selectors'; +import { A4AStore } from 'calypso/state/a8c-for-agencies/types'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; import { errorNotice } from 'calypso/state/notices/actions'; import RevokeLicenseDialog from '../revoke-license-dialog'; @@ -37,6 +39,10 @@ export default function LicenseDetailsActions( { const dispatch = useDispatch(); const translate = useTranslate(); + const canRevoke = useSelector( ( state: A4AStore ) => + hasAgencyCapability( state, 'a4a_revoke_licenses' ) + ); + const [ revokeDialog, setRevokeDialog ] = useState( false ); const isPressableLicense = isPressableHostingProduct( licenseKey ); const pressableManageUrl = 'https://my.pressable.com/agency/auth'; @@ -107,9 +113,10 @@ export default function LicenseDetailsActions( { ) } - { ( isChildLicense - ? licenseState === LicenseState.Attached - : licenseState !== LicenseState.Revoked ) && + { canRevoke && + ( isChildLicense + ? licenseState === LicenseState.Attached + : licenseState !== LicenseState.Revoked ) && licenseType === LicenseType.Partner && ( , - - , - ] } + -

{ title }

- { description } - +
); } diff --git a/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card-delete-dialog/index.tsx b/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card-delete-dialog/index.tsx index 6c7ffc08895c53..3e8aeaf9a627e7 100644 --- a/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card-delete-dialog/index.tsx +++ b/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card-delete-dialog/index.tsx @@ -1,6 +1,6 @@ -import { Button, Dialog } from '@automattic/components'; import { useTranslate } from 'i18n-calypso'; import { useContext, type FunctionComponent } from 'react'; +import { A4AConfirmationDialog } from 'calypso/a8c-for-agencies/components/a4a-confirmation-dialog'; import { PaymentMethodSummary } from 'calypso/lib/checkout/payment-methods'; import { PaymentMethodOverviewContext } from '../../context'; import useStoredCards from '../../hooks/use-stored-cards'; @@ -34,31 +34,21 @@ const StoredCreditCardDeleteDialog: FunctionComponent< Props > = ( { isFetching, } = useStoredCards( paging, true ); + if ( ! isVisible ) { + return null; + } + return ( - - { translate( 'Go back' ) } - , - - , - ] } + onConfirm={ onConfirm } + ctaLabel={ translate( 'Delete payment method' ) } + closeLabel={ translate( 'Go back' ) } + isLoading={ isDeleteInProgress } + isDestructive > -

- { translate( 'Delete payment method' ) } -

-

{ translate( 'The payment method {{paymentMethodSummary/}} will be removed from your account', @@ -87,7 +77,7 @@ const StoredCreditCardDeleteDialog: FunctionComponent< Props > = ( { isFetching={ isFetching } /> ) } -

+ ); }; diff --git a/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card-delete-dialog/style.scss b/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card-delete-dialog/style.scss index be12b0e91038e5..60162fba4ee7e8 100644 --- a/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card-delete-dialog/style.scss +++ b/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card-delete-dialog/style.scss @@ -10,41 +10,4 @@ font-size: 1rem; } } - - .dialog__action-buttons { - display: flex; - justify-content: flex-end; - align-items: center; - background: var(--color-neutral-0); - border: 0; - - a { - margin-right: auto; - font-weight: 600; - text-decoration: underline; - } - - button { - width: 100%; - - @include break-mobile { - width: auto; - } - } - } -} - -.stored-credit-card-delete-dialog__heading { - margin: -16px -16px 16px; - padding: 9px 16px; - font-size: 1.125rem; /* stylelint-disable-line scales/font-sizes */ - font-weight: 600; - line-height: 20px; - border-bottom: 1px solid var(--color-neutral-5); - - @include break-mobile() { - margin: -24px -24px 24px; - padding: 9px 24px; - line-height: 36px; - } } diff --git a/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card/credit-card-actions.tsx b/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card/credit-card-actions.tsx index c2c0744b00ef62..98e483bf0883a7 100644 --- a/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card/credit-card-actions.tsx +++ b/client/a8c-for-agencies/sections/purchases/payment-methods/payment-method-overview/stored-credit-card/credit-card-actions.tsx @@ -22,6 +22,8 @@ export default function CreditCardActions( { const [ isOpen, setIsOpen ] = useState( false ); const dispatch = useDispatch(); + const availableActions = cardActions.filter( ( action ) => action.isEnabled ); + const showActions = () => { setIsOpen( true ); dispatch( recordTracksEvent( 'calypso_a4a_payments_card_actions_button_click' ) ); @@ -31,6 +33,10 @@ export default function CreditCardActions( { setIsOpen( false ); }; + if ( availableActions.length === 0 ) { + return null; + } + return ( <>
), withIcon: false, + hideEnvDataInHeader: true, }; const isDesktop = useDesktopBreakpoint(); diff --git a/client/a8c-for-agencies/sections/referrals/referral-details/purchases.tsx b/client/a8c-for-agencies/sections/referrals/referral-details/purchases.tsx index 15159c54a51770..30628bab840cee 100644 --- a/client/a8c-for-agencies/sections/referrals/referral-details/purchases.tsx +++ b/client/a8c-for-agencies/sections/referrals/referral-details/purchases.tsx @@ -30,7 +30,7 @@ export default function ReferralPurchases( { purchases }: { purchases: ReferralP () => [ { id: 'product-details', - header: translate( 'Product Details' ).toUpperCase(), + label: translate( 'Product Details' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: ReferralPurchase } ): ReactNode => { return ; @@ -40,7 +40,7 @@ export default function ReferralPurchases( { purchases }: { purchases: ReferralP }, { id: 'assigned-to', - header: translate( 'Assigned to' ).toUpperCase(), + label: translate( 'Assigned to' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: ReferralPurchase } ): ReactNode => { return ( @@ -57,7 +57,7 @@ export default function ReferralPurchases( { purchases }: { purchases: ReferralP }, { id: 'date', - header: translate( 'Assigned on' ).toUpperCase(), + label: translate( 'Assigned on' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: ReferralPurchase } ): ReactNode => { return ; @@ -67,7 +67,7 @@ export default function ReferralPurchases( { purchases }: { purchases: ReferralP }, { id: 'total', - header: translate( 'Total' ).toUpperCase(), + label: translate( 'Total' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: ReferralPurchase } ): ReactNode => { return ; diff --git a/client/a8c-for-agencies/sections/referrals/referral-details/style.scss b/client/a8c-for-agencies/sections/referrals/referral-details/style.scss index 68943618d6c528..09126d0a2c54d9 100644 --- a/client/a8c-for-agencies/sections/referrals/referral-details/style.scss +++ b/client/a8c-for-agencies/sections/referrals/referral-details/style.scss @@ -29,3 +29,11 @@ width: auto; } } + +// TODO: remove when the list view declares a proper primary field. +// +// The issue here is that the email is not provided as a primary field, +// hence it's displayed below the space reserved for the media+primary field in list view. +.dataviews-view-list__media-wrapper { + display: none !important; +} diff --git a/client/a8c-for-agencies/sections/referrals/referrals-list/index.tsx b/client/a8c-for-agencies/sections/referrals/referrals-list/index.tsx index faf014892e6433..92e683be9f0caf 100644 --- a/client/a8c-for-agencies/sections/referrals/referrals-list/index.tsx +++ b/client/a8c-for-agencies/sections/referrals/referrals-list/index.tsx @@ -1,5 +1,6 @@ import { Button, Gridicon } from '@automattic/components'; import { useDesktopBreakpoint } from '@automattic/viewport-react'; +import { filterSortAndPaginate } from '@wordpress/dataviews'; import { useTranslate } from 'i18n-calypso'; import { useMemo, useCallback, ReactNode, useEffect } from 'react'; import { DATAVIEWS_LIST } from 'calypso/a8c-for-agencies/components/items-dashboard/constants'; @@ -7,9 +8,10 @@ import ItemsDataViews from 'calypso/a8c-for-agencies/components/items-dashboard/ import { DataViewsState } from 'calypso/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces'; import { useDispatch } from 'calypso/state'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; +import { Referral, ReferralInvoice } from '../types'; import CommissionsColumn from './commissions-column'; import SubscriptionStatus from './subscription-status'; -import type { Referral, ReferralInvoice } from '../types'; +import type { Field } from '@wordpress/dataviews'; import './style.scss'; @@ -42,14 +44,14 @@ export default function ReferralList( { [ dispatch, setDataViewsState ] ); - const fields = useMemo( + const fields: Field< any >[] = useMemo( () => dataViewsState.selectedItem || ! isDesktop ? [ // Show the client column as a button on mobile { id: 'client', - header: translate( 'Client' ).toUpperCase(), + label: translate( 'Client' ).toUpperCase(), getValue: () => '-', render: ( { item }: { item: Referral } ): ReactNode => ( , - - , - ] } + onConfirm={ onConfirm } + ctaLabel={ translate( 'Remove site' ) } + isLoading={ busy } + isDestructive > -

{ title }

- { translate( 'Are you sure you want to remove the site {{b}}%(siteName)s{{/b}} from the dashboard?', { @@ -50,6 +32,6 @@ export function SiteRemoveConfirmationDialog( { siteName, onConfirm, onClose, bu comment: '%(siteName)s is the site name', } ) } - + ); } diff --git a/client/a8c-for-agencies/sections/sites/site-remove-confirmation-dialog/style.scss b/client/a8c-for-agencies/sections/sites/site-remove-confirmation-dialog/style.scss deleted file mode 100644 index b0264bc8044457..00000000000000 --- a/client/a8c-for-agencies/sections/sites/site-remove-confirmation-dialog/style.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import "@wordpress/base-styles/breakpoints"; -@import "@wordpress/base-styles/mixins"; - -.site-remove-confirmation-dialog { - .site-remove-confirmation-dialog__heading { - padding-block-end: 16px; - @include a4a-font-heading-lg; - } -} diff --git a/client/a8c-for-agencies/sections/sites/site-sort/index.tsx b/client/a8c-for-agencies/sections/sites/site-sort/index.tsx index 6ab4f10c85f0ea..1fb27cf7cb1031 100644 --- a/client/a8c-for-agencies/sections/sites/site-sort/index.tsx +++ b/client/a8c-for-agencies/sections/sites/site-sort/index.tsx @@ -32,20 +32,24 @@ export default function SiteSort( { } ) { const { dataViewsState, setDataViewsState } = useContext( SitesDashboardContext ); - const { field, direction } = dataViewsState.sort; + const { field, direction } = dataViewsState.sort ?? {}; const isDefault = field !== SITE_COLUMN_KEY_MAP?.[ columnKey ] || ! field || ! direction; const setSort = () => { - const updatedSort = { ...dataViewsState.sort }; + let updatedSort = dataViewsState.sort; if ( isDefault ) { - updatedSort.field = SITE_COLUMN_KEY_MAP?.[ columnKey ]; - updatedSort.direction = SORT_DIRECTION_ASC; + updatedSort = { + field: SITE_COLUMN_KEY_MAP?.[ columnKey ], + direction: SORT_DIRECTION_ASC, + }; } else if ( direction === SORT_DIRECTION_ASC ) { - updatedSort.direction = SORT_DIRECTION_DESC; + updatedSort = { + field: SITE_COLUMN_KEY_MAP?.[ columnKey ], + direction: SORT_DIRECTION_ASC, + }; } else if ( direction === SORT_DIRECTION_DESC ) { - updatedSort.field = ''; - updatedSort.direction = ''; + updatedSort = undefined; } setDataViewsState( ( sitesViewState ) => ( { diff --git a/client/a8c-for-agencies/sections/sites/sites-dashboard-provider.tsx b/client/a8c-for-agencies/sections/sites/sites-dashboard-provider.tsx index e95388badb3fb4..c224b2a6828e71 100644 --- a/client/a8c-for-agencies/sections/sites/sites-dashboard-provider.tsx +++ b/client/a8c-for-agencies/sections/sites/sites-dashboard-provider.tsx @@ -1,3 +1,5 @@ +import { DESKTOP_BREAKPOINT } from '@automattic/viewport'; +import { useBreakpoint } from '@automattic/viewport-react'; import { ReactNode, useEffect, useState } from 'react'; import { DATAVIEWS_TABLE, @@ -9,8 +11,9 @@ import { DashboardSortInterface, Site, } from 'calypso/jetpack-cloud/sections/agency-dashboard/sites-overview/types'; -import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD, filtersMap } from './constants'; +import { filtersMap } from './constants'; import SitesDashboardContext from './sites-dashboard-context'; +import type { Filter } from '@wordpress/dataviews'; interface Props { showOnlyFavoritesInitialState?: boolean; @@ -28,14 +31,14 @@ interface Props { featurePreview?: ReactNode | null; } -const buildFilters = ( { issueTypes }: { issueTypes: string } ) => { +const buildFilters = ( { issueTypes }: { issueTypes: string } ): Filter[] => { const issueTypesArray = issueTypes?.split( ',' ); return ( issueTypesArray?.map( ( issueType ) => { return { field: 'status', - operator: 'in', + operator: 'is', value: filtersMap.find( ( filterMap ) => filterMap.filterType === issueType )?.ref || 1, }; } ) || [] @@ -87,18 +90,43 @@ export const SitesDashboardProvider = ( { setCurrentLicenseInfo( null ); }; - initialDataViewsState.sort.field = DEFAULT_SORT_FIELD; - initialDataViewsState.sort.direction = DEFAULT_SORT_DIRECTION; - initialDataViewsState.hiddenFields = [ 'status' ]; + // Limit fields on breakpoints smaller than 960px wide. + const isDesktop = useBreakpoint( DESKTOP_BREAKPOINT ); + const desktopFields = [ + 'url', + 'stats', + 'boost', + 'backup', + 'monitor', + 'scan', + 'plugins', + 'favorite', + 'actions', + ]; + const mobileFields = [ 'url', 'actions' ]; + const getFieldsByBreakpoint = ( isDesktop: boolean ) => + isDesktop ? desktopFields : mobileFields; const [ dataViewsState, setDataViewsState ] = useState< DataViewsState >( { ...initialDataViewsState, + fields: getFieldsByBreakpoint( isDesktop ), page: currentPage, search: searchQuery, sort, filters: buildFilters( { issueTypes } ), } ); + useEffect( () => { + const fields = getFieldsByBreakpoint( isDesktop ); + const fieldsForBreakpoint = [ ...fields ].sort().toString(); + const existingFields = [ ...( dataViewsState?.fields ?? [] ) ].sort().toString(); + // Compare the content of the arrays, not its referrences that will always be different. + // sort() sorts the array in place, so we need to clone them first. + if ( existingFields !== fieldsForBreakpoint ) { + setDataViewsState( ( prevState ) => ( { ...prevState, fields } ) ); + } + }, [ isDesktop, dataViewsState?.fields ] ); + useEffect( () => { setInitialSelectedSiteUrl( siteUrlInitialState ); if ( ! siteUrlInitialState ) { diff --git a/client/a8c-for-agencies/sections/sites/sites-dashboard/get-selected-filters.tsx b/client/a8c-for-agencies/sections/sites/sites-dashboard/get-selected-filters.tsx index ab799d83cf62d5..dc2c5dc0cc0609 100644 --- a/client/a8c-for-agencies/sections/sites/sites-dashboard/get-selected-filters.tsx +++ b/client/a8c-for-agencies/sections/sites/sites-dashboard/get-selected-filters.tsx @@ -1,7 +1,7 @@ -import { DataViewsFilter } from 'calypso/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces'; import { filtersMap } from '../constants'; +import type { Filter } from '@wordpress/dataviews'; -export function getSelectedFilters( filters: DataViewsFilter[] = [] ) { +export function getSelectedFilters( filters: Filter[] = [] ) { return ( filters?.map( ( filter ) => { const filterType = diff --git a/client/a8c-for-agencies/sections/sites/sites-dashboard/index.tsx b/client/a8c-for-agencies/sections/sites/sites-dashboard/index.tsx index 1c2864a477e13e..641abf1d88fa98 100644 --- a/client/a8c-for-agencies/sections/sites/sites-dashboard/index.tsx +++ b/client/a8c-for-agencies/sections/sites/sites-dashboard/index.tsx @@ -108,8 +108,8 @@ export default function SitesDashboard() { const { data, isError, isLoading, refetch } = useFetchDashboardSites( { isPartnerOAuthTokenLoaded: false, - searchQuery: dataViewsState.search, - currentPage: dataViewsState.page, + searchQuery: dataViewsState?.search, + currentPage: dataViewsState.page ?? 1, filter: agencyDashboardFilter, sort: dataViewsState.sort, perPage: dataViewsState.perPage, @@ -156,11 +156,11 @@ export default function SitesDashboard() { const updatedUrl = updateSitesDashboardUrl( { category: category, setCategory: setCategory, - filters: dataViewsState.filters, + filters: dataViewsState.filters ?? [], selectedSite: dataViewsState.selectedItem, selectedSiteFeature: selectedSiteFeature, - search: dataViewsState.search, - currentPage: dataViewsState.page, + search: dataViewsState.search ?? '', + currentPage: dataViewsState.page ?? 1, sort: dataViewsState.sort, showOnlyFavorites, showOnlyDevelopmentSites, @@ -274,9 +274,7 @@ export default function SitesDashboard() { } } > div[data-wp-component="VStack"] { align-items: stretch; @@ -589,7 +539,7 @@ display: flex; flex: 1; height: 100%; - overflow: hidden; + overflow: auto; } } @@ -639,10 +589,6 @@ .sites-overview__content { margin-top: 24px; - - &.is-hiding-navigation { - margin-top: 48px; // If there is no navigation bar we need to add a bigger margin. - } } .dataviews-filters__view-actions { diff --git a/client/a8c-for-agencies/sections/sites/sites-dashboard/update-sites-dashboard-url.tsx b/client/a8c-for-agencies/sections/sites/sites-dashboard/update-sites-dashboard-url.tsx index 34694ca4afad85..19bb57083807ad 100644 --- a/client/a8c-for-agencies/sections/sites/sites-dashboard/update-sites-dashboard-url.tsx +++ b/client/a8c-for-agencies/sections/sites/sites-dashboard/update-sites-dashboard-url.tsx @@ -1,5 +1,3 @@ -import { DataViewsFilter } from 'calypso/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces'; -import { Filter } from 'calypso/jetpack-cloud/sections/agency-dashboard/sites-overview/sites-dataviews/interfaces'; import { A4A_SITES_DASHBOARD_DEFAULT_CATEGORY, A4A_SITES_DASHBOARD_DEFAULT_FEATURE, @@ -8,6 +6,7 @@ import { } from '../constants'; import { DashboardSortInterface, Site } from '../types'; import { getSelectedFilters } from './get-selected-filters'; +import type { Filter } from '@wordpress/dataviews'; const buildQueryString = ( { filters, @@ -20,7 +19,7 @@ const buildQueryString = ( { filters: Filter[]; search: string; currentPage: number; - sort: DashboardSortInterface; + sort?: DashboardSortInterface; showOnlyFavorites?: boolean; showOnlyDevelopmentSites?: boolean; } ) => { @@ -36,8 +35,8 @@ const buildQueryString = ( { // ASC is the default sort direction for the URL if ( - sort.field !== DEFAULT_SORT_FIELD || - ( sort.field === DEFAULT_SORT_FIELD && sort.direction !== DEFAULT_SORT_DIRECTION ) + ( sort && sort.field !== DEFAULT_SORT_FIELD ) || + ( sort && sort.field === DEFAULT_SORT_FIELD && sort.direction !== DEFAULT_SORT_DIRECTION ) ) { urlQuery.set( 'sort_field', sort.field ); urlQuery.set( 'sort_direction', sort.direction ); @@ -78,12 +77,12 @@ export const updateSitesDashboardUrl = ( { }: { category?: string; setCategory: ( category: string ) => void; - filters: DataViewsFilter[]; + filters: Filter[]; selectedSite?: Site; selectedSiteFeature?: string; search: string; currentPage: number; - sort: DashboardSortInterface; + sort?: DashboardSortInterface; showOnlyFavorites?: boolean; showOnlyDevelopmentSites?: boolean; } ) => { diff --git a/client/a8c-for-agencies/sections/sites/sites-dataviews/index.tsx b/client/a8c-for-agencies/sections/sites/sites-dataviews/index.tsx index 7a9a1e661531f8..6b74c0b16ae5c4 100644 --- a/client/a8c-for-agencies/sections/sites/sites-dataviews/index.tsx +++ b/client/a8c-for-agencies/sections/sites/sites-dataviews/index.tsx @@ -16,8 +16,9 @@ import SiteStatusContent from 'calypso/jetpack-cloud/sections/agency-dashboard/s import { JETPACK_MANAGE_ONBOARDING_TOURS_EXAMPLE_SITE } from 'calypso/jetpack-cloud/sections/onboarding-tours/constants'; import TextPlaceholder from 'calypso/jetpack-cloud/sections/partner-portal/text-placeholder'; import SiteActions from '../site-actions'; -import { AllowedTypes, Site } from '../types'; -import { SitesDataViewsProps, SiteInfo } from './interfaces'; +import { AllowedTypes, Site, SiteData } from '../types'; +import { SitesDataViewsProps } from './interfaces'; +import type { Field } from '@wordpress/dataviews'; import './style.scss'; @@ -41,7 +42,8 @@ const SitesDataViews = ( { } return data?.total || 0; } )(); - const sitesPerPage = dataViewsState.perPage > 0 ? dataViewsState.perPage : 20; + const sitesPerPage = + dataViewsState.perPage && dataViewsState.perPage > 0 ? dataViewsState.perPage : 20; const totalPages = Math.ceil( totalSites / sitesPerPage ); const sites = useFormattedSites( data?.sites ?? [] ); @@ -57,7 +59,7 @@ const SitesDataViews = ( { ); const renderField = useCallback( - ( column: AllowedTypes, item: SiteInfo ) => { + ( column: AllowedTypes, item: SiteData ) => { if ( isLoading ) { return ; } @@ -88,15 +90,16 @@ const SitesDataViews = ( { const [ actionsRef, setActionsRef ] = useState< HTMLElement | null >(); // todo - refactor: extract fields, along actions, to the upper component - const fields = useMemo( + const fields = useMemo< Field< SiteData >[] >( () => [ { id: 'status', - header: translate( 'Status' ), - getValue: ( { item }: { item: SiteInfo } ) => + label: translate( 'Status' ), + getValue: ( { item }: { item: SiteData } ) => item.site.error || item.scan.status === 'critical', - render: () => {}, - type: 'enumeration', + render: () => { + return null; + }, elements: [ { value: 1, label: translate( 'Needs Attention' ) }, { value: 2, label: translate( 'Backup Failed' ) }, @@ -107,14 +110,15 @@ const SitesDataViews = ( { { value: 7, label: translate( 'Plugins Needing Updates' ) }, ], filterBy: { - operators: [ 'in' ], + operators: [ 'is' ], }, enableHiding: false, enableSorting: false, }, { id: 'site', - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: ( <> ), - getValue: ( { item }: { item: SiteInfo } ) => item.site.value.url, - render: ( { item }: { item: SiteInfo } ) => { + getValue: ( { item }: { item: SiteData } ) => item.site.value.url, + render: ( { item }: { item: SiteData } ) => { if ( isLoading ) { return ; } @@ -150,7 +154,8 @@ const SitesDataViews = ( { }, { id: 'stats', - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: (
), getValue: () => '-', - render: ( { item }: { item: SiteInfo } ) => renderField( 'stats', item ), + render: ( { item }: { item: SiteData } ) => renderField( 'stats', item ), enableHiding: false, enableSorting: false, }, { id: 'boost', - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: ( <> ), - getValue: ( { item }: { item: SiteInfo } ) => item.boost.status, - render: ( { item }: { item: SiteInfo } ) => renderField( 'boost', item ), + getValue: ( { item }: { item: SiteData } ) => item.boost.status, + render: ( { item }: { item: SiteData } ) => renderField( 'boost', item ), enableHiding: false, enableSorting: false, }, { id: 'backup', - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: ( <> ), getValue: () => '-', - render: ( { item }: { item: SiteInfo } ) => renderField( 'backup', item ), + render: ( { item }: { item: SiteData } ) => renderField( 'backup', item ), enableHiding: false, enableSorting: false, }, { id: 'monitor', - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: ( <> ), getValue: () => '-', - render: ( { item }: { item: SiteInfo } ) => renderField( 'monitor', item ), + render: ( { item }: { item: SiteData } ) => renderField( 'monitor', item ), enableHiding: false, enableSorting: false, }, { id: 'scan', - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: ( <> ), getValue: () => '-', - render: ( { item }: { item: SiteInfo } ) => renderField( 'scan', item ), + render: ( { item }: { item: SiteData } ) => renderField( 'scan', item ), enableHiding: false, enableSorting: false, }, { id: 'plugins', - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: ( <> ), getValue: () => '-', - render: ( { item }: { item: SiteInfo } ) => renderField( 'plugin', item ), + render: ( { item }: { item: SiteData } ) => renderField( 'plugin', item ), enableHiding: false, enableSorting: false, }, { id: 'favorite', - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: ( ), - getValue: ( { item }: { item: SiteInfo } ) => item.isFavorite, - render: ( { item }: { item: SiteInfo } ) => { + getValue: ( { item }: { item: SiteData } ) => item.isFavorite, + render: ( { item }: { item: SiteData } ) => { if ( isLoading ) { return ; } @@ -309,8 +320,8 @@ const SitesDataViews = ( { }, { id: 'actions', - getValue: ( { item }: { item: SiteInfo } ) => item.isFavorite, - render: ( { item }: { item: SiteInfo } ) => { + getValue: ( { item }: { item: SiteData } ) => item.isFavorite, + render: ( { item }: { item: SiteData } ) => { if ( isLoading ) { return ; } @@ -332,7 +343,8 @@ const SitesDataViews = ( {
); }, - header: ( + // @ts-expect-error -- Need to fix the label type upstream in @wordpress/dataviews to support React elements. + label: ( { - item.id = item.site.value.blog_id; // setting the id because of a issue with the DataViews component - return item.id; + getItemId={ ( item: SiteData ) => { + return item.site.value.blog_id.toString(); } } - onChangeView={ setDataViewsState } - supportedLayouts={ [ 'table' ] } + onChangeView={ ( view ) => setDataViewsState( () => view ) } + defaultLayouts={ { table: {} } } actions={ [] } // Replace with actions when bulk selections are implemented. isLoading={ isLoading } /> diff --git a/client/a8c-for-agencies/sections/sites/sites-dataviews/interfaces.ts b/client/a8c-for-agencies/sections/sites/sites-dataviews/interfaces.ts index 777b046c3f08f4..ad5b5ef9755983 100644 --- a/client/a8c-for-agencies/sections/sites/sites-dataviews/interfaces.ts +++ b/client/a8c-for-agencies/sections/sites/sites-dataviews/interfaces.ts @@ -1,5 +1,5 @@ import { DataViewsState } from 'calypso/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces'; -import { Site, SiteData } from '../types'; +import { Site } from '../types'; export interface SitesDataResponse { sites: Array< Site >; @@ -19,7 +19,3 @@ export interface SitesDataViewsProps { dataViewsState: DataViewsState; onRefetchSite?: () => Promise< unknown >; } - -export interface SiteInfo extends SiteData { - id: number; -} diff --git a/client/a8c-for-agencies/sections/sites/sites-dataviews/style.scss b/client/a8c-for-agencies/sections/sites/sites-dataviews/style.scss index 437ac2693715d2..667ff66fbccaf6 100644 --- a/client/a8c-for-agencies/sections/sites/sites-dataviews/style.scss +++ b/client/a8c-for-agencies/sections/sites/sites-dataviews/style.scss @@ -243,10 +243,6 @@ .dataviews-loading p { display: none; } - - .dataviews-view-table-wrapper { - height: 0 !important; - } } .dataviews-wrapper:has(.dataviews-no-results) { diff --git a/client/a8c-for-agencies/sections/sites/sites-dataviews/types.d.ts b/client/a8c-for-agencies/sections/sites/sites-dataviews/types.d.ts deleted file mode 100644 index 9d670dfed75ded..00000000000000 --- a/client/a8c-for-agencies/sections/sites/sites-dataviews/types.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '@wordpress/dataviews'; diff --git a/client/a8c-for-agencies/sections/team/hooks/use-handle-member-action.ts b/client/a8c-for-agencies/sections/team/hooks/use-handle-member-action.ts new file mode 100644 index 00000000000000..08f572ff7cd4db --- /dev/null +++ b/client/a8c-for-agencies/sections/team/hooks/use-handle-member-action.ts @@ -0,0 +1,81 @@ +import { useTranslate } from 'i18n-calypso'; +import { useCallback } from 'react'; +import useCancelMemberInviteMutation from 'calypso/a8c-for-agencies/data/team/use-cancel-member-invite'; +import useRemoveMemberMutation from 'calypso/a8c-for-agencies/data/team/use-remove-member'; +import { useDispatch } from 'calypso/state'; +import { errorNotice, successNotice } from 'calypso/state/notices/actions'; +import { TeamMember } from '../types'; + +type Props = { + onRefetchList?: () => void; +}; + +export default function useHandleMemberAction( { onRefetchList }: Props ) { + const translate = useTranslate(); + const dispatch = useDispatch(); + + const { mutate: cancelMemberInvite } = useCancelMemberInviteMutation(); + + const { mutate: removeMember } = useRemoveMemberMutation(); + + return useCallback( + ( action: string, item: TeamMember, callback?: () => void ) => { + if ( action === 'cancel-user-invite' ) { + cancelMemberInvite( + { id: item.id }, + { + onSuccess: () => { + dispatch( + successNotice( translate( 'The invitation has been successfully cancelled.' ), { + id: 'cancel-user-invite-success', + duration: 5000, + } ) + ); + onRefetchList?.(); + callback?.(); + }, + + onError: ( error ) => { + dispatch( + errorNotice( error.message, { + id: 'cancel-user-invite-error', + duration: 5000, + } ) + ); + callback?.(); + }, + } + ); + } + + if ( action === 'delete-user' ) { + removeMember( + { id: item.id }, + { + onSuccess: () => { + dispatch( + successNotice( translate( 'The member has been successfully removed.' ), { + id: 'remove-user-success', + duration: 5000, + } ) + ); + onRefetchList?.(); + callback?.(); + }, + + onError: ( error ) => { + dispatch( + errorNotice( error.message, { + id: 'remove-user-error', + duration: 5000, + } ) + ); + callback?.(); + }, + } + ); + } + }, + [ cancelMemberInvite, dispatch, onRefetchList, removeMember, translate ] + ); +} diff --git a/client/a8c-for-agencies/sections/team/hooks/use-member-list.ts b/client/a8c-for-agencies/sections/team/hooks/use-member-list.ts index 9fca9042166b20..222cf137068733 100644 --- a/client/a8c-for-agencies/sections/team/hooks/use-member-list.ts +++ b/client/a8c-for-agencies/sections/team/hooks/use-member-list.ts @@ -1,6 +1,7 @@ import { useCallback, useMemo } from 'react'; import useFetchActiveMembers from 'calypso/a8c-for-agencies/data/team/use-fetch-active-members'; import useFetchMemberInvites from 'calypso/a8c-for-agencies/data/team/use-fetch-member-invites'; +import { TeamMember } from '../types'; export function useMemberList() { const { @@ -20,7 +21,7 @@ export function useMemberList() { refetchMemberInvites(); }, [ refetchActiveMembers, refetchMemberInvites ] ); - const members = useMemo( () => { + const members: TeamMember[] = useMemo( () => { const data = [ ...( activeMembers ?? [] ), ...( memberInvites?.map( ( invite ) => ( { @@ -28,7 +29,7 @@ export function useMemberList() { displayName: invite.displayName, email: invite.email, avatar: invite.avatar, - status: 'pending', + status: 'pending' as const, } ) ) ?? [] ), ]; diff --git a/client/a8c-for-agencies/sections/team/primary/get-started/index.tsx b/client/a8c-for-agencies/sections/team/primary/get-started/index.tsx index 6c9500788c4a0c..c2054816588bd9 100644 --- a/client/a8c-for-agencies/sections/team/primary/get-started/index.tsx +++ b/client/a8c-for-agencies/sections/team/primary/get-started/index.tsx @@ -27,7 +27,7 @@ export default function GetStarted() { }; return ( - + { title } diff --git a/client/a8c-for-agencies/sections/team/primary/get-started/style.scss b/client/a8c-for-agencies/sections/team/primary/get-started/style.scss index 74774c692f25d7..e8a3f9b8ae1104 100644 --- a/client/a8c-for-agencies/sections/team/primary/get-started/style.scss +++ b/client/a8c-for-agencies/sections/team/primary/get-started/style.scss @@ -4,8 +4,7 @@ .team-list-get-started__heading { @include a4a-font-heading-xl; - - margin-block-start: 48px; + margin-block-start: 16px; margin-block-end: 8px; } @@ -17,3 +16,7 @@ a.team-list-get-started__learn-more-button.is-link { color: var(--color-primary-50); } + +.team-list-get-started__excluded-operation-list { + margin-block-end: 0; +} diff --git a/client/a8c-for-agencies/sections/team/primary/team-accept-invite/index.tsx b/client/a8c-for-agencies/sections/team/primary/team-accept-invite/index.tsx index 93d9a05f3ee137..66adc715aec6e3 100644 --- a/client/a8c-for-agencies/sections/team/primary/team-accept-invite/index.tsx +++ b/client/a8c-for-agencies/sections/team/primary/team-accept-invite/index.tsx @@ -7,11 +7,16 @@ import LayoutHeader, { LayoutHeaderTitle as Title, } from 'calypso/a8c-for-agencies/components/layout/header'; import LayoutTop from 'calypso/a8c-for-agencies/components/layout/top'; +import PagePlaceholder from 'calypso/a8c-for-agencies/components/page-placeholder'; import { A4A_OVERVIEW_LINK } from 'calypso/a8c-for-agencies/components/sidebar-menu/lib/constants'; -import useActivateMemberMutation from 'calypso/a8c-for-agencies/data/team/use-activate-member'; +import useActivateMemberMutation, { + APIError, +} from 'calypso/a8c-for-agencies/data/team/use-activate-member'; +import AgencyLogo from 'calypso/assets/images/a8c-for-agencies/agency-logo.svg'; import { useDispatch, useSelector } from 'calypso/state'; import { fetchAgencies } from 'calypso/state/a8c-for-agencies/agency/actions'; import { getActiveAgency } from 'calypso/state/a8c-for-agencies/agency/selectors'; +import NoMultiAgencyMessage from './no-multi-agency-message'; import './style.scss'; @@ -21,15 +26,7 @@ type Props = { secret?: string; }; -function PlaceHolder() { - return ( -
-
-
-
-
- ); -} +const ALREADY_MEMBER_OF_AGENCY_ERROR_CODE = 'a4a_user_invite_already_member_of_agency'; function ErrorMessage( { error }: { error: string } ) { return
{ error }
; @@ -43,13 +40,13 @@ export default function TeamAcceptInvite( { agencyId, inviteId, secret }: Props const { mutate: activateMember } = useActivateMemberMutation(); - const [ error, setError ] = useState( '' ); + const [ error, setError ] = useState< APIError | null >( null ); - useEffect( () => { - // FIXME: Check if current user is not member of any agency. If so, display some instructions on how to join to the new agency. + const hasCompleteParameters = agencyId && inviteId && secret; - if ( agencyId && inviteId && secret ) { - setError( '' ); + useEffect( () => { + if ( hasCompleteParameters ) { + setError( null ); activateMember( { @@ -61,13 +58,13 @@ export default function TeamAcceptInvite( { agencyId, inviteId, secret }: Props onSuccess: () => { dispatch( fetchAgencies() ); }, - onError: ( error ) => { - setError( error.message ); + onError: ( error: APIError ) => { + setError( error ); }, } ); } - }, [ activateMember, agencyId, dispatch, inviteId, secret ] ); + }, [ activateMember, agencyId, dispatch, hasCompleteParameters, inviteId, secret ] ); useEffect( () => { if ( agency && agency.id === Number( agencyId ) ) { @@ -76,27 +73,46 @@ export default function TeamAcceptInvite( { agencyId, inviteId, secret }: Props } }, [ agency, agencyId ] ); - const title = translate( 'Accepting team invite' ); + const title = useMemo( () => { + if ( error?.code === ALREADY_MEMBER_OF_AGENCY_ERROR_CODE ) { + return ; + } + + return translate( 'Invalid invite link' ); + }, [ error, translate ] ); const content = useMemo( () => { - if ( error ) { - return ; + if ( ! error ) { + return null; } - return ; + if ( + error.code === ALREADY_MEMBER_OF_AGENCY_ERROR_CODE && + error.data?.user_agencies?.length && + error.data?.target_agency + ) { + const currentAgency = error.data.user_agencies[ 0 ]; // Let's check only on the first agency. + const targetAgency = error.data.target_agency; + return ; + } + + return ; }, [ error ] ); + if ( ! error ) { + return ; + } + return ( - + - - { error ? ( - translate( 'Invalid invite link' ) - ) : ( - <div className="team-accept-invite__title-placeholder"></div> - ) } - + { title } { content } diff --git a/client/a8c-for-agencies/sections/team/primary/team-accept-invite/no-multi-agency-message.tsx b/client/a8c-for-agencies/sections/team/primary/team-accept-invite/no-multi-agency-message.tsx new file mode 100644 index 00000000000000..b2bd7af0a2e651 --- /dev/null +++ b/client/a8c-for-agencies/sections/team/primary/team-accept-invite/no-multi-agency-message.tsx @@ -0,0 +1,110 @@ +import { Button } from '@wordpress/components'; +import { Icon, external } from '@wordpress/icons'; +import { useTranslate } from 'i18n-calypso'; +import { + A4A_OVERVIEW_LINK, + A4A_TEAM_LINK, +} from 'calypso/a8c-for-agencies/components/sidebar-menu/lib/constants'; +import StepSection from 'calypso/a8c-for-agencies/sections/referrals/common/step-section'; +import StepSectionItem from 'calypso/a8c-for-agencies/sections/referrals/common/step-section-item'; +import { Agency } from 'calypso/state/a8c-for-agencies/types'; + +type Props = { + currentAgency: Agency; + targetAgency: Agency; +}; + +export default function NoMultiAgencyMessage( { currentAgency, targetAgency }: Props ) { + const translate = useTranslate(); + + return ( + <> +
+ { translate( `You can only join one agency dashboard at a time.` ) } +
+ +
+ { translate( + 'To join %(targetAgencyName)s, first leave the %(currentAgencyName)s dashboard.', + { + args: { + targetAgencyName: targetAgency?.name, + currentAgencyName: currentAgency?.name, + }, + comment: '%(targetAgencyName)s and %(currentAgencyName)s are agency names', + } + ) } +
+ + + + ), + }, + } + ) } + /> + + + + + + +
+ +
+ + ); +} diff --git a/client/a8c-for-agencies/sections/team/primary/team-accept-invite/style.scss b/client/a8c-for-agencies/sections/team/primary/team-accept-invite/style.scss index bddc5ae4bc6f89..407cdab3c77eb4 100644 --- a/client/a8c-for-agencies/sections/team/primary/team-accept-invite/style.scss +++ b/client/a8c-for-agencies/sections/team/primary/team-accept-invite/style.scss @@ -3,38 +3,36 @@ background: var(--color-neutral-10); } -.team-accept-invite__title-placeholder { - @include loading-effect; - - width: 300px; - height: 43px; -} - -.team-accept-invite__section-placeholder { - display: flex; - flex-direction: column; - gap: 16px; +.team-accept-invite .a4a-layout__header-main { + margin-inline: auto; } -.team-accept-invite__section-placeholder-title { - @include loading-effect; +.team-accept-invite__heading { + @include a4a-font-heading-xl; - width: 100%; - height: 32px; + margin-block-start: 48px; + margin-block-end: 8px; } +.team-accept-invite__subtitle { + @include a4a-font-body-lg; + margin-block-end: 32px; + color: var(--color-neutral-60); +} -.team-accept-invite__section-placeholder-body { - @include loading-effect; +a.team-accept-invite__link { + color: var(--color-neutral-60); + text-decoration: underline; +} - width: 100%; - height: 200px; +a.team-accept-invite__learn-more-button.is-link { + color: var(--color-primary-50); } -.team-accept-invite__section-placeholder-footer { + +.team-accept-invite__title-placeholder { @include loading-effect; - width: 20%; - min-width: 100px; + width: 300px; height: 43px; } diff --git a/client/a8c-for-agencies/sections/team/primary/team-invite/index.tsx b/client/a8c-for-agencies/sections/team/primary/team-invite/index.tsx index 57cfac179a71f8..7278cf22572a8c 100644 --- a/client/a8c-for-agencies/sections/team/primary/team-invite/index.tsx +++ b/client/a8c-for-agencies/sections/team/primary/team-invite/index.tsx @@ -63,7 +63,7 @@ export default function TeamInvite() { }, [] ); return ( - + { - return date ? new Date( date ).toLocaleDateString() : ; + const moment = useLocalizedMoment(); + const formattedDate = Number( date ); + return formattedDate ? ( + moment.unix( formattedDate ).format( 'MMMM D, YYYY' ) + ) : ( + + ); }; export const ActionColumn = ( { member, onMenuSelected, - asOwner = true, + canRemove = true, }: { member: TeamMember; - onMenuSelected?: ( action: string ) => void; - asOwner?: boolean; + onMenuSelected?: ( action: string, callback?: () => void ) => void; + canRemove?: boolean; } ): ReactNode => { const translate = useTranslate(); @@ -89,6 +100,10 @@ export const ActionColumn = ( { const buttonActionRef = useRef< HTMLButtonElement | null >( null ); + const [ confirmationDialog, setConfirmationDialog ] = useState< ConfirmationDialog | null >( + null + ); + const onToggleMenu = useCallback( () => { setShowMenu( ( current ) => ! current ); }, [] ); @@ -97,33 +112,88 @@ export const ActionColumn = ( { setShowMenu( false ); }, [] ); - if ( member.role === OWNER_ROLE ) { - return null; - } + const onSelect = useCallback( + ( { + name, + confirmation, + }: { + name: string; + confirmation?: { title: string; children: ReactNode; ctaLabel: string }; + } ) => { + if ( confirmation ) { + setConfirmationDialog( { + ...confirmation, + onConfirm: () => { + setConfirmationDialog( ( prev ) => ( prev ? { ...prev, isLoading: true } : null ) ); + onMenuSelected?.( name, () => setConfirmationDialog( null ) ); + }, + onClose: () => { + setConfirmationDialog( null ); + }, + } ); + } else { + onMenuSelected?.( name ); + } + }, + [ onMenuSelected ] + ); - const actions = - member.status === 'pending' + const actions = useMemo( () => { + return member.status === 'pending' ? [ { name: 'cancel-user-invite', label: translate( 'Cancel invite' ), className: 'is-danger', isEnabled: true, + confirmationDialog: { + title: translate( 'Cancel invitation' ), + children: translate( + 'Are you sure you want to cancel the invitation for {{b}}%(memberName)s{{/b}}?', + { + args: { memberName: member.displayName ?? member.email }, + components: { + b: , + }, + comment: '%(memberName)s is the member name', + } + ), + ctaLabel: translate( 'Cancel invitation' ), + isDestructive: true, + }, }, ] : [ { name: 'password-reset', label: translate( 'Send password reset' ), - isEnabled: true, + isEnabled: false, // FIXME: Implement this action }, { name: 'delete-user', label: translate( 'Delete user' ), className: 'is-danger', - isEnabled: asOwner, + isEnabled: canRemove, + confirmationDialog: { + title: translate( 'Delete user' ), + children: translate( 'Are you sure you want to delete {{b}}%(memberName)s{{/b}}?', { + args: { memberName: member.displayName ?? member.email }, + components: { + b: , + }, + comment: '%(memberName)s is the member name', + } ), + ctaLabel: translate( 'Delete user' ), + isDestructive: true, + }, }, ]; + }, [ member, canRemove, translate ] ); + + // We don't show the action menu when the member is the owner of the team. + if ( member.role === OWNER_ROLE ) { + return null; + } return ( <> @@ -142,13 +212,17 @@ export const ActionColumn = ( { .map( ( action ) => ( onMenuSelected?.( action.name ) } + onClick={ () => + onSelect( { name: action.name, confirmation: action.confirmationDialog } ) + } className={ clsx( 'team-list__action-menu-item', action.className ) } > { action.label } ) ) } + + { confirmationDialog && } ); }; diff --git a/client/a8c-for-agencies/sections/team/primary/team-list/index.tsx b/client/a8c-for-agencies/sections/team/primary/team-list/index.tsx index 7ed8dffd2fc9be..b9de412d3153e0 100644 --- a/client/a8c-for-agencies/sections/team/primary/team-list/index.tsx +++ b/client/a8c-for-agencies/sections/team/primary/team-list/index.tsx @@ -1,8 +1,9 @@ import page from '@automattic/calypso-router'; import { useDesktopBreakpoint } from '@automattic/viewport-react'; import { Button } from '@wordpress/components'; +import { filterSortAndPaginate } from '@wordpress/dataviews'; import { useTranslate } from 'i18n-calypso'; -import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { ReactNode, useMemo, useState } from 'react'; import { initialDataViewsState } from 'calypso/a8c-for-agencies/components/items-dashboard/constants'; import ItemsDataViews from 'calypso/a8c-for-agencies/components/items-dashboard/items-dataviews'; import { DataViewsState } from 'calypso/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces'; @@ -13,11 +14,14 @@ import LayoutHeader, { LayoutHeaderTitle as Title, } from 'calypso/a8c-for-agencies/components/layout/header'; import LayoutTop from 'calypso/a8c-for-agencies/components/layout/top'; +import PagePlaceholder from 'calypso/a8c-for-agencies/components/page-placeholder'; import { A4A_TEAM_INVITE_LINK } from 'calypso/a8c-for-agencies/components/sidebar-menu/lib/constants'; -import useCancelMemberInviteMutation from 'calypso/a8c-for-agencies/data/team/use-cancel-member-invite'; -import { useDispatch } from 'calypso/state'; +import { useDispatch, useSelector } from 'calypso/state'; +import { hasAgencyCapability } from 'calypso/state/a8c-for-agencies/agency/selectors'; +import { A4AStore } from 'calypso/state/a8c-for-agencies/types'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; -import { errorNotice, successNotice } from 'calypso/state/notices/actions'; +import { getCurrentUser } from 'calypso/state/current-user/selectors'; +import useHandleMemberAction from '../../hooks/use-handle-member-action'; import { useMemberList } from '../../hooks/use-member-list'; import { TeamMember } from '../../types'; import GetStarted from '../get-started'; @@ -31,12 +35,19 @@ export default function TeamList() { const isDesktop = useDesktopBreakpoint(); - const [ dataViewsState, setDataViewsState ] = useState< DataViewsState >( initialDataViewsState ); + const [ dataViewsState, setDataViewsState ] = useState< DataViewsState >( { + ...initialDataViewsState, + layout: { + styles: { + actions: { + width: isDesktop ? '10%' : undefined, + }, + }, + }, + } ); const { members, hasMembers, isPending, refetch } = useMemberList(); - const { mutate: cancelMemberInvite } = useCancelMemberInviteMutation(); - const title = translate( 'Manage team members' ); const onInviteClick = () => { @@ -44,37 +55,20 @@ export default function TeamList() { page( A4A_TEAM_INVITE_LINK ); }; - const handleAction = useCallback( - ( action: string, item: TeamMember ) => { - if ( action === 'cancel-user-invite' ) { - cancelMemberInvite( - { id: item.id }, - { - onSuccess: () => { - dispatch( - successNotice( 'The invitation has been successfully cancelled.', { - id: 'cancel-user-invite-success', - duration: 5000, - } ) - ); - refetch(); - }, + const handleAction = useHandleMemberAction( { onRefetchList: refetch } ); - onError: ( error ) => { - dispatch( errorNotice( error.message ) ); - }, - } - ); - } - }, - [ cancelMemberInvite, dispatch, refetch ] + const canRemove = useSelector( ( state: A4AStore ) => + hasAgencyCapability( state, 'a4a_remove_users' ) ); + const currentUser = useSelector( getCurrentUser ); + const fields = useMemo( () => [ { id: 'user', - header: translate( 'User' ).toUpperCase(), + label: translate( 'User' ).toUpperCase(), + getValue: ( { item }: { item: TeamMember } ) => item.displayName ?? '', render: ( { item }: { item: TeamMember } ): ReactNode => { return ; }, @@ -85,7 +79,8 @@ export default function TeamList() { ? [ { id: 'role', - header: translate( 'Role' ).toUpperCase(), + label: translate( 'Role' ).toUpperCase(), + getValue: ( { item }: { item: TeamMember } ) => item.role || '', render: ( { item }: { item: TeamMember } ): ReactNode => { return ; }, @@ -94,7 +89,8 @@ export default function TeamList() { }, { id: 'added-date', - header: translate( 'Added' ).toUpperCase(), + getValue: ( { item }: { item: TeamMember } ): string => item.dateAdded || '', + label: translate( 'Added' ).toUpperCase(), render: ( { item }: { item: TeamMember } ): ReactNode => { return ; }, @@ -105,25 +101,30 @@ export default function TeamList() { : [] ), { id: 'actions', - header: '', + getValue: () => '', + label: '', render: ( { item }: { item: TeamMember } ): ReactNode => { return ( handleAction( action, item ) } + onMenuSelected={ ( action, callback ) => handleAction( action, item, callback ) } + canRemove={ canRemove || item.email === currentUser?.email } /> ); }, - width: isDesktop ? '40%' : undefined, enableHiding: false, enableSorting: false, }, ], - [ handleAction, isDesktop, translate ] + [ canRemove, currentUser?.email, handleAction, isDesktop, translate ] ); + const { data: items, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( members, dataViewsState, fields ); + }, [ members, dataViewsState, fields ] ); + if ( isPending ) { - // FIXME: Add placeholder when UI is pending + return ; } if ( ! hasMembers ) { @@ -131,7 +132,7 @@ export default function TeamList() { } return ( - + { title } @@ -145,17 +146,15 @@ export default function TeamList() { `${ user.id }`, - pagination: { - totalItems: 1, - totalPages: 1, - }, + pagination: paginationInfo, enableSearch: false, - fields: fields, + fields, actions: [], setDataViewsState: setDataViewsState, dataViewsState: dataViewsState, + defaultLayouts: { table: {} }, } } /> diff --git a/client/a8c-for-agencies/sections/team/primary/team-list/style.scss b/client/a8c-for-agencies/sections/team/primary/team-list/style.scss index ae7f11d374d0d5..da8615f7cdaa1d 100644 --- a/client/a8c-for-agencies/sections/team/primary/team-list/style.scss +++ b/client/a8c-for-agencies/sections/team/primary/team-list/style.scss @@ -12,6 +12,7 @@ padding: 16px 48px; } + } diff --git a/client/a8c-for-agencies/style.scss b/client/a8c-for-agencies/style.scss index 66d450d2105abb..f07254a011f365 100644 --- a/client/a8c-for-agencies/style.scss +++ b/client/a8c-for-agencies/style.scss @@ -136,10 +136,8 @@ box-sizing: border-box; border-radius: 4px; - background-color: var(--color-surface); border-color: var(--color-accent-5); fill: var(--color-accent-100); - color: var(--color-accent-100); &:hover, &:focus-visible { @@ -183,6 +181,7 @@ } } + .components-button.is-destructive, .button.is-scary { color: var(--color-scary-50); border-color: var(--color-scary-50); @@ -195,6 +194,7 @@ } } + .components-button.is-primary.is-destructive, .button.is-primary.is-scary { background-color: var(--color-scary-50); color: var(--color-text-inverted); diff --git a/client/assets/images/a8c-for-agencies/agency-logo.svg b/client/assets/images/a8c-for-agencies/agency-logo.svg new file mode 100644 index 00000000000000..dc8ac23341e173 --- /dev/null +++ b/client/assets/images/a8c-for-agencies/agency-logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/client/assets/images/a8c-for-agencies/request-wp-admin-access-illustration.svg b/client/assets/images/a8c-for-agencies/request-wp-admin-access-illustration.svg new file mode 100644 index 00000000000000..e8fddf112a0de8 --- /dev/null +++ b/client/assets/images/a8c-for-agencies/request-wp-admin-access-illustration.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/client/assets/images/performance-profiler/ia-icon.svg b/client/assets/images/performance-profiler/ia-icon.svg new file mode 100644 index 00000000000000..ab9543fa8c24f1 --- /dev/null +++ b/client/assets/images/performance-profiler/ia-icon.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/assets/images/plans/wpcom/ecommerce-trial/best-in-class-hosting.svg b/client/assets/images/plans/wpcom/ecommerce-trial/best-in-class-hosting.svg index 39b320010e6070..ed61dd2e1aec16 100644 --- a/client/assets/images/plans/wpcom/ecommerce-trial/best-in-class-hosting.svg +++ b/client/assets/images/plans/wpcom/ecommerce-trial/best-in-class-hosting.svg @@ -1,9 +1,9 @@ - + - + - + \ No newline at end of file diff --git a/client/assets/images/plans/wpcom/ecommerce-trial/payment-card-checked.svg b/client/assets/images/plans/wpcom/ecommerce-trial/payment-card-checked.svg index b3505120a5b652..8331d85e740957 100644 --- a/client/assets/images/plans/wpcom/ecommerce-trial/payment-card-checked.svg +++ b/client/assets/images/plans/wpcom/ecommerce-trial/payment-card-checked.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/client/assets/images/plans/wpcom/ecommerce-trial/premium-themes.svg b/client/assets/images/plans/wpcom/ecommerce-trial/premium-themes.svg index 3f56dc03d47b16..f21a57bccd5ff8 100644 --- a/client/assets/images/plans/wpcom/ecommerce-trial/premium-themes.svg +++ b/client/assets/images/plans/wpcom/ecommerce-trial/premium-themes.svg @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/client/assets/images/plans/wpcom/ecommerce-trial/priority-support.svg b/client/assets/images/plans/wpcom/ecommerce-trial/priority-support.svg index 517f04bbdcbe9e..c292d0b3df1dc5 100644 --- a/client/assets/images/plans/wpcom/ecommerce-trial/priority-support.svg +++ b/client/assets/images/plans/wpcom/ecommerce-trial/priority-support.svg @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/client/assets/images/plans/wpcom/ecommerce-trial/security-performance.svg b/client/assets/images/plans/wpcom/ecommerce-trial/security-performance.svg index b6f5d014a5cb4d..45bff7306a0b40 100644 --- a/client/assets/images/plans/wpcom/ecommerce-trial/security-performance.svg +++ b/client/assets/images/plans/wpcom/ecommerce-trial/security-performance.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/client/assets/images/plans/wpcom/ecommerce-trial/seo-tools.svg b/client/assets/images/plans/wpcom/ecommerce-trial/seo-tools.svg index d09a6499dda6a0..66fb6fafb6302d 100644 --- a/client/assets/images/plans/wpcom/ecommerce-trial/seo-tools.svg +++ b/client/assets/images/plans/wpcom/ecommerce-trial/seo-tools.svg @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/client/assets/images/plans/wpcom/ecommerce-trial/simple-customization.svg b/client/assets/images/plans/wpcom/ecommerce-trial/simple-customization.svg index 772e4ce5df3c02..0a8fbe8b603f51 100644 --- a/client/assets/images/plans/wpcom/ecommerce-trial/simple-customization.svg +++ b/client/assets/images/plans/wpcom/ecommerce-trial/simple-customization.svg @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/client/assets/images/plans/wpcom/ecommerce-trial/unlimited-products.svg b/client/assets/images/plans/wpcom/ecommerce-trial/unlimited-products.svg index 909cd6be872784..deaa862264afcb 100644 --- a/client/assets/images/plans/wpcom/ecommerce-trial/unlimited-products.svg +++ b/client/assets/images/plans/wpcom/ecommerce-trial/unlimited-products.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/client/assets/images/upgrades/cc-cb.svg b/client/assets/images/upgrades/cc-cb.svg index b832f538af50a7..88dd539e81c1c5 100644 --- a/client/assets/images/upgrades/cc-cb.svg +++ b/client/assets/images/upgrades/cc-cb.svg @@ -1 +1,62 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/blocks/author-compact-profile/index.jsx b/client/blocks/author-compact-profile/index.jsx index 695c6128d15813..7c368010e11862 100644 --- a/client/blocks/author-compact-profile/index.jsx +++ b/client/blocks/author-compact-profile/index.jsx @@ -65,22 +65,23 @@ class AuthorCompactProfile extends Component { - { hasAuthorName && ! hasMatchingAuthorAndSiteNames && ( - - { author.name } - - ) } - { siteName && ( - - { siteName } - - ) } - +
+ { hasAuthorName && ! hasMatchingAuthorAndSiteNames && ( + + { author.name } + + ) } + { siteName && ( + + { siteName } + + ) } +
{ followCount ? (
diff --git a/client/blocks/eligibility-warnings/index.tsx b/client/blocks/eligibility-warnings/index.tsx index 95387ad3382d39..18e86589966558 100644 --- a/client/blocks/eligibility-warnings/index.tsx +++ b/client/blocks/eligibility-warnings/index.tsx @@ -119,7 +119,9 @@ export const EligibilityWarnings = ( { const planSlug = PLAN_BUSINESS; let redirectUrl = `/checkout/${ siteSlug }/${ planSlug }`; if ( context === 'plugins-upload' ) { - redirectUrl = `${ redirectUrl }?redirect_to=/plugins/upload/${ siteSlug }`; + redirectUrl = `${ redirectUrl }?redirect_to=${ encodeURIComponent( + `/plugins/upload/${ siteSlug }?showUpgradeSuccessNotice=true` + ) }`; } if ( showFreeTrial ) { onProceed( options ); diff --git a/client/blocks/login/index.jsx b/client/blocks/login/index.jsx index 3403b1b23e6db7..89b137600bcbf4 100644 --- a/client/blocks/login/index.jsx +++ b/client/blocks/login/index.jsx @@ -155,13 +155,26 @@ class Login extends Component { this.props.requestError?.field === 'usernameOrEmail' && this.props.requestError?.code === 'email_login_not_allowed' ) { - const magicLoginUrl = login( { + let urlConfig = { locale: this.props.locale, twoFactorAuthType: 'link', oauth2ClientId: this.props.currentQuery?.client_id, redirectTo: this.props.redirectTo, usernameOnly: true, - } ); + }; + + if ( this.props.isGravPoweredClient ) { + urlConfig = { + ...urlConfig, + gravatarFrom: + isGravatarOAuth2Client( this.props.oauth2Client ) && + this.props.currentQuery?.gravatar_from, + gravatarFlow: isGravatarFlowOAuth2Client( this.props.oauth2Client ), + emailAddress: this.props.currentQuery?.email_address, + }; + } + + const magicLoginUrl = login( urlConfig ); page( magicLoginUrl ); } @@ -290,6 +303,9 @@ class Login extends Component { event.preventDefault(); this.props.redirectToLogout( signupUrl ); } + + event.preventDefault(); + window.location.href = signupUrl; } } /> ); @@ -559,10 +575,13 @@ class Login extends Component { if ( isGravPoweredLoginPage ) { const isFromGravatar3rdPartyApp = isGravatarOAuth2Client( oauth2Client ) && currentQuery?.gravatar_from === '3rd-party'; + const isGravatarFlowWithEmail = !! ( + isGravatarFlowOAuth2Client( oauth2Client ) && currentQuery?.email_address + ); postHeader = (

- { isFromGravatar3rdPartyApp + { isFromGravatar3rdPartyApp || isGravatarFlowWithEmail ? translate( 'Please log in with your email and password.' ) : translate( 'If you prefer logging in with a password, or a social media account, choose below:' diff --git a/client/blocks/login/login-form.jsx b/client/blocks/login/login-form.jsx index 86c9949de80621..9656358d1f417c 100644 --- a/client/blocks/login/login-form.jsx +++ b/client/blocks/login/login-form.jsx @@ -32,6 +32,7 @@ import { import { isCrowdsignalOAuth2Client, isWooOAuth2Client, + isGravatarFlowOAuth2Client, isGravatarOAuth2Client, } from 'calypso/lib/oauth2-clients'; import { login, lostPassword } from 'calypso/lib/paths'; @@ -776,6 +777,9 @@ export class LoginForm extends Component { ); const isFromGravatar3rdPartyApp = isGravatarOAuth2Client( oauth2Client ) && currentQuery?.gravatar_from === '3rd-party'; + const isGravatarFlowWithEmail = !! ( + isGravatarFlowOAuth2Client( oauth2Client ) && currentQuery?.email_address + ); const signupUrl = this.getSignupUrl(); @@ -836,7 +840,14 @@ export class LoginForm extends Component { config.isEnabled( 'signup/social' ) && ! isFromAutomatticForAgenciesReferralClient && ! isCoreProfilerLostPasswordFlow && - ! isFromGravatar3rdPartyApp; + ! isFromGravatar3rdPartyApp && + ! isGravatarFlowWithEmail; + + const shouldDisableEmailInput = + isFormDisabled || + this.isPasswordView() || + isFromGravatar3rdPartyApp || + isGravatarFlowWithEmail; return (

{ isJetpack && ( diff --git a/client/blocks/reader-full-post/header.jsx b/client/blocks/reader-full-post/header.jsx index 611609ddeade8a..ae869d71396da1 100644 --- a/client/blocks/reader-full-post/header.jsx +++ b/client/blocks/reader-full-post/header.jsx @@ -7,7 +7,7 @@ import TimeSince from 'calypso/components/time-since'; import { recordPermalinkClick } from 'calypso/reader/stats'; import ReaderFullPostHeaderPlaceholder from './placeholders/header'; -const ReaderFullPostHeader = ( { post } ) => { +const ReaderFullPostHeader = ( { post, authorProfile } ) => { const handlePermalinkClick = () => { recordPermalinkClick( 'full_post_title', post ); }; @@ -43,6 +43,7 @@ const ReaderFullPostHeader = ( { post } ) => { ) : null } +
{ authorProfile }
{ post.date ? ( @@ -66,6 +67,7 @@ const ReaderFullPostHeader = ( { post } ) => { ReaderFullPostHeader.propTypes = { post: PropTypes.object.isRequired, + children: PropTypes.node, }; export default ReaderFullPostHeader; diff --git a/client/blocks/reader-full-post/index.jsx b/client/blocks/reader-full-post/index.jsx index 1dbc668e6de2f6..16b638e6c97b23 100644 --- a/client/blocks/reader-full-post/index.jsx +++ b/client/blocks/reader-full-post/index.jsx @@ -577,7 +577,25 @@ export class FullPostView extends Component {
- + + } + /> { post.featured_image && ! isFeaturedImageInContent( post ) && ( Custom Inputs Component { this.withCustomInputs() } + +

With Shortcuts Menu Displayed

+ + + ); } diff --git a/client/components/date-range/index.js b/client/components/date-range/index.js index 8e998b4507e0ff..6da29a72e22d4a 100644 --- a/client/components/date-range/index.js +++ b/client/components/date-range/index.js @@ -8,6 +8,7 @@ import { withLocalizedMoment } from 'calypso/components/localized-moment'; import DateRangePicker from './date-range-picker'; import DateRangeHeader from './header'; import DateRangeInputs from './inputs'; +import Shortcuts from './shortcuts'; import DateRangeTrigger from './trigger'; import './style.scss'; @@ -44,6 +45,7 @@ export class DateRange extends Component { renderTrigger: PropTypes.func, renderHeader: PropTypes.func, renderInputs: PropTypes.func, + displayShortcuts: PropTypes.bool, rootClass: PropTypes.string, }; @@ -56,6 +58,7 @@ export class DateRange extends Component { renderTrigger: ( props ) => , renderHeader: ( props ) => , renderInputs: ( props ) => , + displayShortcuts: false, rootClass: '', }; @@ -387,6 +390,16 @@ export class DateRange extends Component { return window.matchMedia( '(min-width: 480px)' ).matches ? 2 : 1; } + handleDateRangeChange( startDate, endDate ) { + this.setState( { + startDate, + endDate, + textInputStartDate: this.toDateString( startDate ), + textInputEndDate: this.toDateString( endDate ), + } ); + this.props.onDateSelect && this.props.onDateSelect( startDate, endDate ); + } + renderDateHelp() { const { startDate, endDate } = this.state; @@ -443,16 +456,6 @@ export class DateRange extends Component { onInputFocus: this.handleInputFocus, }; - const onDateRangeChange = ( startDate, endDate ) => { - this.setState( { - startDate, - endDate, - textInputStartDate: this.toDateString( startDate ), - textInputEndDate: this.toDateString( endDate ), - } ); - this.props.onDateSelect && this.props.onDateSelect( startDate, endDate ); - }; - return ( -
-
- { this.props.renderHeader( headerProps ) } - { this.renderDateHelp() } +
+
+
+ { this.props.renderHeader( headerProps ) } + { this.renderDateHelp() } +
+ { this.props.renderInputs( inputsProps ) } + { this.renderDatePicker() }
- { this.props.renderInputs( inputsProps ) } - + { /* Render shortcuts to the right of the calendar */ } + { this.props.displayShortcuts && ( +
+ + this.handleDateRangeChange( startDate, endDate ) + } + /> +
+ ) }
); } + /** + * Renders the DatePicker component + * @returns {import('react').Element} the DatePicker component + */ + renderDatePicker() { + return ( + + this.handleDateRangeChange( startDate, endDate ) + } + focusedMonth={ this.state.focusedMonth } + numberOfMonths={ this.getNumberOfMonths() } + /> + ); + } + /** * Renders the component * @returns {import('react').Element} the DateRange component diff --git a/client/components/date-range/shortcuts.tsx b/client/components/date-range/shortcuts.tsx new file mode 100644 index 00000000000000..f135ff714b9e69 --- /dev/null +++ b/client/components/date-range/shortcuts.tsx @@ -0,0 +1,90 @@ +import { Button } from '@wordpress/components'; +import clsx from 'clsx'; +import { useTranslate } from 'i18n-calypso'; +import moment from 'moment'; +import PropTypes from 'prop-types'; +import { useState } from 'react'; + +const DATERANGE_PERIOD = { + DAY: 'day', + WEEK: 'week', + MONTH: 'month', +}; + +const DateRangePickerShortcuts = ( { + currentShortcut, + onClick, +}: { + currentShortcut?: string; + onClick: ( newFromDate: moment.Moment, newToDate: moment.Moment ) => void; +} ) => { + const translate = useTranslate(); + const [ selectedShortcut, setSelectedShortcut ] = useState( currentShortcut ); + + const getShortcutList = () => [ + { + id: 'last_7_days', + label: translate( 'Last 7 Days' ), + offset: 0, + range: 6, + period: DATERANGE_PERIOD.DAY, + }, + { + id: 'last_30_days', + label: translate( 'Last 30 Days' ), + offset: 0, + range: 29, + period: DATERANGE_PERIOD.DAY, + }, + { + id: 'last_3_months', + label: translate( 'Last 90 Days' ), + offset: 0, + range: 89, + period: DATERANGE_PERIOD.WEEK, + }, + { + id: 'last_year', + label: translate( 'Last Year' ), + offset: 0, + range: 364, // ranges are zero based! + period: DATERANGE_PERIOD.MONTH, + }, + ]; + + const shortcutList = getShortcutList(); + + const handleClick = ( { id, offset, range }: { id?: string; offset: number; range: number } ) => { + setSelectedShortcut( id ); + const newToDate = moment().subtract( offset, 'days' ); + const newFromDate = moment().subtract( offset + range, 'days' ); + + onClick( newFromDate, newToDate ); + }; + + return ( +
+
    + { shortcutList.map( ( shortcut, idx ) => ( +
  • + +
  • + ) ) } +
+
+ ); +}; + +DateRangePickerShortcuts.propTypes = { + currentShortcut: PropTypes.string, + onClick: PropTypes.func.isRequired, +}; + +export default DateRangePickerShortcuts; diff --git a/client/components/date-range/style.scss b/client/components/date-range/style.scss index 006a92b2620c41..88a45decd7cc2d 100644 --- a/client/components/date-range/style.scss +++ b/client/components/date-range/style.scss @@ -1,3 +1,8 @@ +@import "@wordpress/base-styles/breakpoints"; + +$date-range-shortcut-min-width: 140px; +$date-range-mobile-layout-switch: $break-small; + .date-range { position: relative; display: flex; @@ -268,3 +273,63 @@ border-radius: 200px !important; } } + +.date-range__popover-content { // Styling to fit optional shortcuts sidebar + display: flex; + align-items: stretch; // Ensure children stretch to full height + + @media (min-width: $date-range-mobile-layout-switch) { + flex-direction: row; // Align items in a row on larger screens + } + + @media (max-width: $date-range-mobile-layout-switch) { + flex-direction: column; // Stack items vertically on small screens + } +} + +.date-range-picker-shortcuts { + padding: 16px; + border-left: 1px solid var(--gray-gray-5, #dcdcde); + box-sizing: border-box; + + @media (max-width: $date-range-mobile-layout-switch) { + border-left: 0 none; + border-top: 1px solid var(--gray-gray-5, #dcdcde); + display: block; // Ensure it takes full width on mobile + margin-bottom: 16px; // Space between shortcuts and calendar + } + + @media (min-width: $date-range-mobile-layout-switch) { + // Adjust layout for larger screens + flex: 0 0 auto; // Allow it to take only necessary space + } +} + +.date-range-picker-shortcuts__list { + list-style: none; + margin: 0; +} + +.date-range-picker-shortcuts__shortcut { + border-radius: 4px; + min-width: $date-range-shortcut-min-width; + + + &:hover { + background-color: var(--color-primary-0); + } + + & + & { + margin-top: 2px; // space for an outline for the current item and hover for the next + } + + &.is-selected { + background-color: var(--color-accent-5); + } + + .components-button { + display: flex; + justify-content: space-between; + width: 100%; + } +} diff --git a/client/components/domains/domain-suggestion/style.scss b/client/components/domains/domain-suggestion/style.scss index c307e06023f204..153ce940119508 100644 --- a/client/components/domains/domain-suggestion/style.scss +++ b/client/components/domains/domain-suggestion/style.scss @@ -20,7 +20,6 @@ transition: box-shadow 0.25s cubic-bezier(0.19, 1, 0.22, 1); &:hover { - box-shadow: 0 0 0 1px var(--color-neutral-light); z-index: z-index("root", ".domain-suggestion.is-clickable:hover"); } } @@ -67,6 +66,13 @@ } } + .is-section-stepper & { + @include breakpoint-deprecated(">480px") { + flex-direction: row; + align-items: center; + } + } + .is-section-domains & { @include breakpoint-deprecated( ">800px" ) { flex-direction: row; @@ -331,6 +337,8 @@ } } + +body.is-section-stepper, body.is-section-signup.is-white-signup { svg { padding-top: 0; diff --git a/client/components/domains/featured-domain-suggestions/style.scss b/client/components/domains/featured-domain-suggestions/style.scss index 473dd8d17730aa..cb50024bc0210e 100644 --- a/client/components/domains/featured-domain-suggestions/style.scss +++ b/client/components/domains/featured-domain-suggestions/style.scss @@ -114,6 +114,11 @@ align-items: flex-start; margin-top: 12px; } + + body.is-section-stepper & { + align-items: flex-start; + margin-top: 12px; + } } .domain-registration-suggestion__badges { @@ -166,6 +171,7 @@ } } +body.is-section-stepper .step-container .featured-domain-suggestion.card, body.is-section-signup.is-white-signup .step-wrapper .featured-domain-suggestion.card { border: none; border-radius: 0; diff --git a/client/components/domains/register-domain-step/style.scss b/client/components/domains/register-domain-step/style.scss index 28b4af5d129cae..0bd80dc28824ae 100644 --- a/client/components/domains/register-domain-step/style.scss +++ b/client/components/domains/register-domain-step/style.scss @@ -103,6 +103,7 @@ @include breakpoint-deprecated( ">660px" ) { padding-top: 18px; + .is-section-stepper &, .is-white-signup & { padding-top: 0; padding-bottom: 16px; @@ -244,6 +245,7 @@ body.is-section-signup:not(.is-white-signup) { } } +body.is-section-stepper, body.is-section-signup.is-white-signup { .domain-search-results__domain-availability .card { box-shadow: none; diff --git a/client/components/domains/use-my-domain/style.scss b/client/components/domains/use-my-domain/style.scss index 060420febac985..8c563f8ea29606 100644 --- a/client/components/domains/use-my-domain/style.scss +++ b/client/components/domains/use-my-domain/style.scss @@ -3,11 +3,18 @@ @import "@wordpress/base-styles/mixins"; @import "@wordpress/base-styles/colors"; +.useMyDomain { + .domain-transfer-or-connect__content { + box-shadow: none; + } +} + .use-my-domain { display: flex; align-items: center; flex-direction: column; padding: 36px 24px; + box-shadow: none; & &__domain-illustration { margin-bottom: 36px; diff --git a/client/components/jetpack/card/jetpack-product-card/display-price/style.scss b/client/components/jetpack/card/jetpack-product-card/display-price/style.scss index 936fee4b562f2d..1ae25b81184308 100644 --- a/client/components/jetpack/card/jetpack-product-card/display-price/style.scss +++ b/client/components/jetpack/card/jetpack-product-card/display-price/style.scss @@ -60,7 +60,7 @@ color: var(--studio-gray-20); &::before { - border-top: 3px solid var(--studio-pink-50); + border-top: 3px solid var(--studio-wordpress-blue); border-radius: 3px; transform: initial; } @@ -204,7 +204,7 @@ } &__expiration-date { - color: var(--studio-pink-50); + color: var(--studio-red); } &__standalone-card-price { diff --git a/client/components/jetpack/current-user-has-capabilities-switch/index.tsx b/client/components/jetpack/current-user-has-capabilities-switch/index.tsx index d00d9418c0f674..449993cff86fc4 100644 --- a/client/components/jetpack/current-user-has-capabilities-switch/index.tsx +++ b/client/components/jetpack/current-user-has-capabilities-switch/index.tsx @@ -21,7 +21,7 @@ const CurrentUserHasCapabilitiesSwitch: FC< Props > = ( { const userCapabilities = useSelector( ( state ) => getCurrentUserCapabilities( state, siteId ) ); const hasCapabilities = useCallback( - () => capabilities.every( ( p: string ) => userCapabilities[ p ] ), + () => capabilities.every( ( p: string ) => userCapabilities?.[ p ] ), [ capabilities, userCapabilities ] ); diff --git a/client/components/jetpack/daily-backup-status/status-card/backup-realtime-message.tsx b/client/components/jetpack/daily-backup-status/status-card/backup-realtime-message.tsx new file mode 100644 index 00000000000000..e433328d3eaa94 --- /dev/null +++ b/client/components/jetpack/daily-backup-status/status-card/backup-realtime-message.tsx @@ -0,0 +1,80 @@ +import { useTranslate } from 'i18n-calypso'; +import { FunctionComponent } from 'react'; +import { useLocalizedMoment } from 'calypso/components/localized-moment'; +import type { Moment } from 'moment'; + +type Props = { + baseBackupDate: Moment; + eventsCount: number; + selectedBackupDate: Moment; +}; + +export const BackupRealtimeMessage: FunctionComponent< Props > = ( { + baseBackupDate, + eventsCount, + selectedBackupDate, +} ) => { + const translate = useTranslate(); + const moment = useLocalizedMoment(); + + if ( + ! moment.isMoment( baseBackupDate ) || + ! moment.isMoment( selectedBackupDate ) || + eventsCount < 0 + ) { + return; + } + + const daysDiff = selectedBackupDate.diff( baseBackupDate, 'days' ); + let message: string | React.ReactNode; + + if ( daysDiff === 0 ) { + // Base backup date is the same as the selected backup date + message = translate( + 'We are using a full backup from this day (%(baseBackupDate)s) with %(eventsCount)d change you have made since then until now.', + 'We are using a full backup from this day (%(baseBackupDate)s) with %(eventsCount)d changes you have made since then until now.', + { + count: eventsCount, + args: { + baseBackupDate: baseBackupDate.format( 'YYYY-MM-DD hh:mm A' ), + eventsCount: eventsCount, + }, + comment: + '%(baseBackupDate)s is the date and time of the backup, and %(eventsCount)d is the number of changes made since the backup.', + } + ); + } else if ( daysDiff === 1 ) { + // Base backup date is the day before the selected backup date + message = translate( + 'We are using a full backup from the previous day (%(baseBackupDate)s) with %(eventsCount)d change you have made since then until now.', + 'We are using a full backup from the previous day (%(baseBackupDate)s) with %(eventsCount)d changes you have made since then until now.', + { + count: eventsCount, + args: { + baseBackupDate: baseBackupDate.format( 'YYYY-MM-DD hh:mm A' ), + eventsCount: eventsCount, + }, + comment: + '%(baseBackupDate)s is the date and time of the backup, and %(eventsCount)d is the number of changes made since the backup.', + } + ); + } else { + // Base backup date is two or more days before the selected backup date + message = translate( + 'We are using a %(daysAgo)d-day old full backup (%(baseBackupDate)s) with %(eventsCount)d change you have made since then until now.', + 'We are using a %(daysAgo)d-day old full backup (%(baseBackupDate)s) with %(eventsCount)d changes you have made since then until now.', + { + count: eventsCount, + args: { + daysAgo: daysDiff, + baseBackupDate: baseBackupDate.format( 'YYYY-MM-DD hh:mm A' ), + eventsCount: eventsCount, + }, + comment: + '%(daysAgo)d is the number of days since the backup, %(baseBackupDate)s is the date and time of the backup, and %(eventsCount)d is the number of changes made since the backup.', + } + ); + } + + return <>{ message }; +}; diff --git a/client/components/jetpack/daily-backup-status/status-card/backup-successful.jsx b/client/components/jetpack/daily-backup-status/status-card/backup-successful.jsx index 3671fc9028d0eb..b79d54b0bcb87e 100644 --- a/client/components/jetpack/daily-backup-status/status-card/backup-successful.jsx +++ b/client/components/jetpack/daily-backup-status/status-card/backup-successful.jsx @@ -1,3 +1,4 @@ +import config from '@automattic/calypso-config'; import { useTranslate } from 'i18n-calypso'; import { useSelector } from 'react-redux'; import { default as ActivityCard, useToggleContent } from 'calypso/components/activity-card'; @@ -15,6 +16,7 @@ import isJetpackSiteMultiSite from 'calypso/state/sites/selectors/is-jetpack-sit import { getSelectedSiteId } from 'calypso/state/ui/selectors'; import ActionButtons from '../action-buttons'; import useGetDisplayDate from '../use-get-display-date'; +import { BackupRealtimeMessage } from './backup-realtime-message'; import cloudSuccessIcon from './icons/cloud-success.svg'; import cloudWarningIcon from './icons/cloud-warning.svg'; @@ -67,6 +69,8 @@ const BackupSuccessful = ( { const isCloneFlow = availableActions && availableActions.length === 1 && availableActions[ 0 ] === 'clone'; + const baseBackupDate = backup.baseRewindId ? moment.unix( backup.baseRewindId ) : null; + const showRealTimeMessage = backup.baseRewindId && baseBackupDate && backup.rewindStepCount > 0; return ( <>
@@ -93,6 +97,13 @@ const BackupSuccessful = ( {
{ displayDateNoLatest }
+ { config.isEnabled( 'jetpack/backup-realtime-message' ) && showRealTimeMessage && ( + + ) }
{ meta }
{ isMultiSite && ( diff --git a/client/components/jetpack/daily-backup-status/test/backup-realtime-message.jsx b/client/components/jetpack/daily-backup-status/test/backup-realtime-message.jsx new file mode 100644 index 00000000000000..b1d1488a38b12d --- /dev/null +++ b/client/components/jetpack/daily-backup-status/test/backup-realtime-message.jsx @@ -0,0 +1,80 @@ +/** + * @jest-environment jsdom + */ +import { render } from '@testing-library/react'; +import { useTranslate } from 'i18n-calypso'; +import moment from 'moment'; +import { BackupRealtimeMessage } from '../status-card/backup-realtime-message'; + +// Mock the useTranslate function +jest.mock( 'i18n-calypso' ); + +describe( 'BackupRealtimeMessage', () => { + const renderMessage = ( baseBackupDate, eventsCount, selectedDate ) => { + return render( + + ); + }; + + const translateMock = jest.fn( ( singular, plural, { count, args } ) => { + const translatedText = count === 1 ? singular : plural; + return translatedText + .replace( '%(baseBackupDate)s', args.baseBackupDate ) + .replace( '%(eventsCount)d', args.eventsCount ) + .replace( '%(daysAgo)d', args.daysAgo ); + } ); + + beforeEach( () => { + jest.clearAllMocks(); + useTranslate.mockImplementation( () => translateMock ); + } ); + + test( 'renders the correct message when the base backup date is the same as the selected backup date', () => { + const selectedDate = moment( '2024-08-26T12:00:00Z' ); + const baseBackupDate = selectedDate; // same day + const { container } = renderMessage( baseBackupDate, 3, selectedDate ); + expect( container.textContent ).toBe( + `We are using a full backup from this day (2024-08-26 12:00 PM) with 3 changes you have made since then until now.` + ); + } ); + + test( 'renders the correct message when the base backup date is the day before the selected backup date', () => { + const selectedDate = moment( '2024-08-26T12:00:00Z' ); + const baseBackupDate = selectedDate.clone().subtract( 1, 'day' ); // previous day + const { container } = renderMessage( baseBackupDate, 25, selectedDate ); + expect( container.textContent ).toBe( + `We are using a full backup from the previous day (2024-08-25 12:00 PM) with 25 changes you have made since then until now.` + ); + } ); + + test( 'renders the correct message when the base backup date is two or more days before the selected backup date', () => { + const selectedDate = moment( '2024-08-26T12:00:00Z' ); + const baseBackupDate = moment( '2024-08-23T12:00:00Z' ); + const { container } = renderMessage( baseBackupDate, 5, selectedDate ); + expect( container.textContent ).toBe( + `We are using a 3-day old full backup (2024-08-23 12:00 PM) with 5 changes you have made since then until now.` + ); + } ); + + test( 'renders the correct singular message when there is only 1 change', () => { + const selectedDate = moment( '2024-08-26T12:00:00Z' ); + const baseBackupDate = moment( '2024-08-26T12:00:00Z' ); + const { container } = renderMessage( baseBackupDate, 1, selectedDate ); + expect( container.textContent ).toBe( + `We are using a full backup from this day (2024-08-26 12:00 PM) with 1 change you have made since then until now.` + ); + } ); + + test( 'renders the correct plural message when there are 2 or more changes', () => { + const selectedDate = moment( '2024-08-26T12:00:00Z' ); + const baseBackupDate = moment( '2024-08-26T12:00:00Z' ); + const { container } = renderMessage( baseBackupDate, 2, selectedDate ); + expect( container.textContent ).toBe( + `We are using a full backup from this day (2024-08-26 12:00 PM) with 2 changes you have made since then until now.` + ); + } ); +} ); diff --git a/client/components/payment-logo/index.jsx b/client/components/payment-logo/index.jsx index 002507bb744823..2657a51ff592bc 100644 --- a/client/components/payment-logo/index.jsx +++ b/client/components/payment-logo/index.jsx @@ -17,7 +17,7 @@ import './style.scss'; const LOGO_PATHS = { amex: creditCardAmexImage, - cb: creditCardCartesBancairesImage, + cartes_bancaires: creditCardCartesBancairesImage, diners: creditCardDinersImage, discover: creditCardDiscoverImage, jcb: creditCardJCBImage, @@ -32,7 +32,7 @@ const ALT_TEXT = { amex: 'American Express', 'apple-pay': 'Apple Pay', bancontact: 'Bancontact', - cb: 'Cartes Bancaires', + cartes_bancaires: 'Cartes Bancaires', diners: 'Diners Club', discover: 'Discover', eps: 'eps', diff --git a/client/components/stats-date-control/index.tsx b/client/components/stats-date-control/index.tsx index d9bea31d8eb9c4..e2bd7fe7f04f0d 100644 --- a/client/components/stats-date-control/index.tsx +++ b/client/components/stats-date-control/index.tsx @@ -146,6 +146,7 @@ const StatsDateControl = ( { ); } } rootClass="stats-date-control-picker" + displayShortcuts /> ) : (
}; }; -const agencyDashboardSortToQueryObject = ( sort: DashboardSortInterface ) => { +const agencyDashboardSortToQueryObject = ( sort?: DashboardSortInterface ) => { + if ( ! sort ) { + return; + } + return { ...( sort?.field && { sort_field: sort.field } ), ...( sort?.direction && { sort_direction: sort.direction } ), @@ -33,10 +37,10 @@ const agencyDashboardSortToQueryObject = ( sort: DashboardSortInterface ) => { export interface FetchDashboardSitesArgsInterface { isPartnerOAuthTokenLoaded: boolean; - searchQuery: string; + searchQuery: string | undefined; currentPage: number; filter: AgencyDashboardFilter; - sort: DashboardSortInterface; + sort?: DashboardSortInterface; perPage?: number; agencyId?: number; } diff --git a/client/data/paid-newsletter/use-map-stripe-plan-to-product-mutation.ts b/client/data/paid-newsletter/use-map-stripe-plan-to-product-mutation.ts index 38c0a939574c87..b847fc6ca96d2f 100644 --- a/client/data/paid-newsletter/use-map-stripe-plan-to-product-mutation.ts +++ b/client/data/paid-newsletter/use-map-stripe-plan-to-product-mutation.ts @@ -8,7 +8,7 @@ import { useCallback } from 'react'; import wp from 'calypso/lib/wp'; interface MutationVariables { - siteId: string; + siteId: number; engine: string; currentStep: string; stripePlan: string; @@ -48,10 +48,8 @@ export const useMapStripePlanToProductMutation = ( }, ...options, onSuccess( ...args ) { - const [ , { siteId, engine, currentStep } ] = args; - queryClient.invalidateQueries( { - queryKey: [ 'paid-newsletter-importer', siteId, engine, currentStep ], - } ); + const [ data, { siteId, engine } ] = args; + queryClient.setQueryData( [ 'paid-newsletter-importer', siteId, engine ], data ); options.onSuccess?.( ...args ); }, } ); @@ -60,7 +58,7 @@ export const useMapStripePlanToProductMutation = ( const mapStripePlanToProduct = useCallback( ( - siteId: string, + siteId: number, engine: string, currentStep: string, stripePlan: string, diff --git a/client/data/paid-newsletter/use-paid-newsletter-query.ts b/client/data/paid-newsletter/use-paid-newsletter-query.ts index 184b9b699c3f2f..91912777789ce1 100644 --- a/client/data/paid-newsletter/use-paid-newsletter-query.ts +++ b/client/data/paid-newsletter/use-paid-newsletter-query.ts @@ -1,20 +1,82 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query'; import wp from 'calypso/lib/wp'; -interface PaidNewsletterStep { - status: string; - content?: any; +export type StepId = 'content' | 'subscribers' | 'paid-subscribers' | 'summary'; +export type StepStatus = 'initial' | 'skipped' | 'importing' | 'done'; + +export interface ContentStepContent {} + +export interface SubscribersStepContent { + meta?: { + email_count: string; + id: number; + paid_subscribers_count: string; + platform: string; + scheduled_at: string; + status: string; + subscribed_count: string | null; + timestamp: string; + }; +} + +export interface Product { + currency: string; + id: number; + interval: string; + price: string; + title: string; +} + +export interface Plan { + active_subscriptions: boolean; + is_active: boolean; + name: string; + plan_amount_decimal: number; + plan_currency: string; + plan_id: string; + plan_interval: string; + product_id: string; +} + +export interface PaidSubscribersStepContent { + available_tiers: Product[]; + connect_url?: string; + is_connected_stripe: boolean; + map_plans: Record< string, string >; + plans: Plan[]; +} + +export interface SummaryStepContent {} + +interface Step< T > { + status: StepStatus; + content?: T; +} + +interface Steps { + content: Step< ContentStepContent >; + subscribers: Step< SubscribersStepContent >; + 'paid-subscribers': Step< PaidSubscribersStepContent >; + summary: Step< SummaryStepContent >; } interface PaidNewsletterData { - current_step: string; - steps: Record< string, PaidNewsletterStep >; + current_step: StepId; + steps: Steps; } -export const usePaidNewsletterQuery = ( engine: string, currentStep: string, siteId?: number ) => { +const REFRESH_INTERVAL = 2000; // every 2 seconds. + +export const usePaidNewsletterQuery = ( + engine: string, + currentStep: StepId, + siteId?: number, + autoRefresh?: boolean +) => { return useQuery( { enabled: !! siteId, - queryKey: [ 'paid-newsletter-importer', siteId, engine, currentStep ], + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: [ 'paid-newsletter-importer', siteId, engine ], queryFn: (): Promise< PaidNewsletterData > => { return wp.req.get( { @@ -30,5 +92,6 @@ export const usePaidNewsletterQuery = ( engine: string, currentStep: string, sit placeholderData: keepPreviousData, refetchOnWindowFocus: true, staleTime: 6000, // 10 minutes + refetchInterval: autoRefresh ? REFRESH_INTERVAL : false, } ); }; diff --git a/client/data/paid-newsletter/use-reset-mutation.ts b/client/data/paid-newsletter/use-reset-mutation.ts index a23caa753ae686..5f58327ff6b352 100644 --- a/client/data/paid-newsletter/use-reset-mutation.ts +++ b/client/data/paid-newsletter/use-reset-mutation.ts @@ -38,10 +38,9 @@ export const useResetMutation = ( }, ...options, onSuccess( ...args ) { - const [ , { siteId, engine, currentStep } ] = args; - queryClient.invalidateQueries( { - queryKey: [ 'paid-newsletter-importer', siteId, engine, currentStep ], - } ); + const [ data, { siteId, engine } ] = args; + + queryClient.setQueryData( [ 'paid-newsletter-importer', siteId, engine ], data ); options.onSuccess?.( ...args ); }, } ); diff --git a/client/data/paid-newsletter/use-skip-next-step-mutation.ts b/client/data/paid-newsletter/use-skip-next-step-mutation.ts index d602be6ef59405..50976016f23812 100644 --- a/client/data/paid-newsletter/use-skip-next-step-mutation.ts +++ b/client/data/paid-newsletter/use-skip-next-step-mutation.ts @@ -20,6 +20,16 @@ export const useSkipNextStepMutation = ( const queryClient = useQueryClient(); const mutation = useMutation( { mutationFn: async ( { siteId, engine, currentStep, skipStep }: MutationVariables ) => { + queryClient.setQueryData( + [ 'paid-newsletter-importer', siteId, engine ], + ( previous: any ) => { + const optimisticData = previous; // { steps: [ { [ skipStep ]: { status: 'skipped' } } ] }; + + optimisticData.steps[ skipStep ].status = 'skipped'; + return optimisticData; + } + ); + const response = await wp.req.post( { path: `/sites/${ siteId }/site-importer/paid-newsletter`, @@ -40,10 +50,8 @@ export const useSkipNextStepMutation = ( }, ...options, onSuccess( ...args ) { - const [ , { siteId, engine, currentStep } ] = args; - queryClient.invalidateQueries( { - queryKey: [ 'paid-newsletter-importer', siteId, engine, currentStep ], - } ); + const [ data, { siteId, engine } ] = args; + queryClient.setQueryData( [ 'paid-newsletter-importer', siteId, engine ], data ); options.onSuccess?.( ...args ); }, } ); diff --git a/client/data/paid-newsletter/use-subscriber-import-mutation.ts b/client/data/paid-newsletter/use-subscriber-import-mutation.ts new file mode 100644 index 00000000000000..210f38e173beaf --- /dev/null +++ b/client/data/paid-newsletter/use-subscriber-import-mutation.ts @@ -0,0 +1,56 @@ +import { + DefaultError, + useMutation, + UseMutationOptions, + useQueryClient, +} from '@tanstack/react-query'; +import { useCallback } from 'react'; +import wp from 'calypso/lib/wp'; + +interface MutationVariables { + siteId: number; + engine: string; + currentStep: string; +} + +export const useSubscriberImportMutation = ( + options: UseMutationOptions< unknown, DefaultError, MutationVariables > = {} +) => { + const queryClient = useQueryClient(); + const mutation = useMutation( { + mutationFn: async ( { siteId, engine, currentStep }: MutationVariables ) => { + const response = await wp.req.post( + { + path: `/sites/${ siteId }/site-importer/paid-newsletter/subscriber-import`, + apiNamespace: 'wpcom/v2', + }, + { + engine: engine, + current_step: currentStep, + } + ); + + if ( ! response.current_step ) { + throw new Error( 'unsuccsefully skipped step', response ); + } + + return response; + }, + ...options, + onSuccess( ...args ) { + const [ data, { siteId, engine } ] = args; + queryClient.setQueryData( [ 'paid-newsletter-importer', siteId, engine ], data ); + options.onSuccess?.( ...args ); + }, + } ); + + const { mutate } = mutation; + + const enqueueSubscriberImport = useCallback( + ( siteId: number, engine: string, currentStep: string ) => + mutate( { siteId, engine, currentStep } ), + [ mutate ] + ); + + return { enqueueSubscriberImport, ...mutation }; +}; diff --git a/client/data/site-profiler/types.ts b/client/data/site-profiler/types.ts index 5089fef55afaf8..da87237bc95669 100644 --- a/client/data/site-profiler/types.ts +++ b/client/data/site-profiler/types.ts @@ -112,6 +112,10 @@ export interface UrlBasicMetricsQueryResponse { token: string; } +export interface LeadMutationResponse { + success: boolean; +} + export interface UrlSecurityMetricsQueryResponse { wpscan: { report: { diff --git a/client/data/site-profiler/use-lead-query.ts b/client/data/site-profiler/use-lead-query.ts new file mode 100644 index 00000000000000..852a8fa07c10da --- /dev/null +++ b/client/data/site-profiler/use-lead-query.ts @@ -0,0 +1,20 @@ +import { useMutation } from '@tanstack/react-query'; +import { LeadMutationResponse } from 'calypso/data/site-profiler/types'; +import wp from 'calypso/lib/wp'; + +export const useLeadMutation = ( url?: string, hash?: string ) => { + return useMutation( { + mutationKey: [ 'lead', url, hash ], + mutationFn: (): Promise< LeadMutationResponse > => + wp.req.post( + { + path: '/site-profiler/lead', + apiNamespace: 'wpcom/v2', + }, + { + url, + hash, + } + ), + } ); +}; diff --git a/client/devdocs/design/index.jsx b/client/devdocs/design/index.jsx index 066c358d7ce8c4..6b0202fa436aeb 100644 --- a/client/devdocs/design/index.jsx +++ b/client/devdocs/design/index.jsx @@ -121,7 +121,6 @@ import WpcomColophon from 'calypso/components/wpcom-colophon/docs/example'; import Collection from 'calypso/devdocs/design/search-collection'; import { slugToCamelCase } from 'calypso/devdocs/docs-example/util'; import SitesGridItemExample from 'calypso/sites-dashboard/components/sites-grid-item/docs/example'; -import SitesGridItemSelectExample from 'calypso/sites-dashboard/components/sites-grid-item-select/docs/example'; export default class DesignAssets extends Component { static displayName = 'DesignAssets'; @@ -267,7 +266,6 @@ export default class DesignAssets extends Component { - diff --git a/client/gutenberg/editor/calypsoify-iframe.tsx b/client/gutenberg/editor/calypsoify-iframe.tsx index d884a73439ed9a..6ecdbfede05b92 100644 --- a/client/gutenberg/editor/calypsoify-iframe.tsx +++ b/client/gutenberg/editor/calypsoify-iframe.tsx @@ -95,7 +95,7 @@ enum WindowActions { enum EditorActions { GoToAllPosts = 'goToAllPosts', // Unused action in favor of CloseEditor. Maintained here to support cached scripts. - GoToPatterns = 'goToPatterns', + WpAdminRedirect = 'wpAdminRedirect', CloseEditor = 'closeEditor', OpenMediaModal = 'openMediaModal', OpenCheckoutModal = 'openCheckoutModal', @@ -383,7 +383,7 @@ class CalypsoifyIframe extends Component< ComponentProps, State > { this.navigate( destinationUrl, unsavedChanges ); } - if ( EditorActions.GoToPatterns === action ) { + if ( EditorActions.WpAdminRedirect === action ) { const { destinationUrl, unsavedChanges } = payload; this.navigate( `https://${ this.props.siteSlug }${ destinationUrl }`, unsavedChanges ); diff --git a/client/hosting/hosting-features/components/hosting-features.tsx b/client/hosting/hosting-features/components/hosting-features.tsx index 39800ead7de6ff..41993442b4aba6 100644 --- a/client/hosting/hosting-features/components/hosting-features.tsx +++ b/client/hosting/hosting-features/components/hosting-features.tsx @@ -63,7 +63,8 @@ const HostingFeatures = () => { // `siteTransferData?.isTransferring` is not a fully reliable indicator by itself, which is why // we also look at `siteTransferData.status` const isTransferInProgress = - siteTransferData?.isTransferring || siteTransferData?.status === transferStates.COMPLETED; + ( siteTransferData?.isTransferring || siteTransferData?.status === transferStates.COMPLETED ) && + ! isPlanExpired; useEffect( () => { if ( ! siteId ) { diff --git a/client/hosting/overview/components/plan-card.tsx b/client/hosting/overview/components/plan-card.tsx index 26be5d7af8c832..520dfbc1fff43e 100644 --- a/client/hosting/overview/components/plan-card.tsx +++ b/client/hosting/overview/components/plan-card.tsx @@ -127,8 +127,6 @@ const PricingSection: FC = () => {
{ isFreePlan && (
-
-
- - { translate( 'WordPress admin username' ) } - - ( +
+ + { translate( 'WordPress admin username' ) } + + ( + { + const trimmedValue = e.target.value.trim(); + field.onChange( trimmedValue ); + } } + onBlur={ ( e: any ) => { + field.onBlur(); + e.target.value = e.target.value.trim(); + } } + /> + ) } + /> +
+ +
+ + { translate( 'Password' ) } + + ( +
{ - const trimmedValue = e.target.value.trim(); - field.onChange( trimmedValue ); - } } - onBlur={ ( e: any ) => { - field.onBlur(); - e.target.value = e.target.value.trim(); - } } /> - ) } - /> -
-
- - { translate( 'Password' ) } - - ( -
- - -
- ) } - /> -
+ +
+ ) } + />
{ ( errors.username || errors.password ) && ( @@ -339,7 +353,7 @@ export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip { errors.backupFileLocation?.message }
) } -
+
{ translate( "Upload your file to a service like Dropbox or Google Drive to get a link. Don't forget to make sure that anyone with the link can access it." ) } @@ -348,28 +362,60 @@ export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip
) } -
- { translate( 'Notes (optional)' ) } - ( - + { ( locale === 'en' || hasTranslation( 'Special instructions' ) ) && ( + + ) } + { showNotes && ( + <> +
+ ( + + ) } + /> +
+ { errors?.notes && ( +
+ { errors.notes.message } +
+ ) } + { ( locale === 'en' || + hasTranslation( + "Please don't share any passwords or secure information in this field. We'll reach out to collect that information if you have any additional credentials to access your site." + ) ) && ( +
+ { translate( + "Please don't share any passwords or secure information in this field. We'll reach out to collect that information if you have any additional credentials to access your site." + ) } +
+ ) } + + ) }
- { errors?.notes && ( -
{ errors.notes.message }
- ) } + { errors?.root && (
{ errors.root.message }
) } diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/style.scss b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/style.scss index 57f0bfdda6030f..1703a6a5be7a70 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/style.scss +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/style.scss @@ -35,6 +35,9 @@ margin-top: 1em; flex: 1; } + .site-migration-credentials__form-field--notes { + margin-top: 5px; + } .site-migration-credentials__radio-group { display: flex; flex-direction: column; @@ -61,8 +64,9 @@ } } .site-migration-credentials__form-note { - margin-top: 1em; color: var(--color-text-subtle); + font-size: 0.875rem; + font-style: italic; } .site-migration-credentials__form-error { color: var(--color-error); @@ -97,4 +101,21 @@ margin-top: 1em; text-align: center; } + .site-migration-credentials__special-instructions { + .components-button { + padding: 0; + height: auto; + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text); + margin-top: 1rem; + } + + .site-migration-credentials__special-instructions-icon { + fill: var(--color-text) !important; + } + } + .site-migration-credentials__backup-note { + margin-top: 0.25rem; + } } diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/test/index.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/test/index.tsx index 8e107ebca2ffb3..1e82af6c899c40 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/test/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-migration-credentials/test/index.tsx @@ -41,7 +41,7 @@ describe( 'SiteMigrationCredentials', () => { expect( screen.queryByText( 'Site address' ) ).toBeInTheDocument(); expect( screen.queryByText( 'WordPress admin username' ) ).toBeInTheDocument(); expect( screen.queryByText( 'Password' ) ).toBeInTheDocument(); - expect( screen.queryByText( 'Notes (optional)' ) ).toBeInTheDocument(); + expect( screen.queryByText( 'Special instructions' ) ).toBeInTheDocument(); } ); it( 'does not show any error message by default', async () => { @@ -110,10 +110,13 @@ describe( 'SiteMigrationCredentials', () => { const submit = jest.fn(); render( { navigation: { submit } } ); + const expandNotesButton = screen.getByTestId( 'special-instructions' ); + await userEvent.click( expandNotesButton ); + const addressInput = screen.getByLabelText( /Site address/ ); const usernameInput = screen.getByLabelText( /WordPress admin username/ ); const passwordInput = screen.getByLabelText( /Password/ ); - const notesInput = screen.getByLabelText( /Notes \(optional\)/ ); + const notesInput = screen.getByTestId( 'special-instructions-textarea' ); siteAddress && ( await userEvent.type( addressInput, siteAddress ) ); username && ( await userEvent.type( usernameInput, username ) ); @@ -175,7 +178,11 @@ describe( 'SiteMigrationCredentials', () => { const addressInput = screen.getByLabelText( isBackupRequest ? /Backup file location/ : /Site address/ ); - const notesInput = screen.getByLabelText( /Notes \(optional\)/ ); + + const expandNotesButton = screen.getByTestId( 'special-instructions' ); + await userEvent.click( expandNotesButton ); + + const notesInput = screen.getByTestId( 'special-instructions-textarea' ); await userEvent.type( addressInput, siteAddress ); username && @@ -281,10 +288,13 @@ describe( 'SiteMigrationCredentials', () => { const submit = jest.fn(); render( { navigation: { submit } } ); + const expandNotesButton = screen.getByTestId( 'special-instructions' ); + await userEvent.click( expandNotesButton ); + await userEvent.type( screen.getByLabelText( /Site address/ ), 'test.com' ); await userEvent.type( screen.getByLabelText( /WordPress admin username/ ), 'username' ); await userEvent.type( screen.getByLabelText( /Password/ ), 'password' ); - await userEvent.type( screen.getByLabelText( /Notes \(optional\)/ ), 'notes' ); + await userEvent.type( screen.getByTestId( 'special-instructions-textarea' ), 'notes' ); ( wpcomRequest as jest.Mock ).mockRejectedValue( errorResponse ); diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/site-picker/site-picker.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/site-picker/site-picker.tsx index 9e28de82c9fa61..66821bc8b1b2f3 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/site-picker/site-picker.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/site-picker/site-picker.tsx @@ -86,7 +86,6 @@ const SitePicker = function SitePicker( props: Props ) { { ( selectedStatus.hiddenCount > 0 || sites.length > perPage ) && ( diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/unified-domains/index.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/unified-domains/index.tsx new file mode 100644 index 00000000000000..d06f0aacdb10c3 --- /dev/null +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/unified-domains/index.tsx @@ -0,0 +1,105 @@ +import { PLAN_PERSONAL } from '@automattic/calypso-products'; +import { usePersistedState } from '@automattic/onboarding'; +import { withShoppingCart } from '@automattic/shopping-cart'; +import { localize } from 'i18n-calypso'; +import { isEmpty } from 'lodash'; +import { connect } from 'react-redux'; +import { recordUseYourDomainButtonClick } from 'calypso/components/domains/register-domain-step/analytics'; +import { planItem } from 'calypso/lib/cart-values/cart-items'; +import CalypsoShoppingCartProvider from 'calypso/my-sites/checkout/calypso-shopping-cart-provider'; +import withCartKey from 'calypso/my-sites/checkout/with-cart-key'; +import { RenderDomainsStep, submitDomainStepSelection } from 'calypso/signup/steps/domains'; +import { recordTracksEvent } from 'calypso/state/analytics/actions'; +import { DOMAINS_WITH_PLANS_ONLY } from 'calypso/state/current-user/constants'; +import { + currentUserHasFlag, + getCurrentUser, + getCurrentUserSiteCount, + isUserLoggedIn, +} from 'calypso/state/current-user/selectors'; +import { + recordAddDomainButtonClick, + recordAddDomainButtonClickInMapDomain, + recordAddDomainButtonClickInTransferDomain, + recordAddDomainButtonClickInUseYourDomain, +} from 'calypso/state/domains/actions'; +import { getAvailableProductsList } from 'calypso/state/products-list/selectors'; +import getSitesItems from 'calypso/state/selectors/get-sites-items'; +import { fetchUsernameSuggestion } from 'calypso/state/signup/optional-dependencies/actions'; +import { removeStep } from 'calypso/state/signup/progress/actions'; +import { setDesignType } from 'calypso/state/signup/steps/design-type/actions'; +import { getDesignType } from 'calypso/state/signup/steps/design-type/selectors'; +import { getSelectedSite } from 'calypso/state/ui/selectors'; +import { ProvidedDependencies, StepProps } from '../../types'; + +const RenderDomainsStepConnect = connect( + ( state, { flow }: StepProps ) => { + const productsList = getAvailableProductsList( state ); + const productsLoaded = ! isEmpty( productsList ); + const selectedSite = getSelectedSite( state ); + const multiDomainDefaultPlan = planItem( PLAN_PERSONAL ); + const userLoggedIn = isUserLoggedIn( state as object ); + const currentUserSiteCount = getCurrentUserSiteCount( state as object ); + const stepSectionName = window.location.pathname.includes( 'use-your-domain' ) + ? 'use-your-domain' + : undefined; + + return { + designType: getDesignType( state ), + currentUser: getCurrentUser( state as object ), + productsList, + productsLoaded, + selectedSite, + isDomainOnly: false, + sites: getSitesItems( state ), + userSiteCount: currentUserSiteCount, + previousStepName: 'user', + isPlanSelectionAvailableLaterInFlow: true, + userLoggedIn, + multiDomainDefaultPlan, + domainsWithPlansOnly: currentUserHasFlag( state as object, DOMAINS_WITH_PLANS_ONLY ), + flowName: flow, + path: window.location.pathname, + positionInFlow: 1, + isReskinned: true, + stepSectionName, + }; + }, + { + recordAddDomainButtonClick, + recordAddDomainButtonClickInMapDomain, + recordAddDomainButtonClickInTransferDomain, + recordAddDomainButtonClickInUseYourDomain, + recordUseYourDomainButtonClick, + removeStep, + submitDomainStepSelection, + setDesignType, + recordTracksEvent, + fetchUsernameSuggestion, + } +)( withCartKey( withShoppingCart( localize( RenderDomainsStep ) ) ) ); + +export default function DomainsStep( props: StepProps ) { + const [ stepState, setStepState ] = usePersistedState< ProvidedDependencies >(); + + return ( + + window.location.assign( url ) } + saveSignupStep={ ( state: ProvidedDependencies ) => + setStepState( { ...stepState, ...state } ) + } + submitSignupStep={ ( state: ProvidedDependencies ) => { + setStepState( { ...stepState, ...state } ); + } } + goToNextStep={ ( state: ProvidedDependencies ) => + props.navigation.submit?.( { ...stepState, ...state } ) + } + step={ stepState } + flowName={ props.flow } + useStepperWrapper + /> + + ); +} diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/use-my-domain/index.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/use-my-domain/index.tsx index 76e0ba2f9e3732..d3cb9cf50a5ceb 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/use-my-domain/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/use-my-domain/index.tsx @@ -2,6 +2,7 @@ import { DESIGN_FIRST_FLOW, START_WRITING_FLOW, + ONBOARDING_FLOW, StepContainer, isStartWritingFlow, } from '@automattic/onboarding'; @@ -70,6 +71,7 @@ const UseMyDomain: Step = function UseMyDomain( { navigation, flow } ) { switch ( flow ) { case START_WRITING_FLOW: case DESIGN_FIRST_FLOW: + case ONBOARDING_FLOW: return getBlogOnboardingFlowStepContent(); default: return getDefaultStepContent(); diff --git a/client/landing/stepper/declarative-flow/migration/index.tsx b/client/landing/stepper/declarative-flow/migration/index.tsx index 1a1b4f8d6222eb..d5efce93ed8876 100644 --- a/client/landing/stepper/declarative-flow/migration/index.tsx +++ b/client/landing/stepper/declarative-flow/migration/index.tsx @@ -294,7 +294,7 @@ export default { useStepNavigation( currentStep, navigate ) { const stepHandlers = useCreateStepHandlers( navigate, this ); - return stepHandlers[ currentStep ]; + return stepHandlers[ currentStep ] || {}; }, useSideEffect( currentStep, navigate ) { diff --git a/client/landing/stepper/declarative-flow/onboarding.ts b/client/landing/stepper/declarative-flow/onboarding.ts index 52747ea550b596..cea94c1ab5cb53 100644 --- a/client/landing/stepper/declarative-flow/onboarding.ts +++ b/client/landing/stepper/declarative-flow/onboarding.ts @@ -19,7 +19,11 @@ const onboarding: Flow = { return stepsWithRequiredLogin( [ { slug: 'domains', - asyncComponent: () => import( './internals/steps-repository/domains' ), + asyncComponent: () => import( './internals/steps-repository/unified-domains' ), + }, + { + slug: 'use-my-domain', + asyncComponent: () => import( './internals/steps-repository/use-my-domain' ), }, { slug: 'plans', @@ -45,6 +49,8 @@ const onboarding: Flow = { useStepNavigation( currentStepSlug, navigate ) { const flowName = this.name; + const { setDomain, setDomainCartItems } = useDispatch( ONBOARD_STORE ); + const { domainCartItem, planCartItem } = useSelect( ( select: ( key: string ) => OnboardSelect ) => ( { domainCartItem: select( ONBOARD_STORE ).getDomainCartItem(), @@ -58,6 +64,13 @@ const onboarding: Flow = { const submit = async ( providedDependencies: ProvidedDependencies = {} ) => { switch ( currentStepSlug ) { case 'domains': + setDomain( providedDependencies.suggestion ); + setDomainCartItems( providedDependencies.domainCart ); + if ( providedDependencies.navigateToUseMyDomain ) { + return navigate( 'use-my-domain' ); + } + return navigate( 'plans' ); + case 'use-my-domain': return navigate( 'plans' ); case 'plans': return navigate( 'create-site', undefined, true ); @@ -68,9 +81,9 @@ const onboarding: Flow = { siteSlug: providedDependencies.siteSlug, } ); persistSignupDestination( destination ); - if ( providedDependencies.goToCheckout ) { const siteSlug = providedDependencies.siteSlug as string; + if ( planCartItem && siteSlug && flowName ) { await addPlanToCart( siteSlug, flowName, true, '', planCartItem ); } @@ -100,6 +113,8 @@ const onboarding: Flow = { const goBack = () => { switch ( currentStepSlug ) { + case 'use-my-domain': + return navigate( 'domains' ); case 'plans': return navigate( 'domains' ); default: diff --git a/client/landing/stepper/declarative-flow/site-migration-flow.ts b/client/landing/stepper/declarative-flow/site-migration-flow.ts index 63bcc14e50a931..d6452e4c23d559 100644 --- a/client/landing/stepper/declarative-flow/site-migration-flow.ts +++ b/client/landing/stepper/declarative-flow/site-migration-flow.ts @@ -311,6 +311,19 @@ const siteMigration: Flow = { // Do it for me option. if ( providedDependencies?.how === HOW_TO_MIGRATE_OPTIONS.DO_IT_FOR_ME ) { + if ( config.isEnabled( 'automated-migration/collect-credentials' ) ) { + return navigate( + addQueryArgs( + { + siteSlug, + from: fromQueryParam, + siteId, + }, + STEPS.SITE_MIGRATION_CREDENTIALS.slug + ) + ); + } + return navigate( STEPS.SITE_MIGRATION_ASSISTED_MIGRATION.slug, { siteId, siteSlug, diff --git a/client/landing/stepper/declarative-flow/test/hosted-site-migration-flow.tsx b/client/landing/stepper/declarative-flow/test/hosted-site-migration-flow.tsx index 8eab4609657b32..117006ea3b0b4f 100644 --- a/client/landing/stepper/declarative-flow/test/hosted-site-migration-flow.tsx +++ b/client/landing/stepper/declarative-flow/test/hosted-site-migration-flow.tsx @@ -1,6 +1,7 @@ /** * @jest-environment jsdom */ +import config from '@automattic/calypso-config'; import { PLAN_MIGRATION_TRIAL_MONTHLY } from '@automattic/calypso-products'; import { isCurrentUserLoggedIn } from '@automattic/data-stores/src/user/selectors'; import { waitFor } from '@testing-library/react'; @@ -180,6 +181,7 @@ describe( 'Hosted site Migration Flow', () => { } ); it( 'migrate redirects from the how-to-migrate (do it for me) page to assisted migration page', () => { + config.disable( 'automated-migration/collect-credentials' ); const { runUseStepNavigationSubmit } = renderFlow( hostedSiteMigrationFlow ); runUseStepNavigationSubmit( { @@ -196,6 +198,24 @@ describe( 'Hosted site Migration Flow', () => { siteSlug: 'example.wordpress.com', }, } ); + config.enable( 'automated-migration/collect-credentials' ); + } ); + + it( 'migrate redirects from the how-to-migrate (do it for me) page to credential collection step', () => { + const { runUseStepNavigationSubmit } = renderFlow( hostedSiteMigrationFlow ); + + runUseStepNavigationSubmit( { + currentStep: STEPS.SITE_MIGRATION_HOW_TO_MIGRATE.slug, + dependencies: { + destination: 'migrate', + how: HOW_TO_MIGRATE_OPTIONS.DO_IT_FOR_ME, + }, + } ); + + expect( getFlowLocation() ).toEqual( { + path: `/${ STEPS.SITE_MIGRATION_CREDENTIALS.slug }?siteSlug=example.wordpress.com`, + state: null, + } ); } ); it( 'migrate redirects from the how-to-migrate (upgrade needed) page to site-migration-upgrade-plan step', () => { diff --git a/client/landing/stepper/declarative-flow/test/site-migration-flow.tsx b/client/landing/stepper/declarative-flow/test/site-migration-flow.tsx index 6d7358500421ba..f97eb38ad263f0 100644 --- a/client/landing/stepper/declarative-flow/test/site-migration-flow.tsx +++ b/client/landing/stepper/declarative-flow/test/site-migration-flow.tsx @@ -224,6 +224,7 @@ describe( 'Site Migration Flow', () => { } ); it( 'migrate redirects from the how-to-migrate (do it for me) page to assisted migration page', () => { + config.disable( 'automated-migration/collect-credentials' ); const { runUseStepNavigationSubmit } = renderFlow( siteMigrationFlow ); runUseStepNavigationSubmit( { @@ -240,6 +241,24 @@ describe( 'Site Migration Flow', () => { siteSlug: 'example.wordpress.com', }, } ); + config.enable( 'automated-migration/collect-credentials' ); + } ); + + it( 'migrate redirects from the how-to-migrate (do it for me) page to credential collection step', () => { + const { runUseStepNavigationSubmit } = renderFlow( siteMigrationFlow ); + + runUseStepNavigationSubmit( { + currentStep: STEPS.SITE_MIGRATION_HOW_TO_MIGRATE.slug, + dependencies: { + destination: 'migrate', + how: HOW_TO_MIGRATE_OPTIONS.DO_IT_FOR_ME, + }, + } ); + + expect( getFlowLocation() ).toEqual( { + path: `/${ STEPS.SITE_MIGRATION_CREDENTIALS.slug }?siteSlug=example.wordpress.com`, + state: null, + } ); } ); it( 'migrate redirects from the how-to-migrate (upgrade needed) page to site-migration-upgrade-plan step', () => { diff --git a/client/landing/stepper/index.tsx b/client/landing/stepper/index.tsx index ea6c04c4986ada..df20ca49f7c953 100644 --- a/client/landing/stepper/index.tsx +++ b/client/landing/stepper/index.tsx @@ -4,7 +4,12 @@ import { initializeAnalytics } from '@automattic/calypso-analytics'; import { CurrentUser } from '@automattic/calypso-analytics/dist/types/utils/current-user'; import config from '@automattic/calypso-config'; import { User as UserStore } from '@automattic/data-stores'; -import { IMPORT_HOSTED_SITE_FLOW } from '@automattic/onboarding'; +import { + HOSTED_SITE_MIGRATION_FLOW, + MIGRATION_FLOW, + MIGRATION_SIGNUP_FLOW, + SITE_MIGRATION_FLOW, +} from '@automattic/onboarding'; import { QueryClientProvider } from '@tanstack/react-query'; import { useDispatch } from '@wordpress/data'; import defaultCalypsoI18n from 'i18n-calypso'; @@ -26,6 +31,8 @@ import { getInitialState, getStateFromCache, persistOnChange } from 'calypso/sta import { createQueryClient } from 'calypso/state/query-client'; import initialReducer from 'calypso/state/reducer'; import { setStore } from 'calypso/state/redux-store'; +import { setCurrentFlowName } from 'calypso/state/signup/flow/actions'; +import { setSelectedSiteId } from 'calypso/state/ui/actions'; import { FlowRenderer } from './declarative-flow/internals'; import { AsyncHelpCenter } from './declarative-flow/internals/components'; import 'calypso/components/environment-badge/style.scss'; @@ -37,6 +44,7 @@ import { enhanceFlowWithAuth } from './utils/enhanceFlowWithAuth'; import { startStepperPerformanceTracking } from './utils/performance-tracking'; import { WindowLocaleEffectManager } from './utils/window-locale-effect-manager'; import type { Flow } from './declarative-flow/internals/types'; +import type { AnyAction } from 'redux'; declare const window: AppWindow; @@ -76,8 +84,15 @@ const getFlowFromURL = () => { return fromPath || fromQuery; }; +const HOTJAR_ENABLED_FLOWS = [ + MIGRATION_FLOW, + SITE_MIGRATION_FLOW, + HOSTED_SITE_MIGRATION_FLOW, + MIGRATION_SIGNUP_FLOW, +]; + const initializeHotJar = ( flowName: string ) => { - if ( flowName === IMPORT_HOSTED_SITE_FLOW ) { + if ( HOTJAR_ENABLED_FLOWS.includes( flowName ) ) { addHotJarScript(); } }; @@ -124,6 +139,11 @@ window.AppBoot = async () => { const { default: rawFlow } = await flowLoader(); const flow = rawFlow.__experimentalUseBuiltinAuth ? enhanceFlowWithAuth( rawFlow ) : rawFlow; + // When re-using steps from /start, we need to set the current flow name in the redux store, since some depend on it. + reduxStore.dispatch( setCurrentFlowName( flow.name ) ); + // Reset the selected site ID when the stepper is loaded. + reduxStore.dispatch( setSelectedSiteId( null ) as unknown as AnyAction ); + const root = createRoot( document.getElementById( 'wpcom' ) as HTMLElement ); root.render( diff --git a/client/layout/color-scheme/index.jsx b/client/layout/color-scheme/index.jsx new file mode 100644 index 00000000000000..c9338a02ffa00f --- /dev/null +++ b/client/layout/color-scheme/index.jsx @@ -0,0 +1,45 @@ +import { getAdminColor } from 'calypso/state/admin-color/selectors'; +import { getPreference } from 'calypso/state/preferences/selectors'; +import { getSelectedSiteId } from 'calypso/state/ui/selectors'; + +export function getColorScheme( { state, isGlobalSidebarVisible, sectionName } ) { + if ( isGlobalSidebarVisible ) { + return 'global'; + } + if ( sectionName === 'checkout' ) { + return null; + } + const calypsoColorScheme = getPreference( state, 'colorScheme' ); + const siteId = getSelectedSiteId( state ); + const siteColorScheme = getAdminColor( state, siteId ); + + return siteColorScheme ?? calypsoColorScheme; +} + +export function refreshColorScheme( prevColorScheme, nextColorScheme ) { + if ( typeof document === 'undefined' ) { + return; + } + if ( prevColorScheme === nextColorScheme ) { + return; + } + + const classList = document.querySelector( 'body' ).classList; + + if ( prevColorScheme ) { + classList.remove( `is-${ prevColorScheme }` ); + } + + if ( nextColorScheme ) { + classList.add( `is-${ nextColorScheme }` ); + + const themeColor = getComputedStyle( document.body ) + .getPropertyValue( '--color-masterbar-background' ) + .trim(); + const themeColorMeta = document.querySelector( 'meta[name="theme-color"]' ); + // We only adjust the `theme-color` meta content value in case we set it in `componentDidMount` + if ( themeColorMeta && themeColorMeta.getAttribute( 'data-colorscheme' ) === 'true' ) { + themeColorMeta.content = themeColor; + } + } +} diff --git a/client/layout/index.jsx b/client/layout/index.jsx index 6f6e6c18e375f4..63cf3594372c0f 100644 --- a/client/layout/index.jsx +++ b/client/layout/index.jsx @@ -35,7 +35,6 @@ import isReaderTagEmbedPage from 'calypso/lib/reader/is-reader-tag-embed-page'; import { getMessagePathForJITM } from 'calypso/lib/route'; import UserVerificationChecker from 'calypso/lib/user/verification-checker'; import { useSelector } from 'calypso/state'; -import { getAdminColor } from 'calypso/state/admin-color/selectors'; import { isOffline } from 'calypso/state/application/selectors'; import { isUserLoggedIn, getCurrentUser } from 'calypso/state/current-user/selectors'; import { @@ -45,7 +44,6 @@ import { } from 'calypso/state/global-sidebar/selectors'; import { isUserNewerThan, WEEK_IN_MILLISECONDS } from 'calypso/state/guided-tours/contexts'; import { getCurrentOAuth2Client } from 'calypso/state/oauth2-clients/ui/selectors'; -import { getPreference } from 'calypso/state/preferences/selectors'; import getCurrentQueryArguments from 'calypso/state/selectors/get-current-query-arguments'; import getIsBlazePro from 'calypso/state/selectors/get-is-blaze-pro'; import getPrimarySiteSlug from 'calypso/state/selectors/get-primary-site-slug'; @@ -63,6 +61,7 @@ import { masterbarIsVisible, } from 'calypso/state/ui/selectors'; import BodySectionCssClass from './body-section-css-class'; +import { getColorScheme, refreshColorScheme } from './color-scheme'; import GlobalNotifications from './global-notifications'; import LayoutLoader from './loader'; import { shouldLoadInlineHelp, handleScroll } from './utils'; @@ -245,70 +244,11 @@ class Layout extends Component { this.setState( { isDesktop } ); } ); - this.refreshColorScheme( undefined, this.props.colorScheme ); + refreshColorScheme( undefined, this.props.colorScheme ); } - /** - * Refresh the color scheme if - * - the color scheme has changed - * - the global sidebar is visible and the color scheme is not `global` - * - the global sidebar was visible and is now hidden and the color scheme is not `global` - * - the section changed to `checkout` or changes from 'checkout' to something else - * @param prevProps object - */ componentDidUpdate( prevProps ) { - const willTransitionFromOrToCheckout = - ( prevProps.sectionName === 'checkout' && this.props.sectionName !== 'checkout' ) || - ( prevProps.sectionName !== 'checkout' && this.props.sectionName === 'checkout' ); - - if ( - prevProps.colorScheme !== this.props.colorScheme || - ( this.props.isGlobalSidebarVisible && this.props.colorScheme !== 'global' ) || - ( prevProps.isGlobalSidebarVisible && - ! this.props.isGlobalSidebarVisible && - this.props.colorScheme !== 'global' ) || - willTransitionFromOrToCheckout - ) { - this.refreshColorScheme( prevProps.colorScheme, this.props.colorScheme ); - } - } - - refreshColorScheme( prevColorScheme, nextColorScheme ) { - if ( ! config.isEnabled( 'me/account/color-scheme-picker' ) ) { - return; - } - - if ( typeof document !== 'undefined' ) { - const classList = document.querySelector( 'body' ).classList; - const globalColorScheme = 'global'; - - if ( this.props.sectionName === 'checkout' ) { - classList.remove( `is-${ prevColorScheme }` ); - return; - } - - if ( this.props.isGlobalSidebarVisible ) { - // Force the global color scheme when the global sidebar is visible. - nextColorScheme = globalColorScheme; - } else { - // Revert back to user's color scheme when the global sidebar is gone. - prevColorScheme = globalColorScheme; - } - - classList.remove( `is-${ prevColorScheme }` ); - classList.add( `is-${ nextColorScheme }` ); - - const themeColor = getComputedStyle( document.body ) - .getPropertyValue( '--color-masterbar-background' ) - .trim(); - const themeColorMeta = document.querySelector( 'meta[name="theme-color"]' ); - // We only adjust the `theme-color` meta content value in case we set it in `componentDidMount` - if ( themeColorMeta && themeColorMeta.getAttribute( 'data-colorscheme' ) === 'true' ) { - themeColorMeta.content = themeColor; - } - } - - // intentionally don't remove these in unmount + refreshColorScheme( prevProps.colorScheme, this.props.colorScheme ); } renderMasterbar( loadHelpCenterIcon ) { @@ -564,15 +504,16 @@ export default withCurrentRoute( 'comments', ].includes( sectionName ); const sidebarIsHidden = ! secondary || isWcMobileApp() || isDomainAndPlanPackageFlow; + const isGlobalSidebarVisible = shouldShowGlobalSidebar && ! sidebarIsHidden; + const userAllowedToHelpCenter = config.isEnabled( 'calypso/help-center' ) && ! getIsOnboardingAffiliateFlow( state ); - const calypsoColorScheme = getPreference( state, 'colorScheme' ); - const siteColorScheme = getAdminColor( state, siteId ) ?? calypsoColorScheme; - const colorScheme = - shouldShowUnifiedSiteSidebar || config.isEnabled( 'layout/site-level-user-profile' ) - ? siteColorScheme - : calypsoColorScheme; + const colorScheme = getColorScheme( { + state, + sectionName, + isGlobalSidebarVisible, + } ); return { masterbarIsHidden, @@ -606,7 +547,7 @@ export default withCurrentRoute( sidebarIsCollapsed: sectionName !== 'reader' && getSidebarIsCollapsed( state ), userAllowedToHelpCenter, currentRoute, - isGlobalSidebarVisible: shouldShowGlobalSidebar && ! sidebarIsHidden, + isGlobalSidebarVisible, isGlobalSidebarCollapsed: shouldShowCollapsedGlobalSidebar && ! sidebarIsHidden, isUnifiedSiteSidebarVisible: shouldShowUnifiedSiteSidebar && ! sidebarIsHidden, isNewUser: isUserNewerThan( WEEK_IN_MILLISECONDS )( state ), diff --git a/client/layout/logged-out.jsx b/client/layout/logged-out.jsx index 2b606f93e8e99b..85112d82e68bec 100644 --- a/client/layout/logged-out.jsx +++ b/client/layout/logged-out.jsx @@ -193,19 +193,16 @@ const LayoutLoggedOut = ( { 'subscriptions', 'theme', 'themes', - 'start-with', ].includes( sectionName ) && ! isReaderTagPage && ! isReaderSearchPage && ! isReaderDiscoverPage ) { const nonMonochromeSections = [ 'plugins' ]; - const whiteNavbarSections = [ 'start-with' ]; const className = clsx( { 'is-style-monochrome': isEnabled( 'site-profiler/metrics' ) && ! nonMonochromeSections.includes( sectionName ), - 'is-style-white': whiteNavbarSections.includes( sectionName ), } ); masterbar = ( @@ -217,9 +214,6 @@ const LayoutLoggedOut = ( { ! nonMonochromeSections.includes( sectionName ) && { logoColor: 'white', } ) } - { ...( whiteNavbarSections.includes( sectionName ) && { - logoColor: 'black', - } ) } { ...( sectionName === 'subscriptions' && { variant: 'minimal' } ) } { ...( sectionName === 'patterns' && { startUrl: getPatternLibraryOnboardingUrl( locale, isLoggedIn ), diff --git a/client/layout/masterbar/crowdsignal.scss b/client/layout/masterbar/crowdsignal.scss index e75c233b85abf4..bf5f0c5bc19454 100644 --- a/client/layout/masterbar/crowdsignal.scss +++ b/client/layout/masterbar/crowdsignal.scss @@ -5,8 +5,8 @@ --color-primary: var(--studio-wordpress-blue-90); --color-primary-dark: var(--studio-wordpress-blue-100); - --color-accent: var(--studio-pink-50); - --color-accent-dark: var(--studio-pink-70); + --color-accent: var(--studio-wordpress-blue); + --color-accent-dark: var(--studio-wordpress-blue-70); --color-border: var(--studio-gray-5); --color-surface: var(--studio-white); diff --git a/client/layout/masterbar/logged-in.jsx b/client/layout/masterbar/logged-in.jsx index 1123f86f884a6f..cdd4e56fb89937 100644 --- a/client/layout/masterbar/logged-in.jsx +++ b/client/layout/masterbar/logged-in.jsx @@ -4,6 +4,8 @@ import page from '@automattic/calypso-router'; import { PromptIcon } from '@automattic/command-palette'; import { Button, Popover } from '@automattic/components'; import { isWithinBreakpoint, subscribeIsWithinBreakpoint } from '@automattic/viewport'; +import { Button as WPButton } from '@wordpress/components'; +import { debounce } from '@wordpress/compose'; import { Icon, category } from '@wordpress/icons'; import { localize } from 'i18n-calypso'; import PropTypes from 'prop-types'; @@ -66,6 +68,7 @@ import Notifications from './masterbar-notifications/notifications-button'; const NEW_MASTERBAR_SHIPPING_DATE = new Date( 2022, 3, 14 ).getTime(); const MENU_POPOVER_PREFERENCE_KEY = 'dismissible-card-masterbar-collapsable-menu-popover'; +const READER_POPOVER_PREFERENCE_KEY = 'dismissible-card-masterbar-reader-popover'; const MOBILE_BREAKPOINT = '<480px'; const IS_RESPONSIVE_MENU_BREAKPOINT = '<782px'; @@ -78,6 +81,8 @@ class MasterbarLoggedIn extends Component { isResponsiveMenu: isWithinBreakpoint( IS_RESPONSIVE_MENU_BREAKPOINT ), // making the ref a state triggers a re-render when it changes (needed for popover) menuBtnRef: null, + readerBtnRef: null, + readerPosition: null, }; static propTypes = { @@ -93,6 +98,7 @@ class MasterbarLoggedIn extends Component { isCheckoutFailed: PropTypes.bool, isInEditor: PropTypes.bool, hasDismissedThePopover: PropTypes.bool, + hasDismissedReaderPopover: PropTypes.bool, isUserNewerThanNewNavigation: PropTypes.bool, loadHelpCenterIcon: PropTypes.bool, isGlobalSidebarVisible: PropTypes.bool, @@ -121,6 +127,27 @@ class MasterbarLoggedIn extends Component { } }; + setupReaderPositionObserver() { + if ( this.props.sectionName !== 'home' && this.props.sectionGroup !== 'sites-dashboard' ) { + return; + } + const readerItem = document.querySelector( '.masterbar__reader' ); + this.resizeObserver = new ResizeObserver( + debounce( () => { + const newRect = readerItem.getBoundingClientRect(); + const readerPositionChanged = + ! this.lastReaderPosition || + newRect.left !== this.lastReaderPosition.left || + newRect.top !== this.lastReaderPosition.top; + if ( readerPositionChanged ) { + this.setState( { readerPosition: newRect } ); + this.lastReaderPosition = newRect; + } + }, 100 ) + ); + this.resizeObserver.observe( readerItem ); + } + componentDidMount() { // Give a chance to direct URLs to open the sidebar on page load ( eg by clicking 'me' in wp-admin ). const qryString = parse( document.location.search.replace( /^\?/, '' ) ); @@ -134,12 +161,20 @@ class MasterbarLoggedIn extends Component { }; document.addEventListener( 'keydown', this.actionSearchShortCutListener ); this.subscribeToViewPortChanges(); + + // Observe if the position of the reader item has changed. + // Re-render to update the reader tooltip position. + this.setupReaderPositionObserver(); } componentWillUnmount() { document.removeEventListener( 'keydown', this.actionSearchShortCutListener ); this.unsubscribeToViewPortChanges?.(); this.unsubscribeResponsiveMenuViewPortChanges?.(); + + if ( this.resizeObserver ) { + this.resizeObserver.disconnect(); + } } handleToggleMobileMenu = () => { @@ -319,6 +354,10 @@ class MasterbarLoggedIn extends Component { this.props.savePreference( MENU_POPOVER_PREFERENCE_KEY, true ); }; + dismissReaderPopover = () => { + this.props.savePreference( READER_POPOVER_PREFERENCE_KEY, true ); + }; + renderCheckout() { const { isCheckoutPending, @@ -520,30 +559,63 @@ class MasterbarLoggedIn extends Component { } renderReader() { - const { translate } = this.props; + const { translate, sectionName, sectionGroup, isFetchingPrefs, hasDismissedReaderPopover } = + this.props; + const { readerBtnRef } = this.state; return ( - + + + + } + onClick={ this.clickReader } + isActive={ this.isActive( 'reader', true ) } + tooltip={ translate( 'Read the blogs and topics you follow' ) } + preloadSection={ this.preloadReader } + ref={ ( ref ) => ref !== readerBtnRef && this.setState( { readerBtnRef: ref } ) } + hasGlobalBorderStyle + /> + { readerBtnRef && ( + - - - } - onClick={ this.clickReader } - isActive={ this.isActive( 'reader', true ) } - tooltip={ translate( 'Read the blogs and topics you follow' ) } - preloadSection={ this.preloadReader } - hasGlobalBorderStyle - /> +

+ { translate( "We've moved the Reader!", { + comment: 'This is a popover title', + } ) } +

+

+ { translate( 'Click the eyeglasses icon to check it out.' ) } +

+
+ + { translate( 'Got it', { comment: 'Got it, as in OK' } ) } + +
+
+ ) } + ); } @@ -858,6 +930,7 @@ class MasterbarLoggedIn extends Component { export default connect( ( state ) => { const sectionGroup = getSectionGroup( state ); + const sectionName = getSectionName( state ); // Falls back to using the user's primary site if no site has been selected // by the user yet @@ -881,6 +954,7 @@ export default connect( siteAdminUrl: getSiteAdminUrl( state, siteId ), siteHomeUrl: getSiteHomeUrl( state, siteId ), sectionGroup, + sectionName, domainOnlySite: isDomainOnlySite( state, siteId ), hasNoSites: siteCount === 0, user: getCurrentUser( state ), @@ -894,6 +968,7 @@ export default connect( isJetpackNotAtomic: isJetpackSite( state, siteId ) && ! isAtomicSite( state, siteId ), currentLayoutFocus: getCurrentLayoutFocus( state ), hasDismissedThePopover: getPreference( state, MENU_POPOVER_PREFERENCE_KEY ), + hasDismissedReaderPopover: getPreference( state, READER_POPOVER_PREFERENCE_KEY ), isFetchingPrefs: isFetchingPreferences( state ), // If the user is newer than new navigation shipping date, don't tell them this nav is new. Everything is new to them. isUserNewerThanNewNavigation: diff --git a/client/layout/masterbar/masterbar-cart/masterbar-cart-count.scss b/client/layout/masterbar/masterbar-cart/masterbar-cart-count.scss index dfc5826fd7852f..624e8b2bccd353 100644 --- a/client/layout/masterbar/masterbar-cart/masterbar-cart-count.scss +++ b/client/layout/masterbar/masterbar-cart/masterbar-cart-count.scss @@ -7,7 +7,7 @@ text-align: center; min-width: 0.5rem; border-radius: 50%; - background-color: #c9356e; + background-color: var(--studio-wordpress-blue); top: 5px; position: relative; diff --git a/client/layout/masterbar/style.scss b/client/layout/masterbar/style.scss index bf27a175f37d3a..0612c479b0d86f 100644 --- a/client/layout/masterbar/style.scss +++ b/client/layout/masterbar/style.scss @@ -1491,3 +1491,56 @@ a.masterbar__quick-language-switcher { opacity: 1; } } + +.popover.masterbar__reader-popover { + z-index: 176; + + .popover__arrow::before { + --color-border-inverted: var(--color-neutral-100); + } + + .popover__arrow { + border: 10px dashed var(--color-neutral-70) !important; + border-bottom-style: solid !important; + border-top: none !important; + border-left-color: transparent !important; + border-right-color: transparent !important; + } + .popover__inner { + display: flex; + gap: 16px; + padding: 16px; + flex-direction: column; + align-items: flex-start; + border-radius: 4px !important; + background-color: var(--color-neutral-100) !important; + border: 1px solid var(--color-neutral-70) !important; + left: 0 !important; + } + + .masterbar__reader-popover-heading { + font-size: rem(16px); + color: var(--color-text-inverted); + font-weight: 500; + } + + .masterbar__reader-popover-description { + max-width: 325px; + margin-top: -8px; + margin-bottom: 0; + font-size: rem(13px); + color: var(--color-neutral-0); + text-align: left; + } + + .masterbar__reader-popover-actions { + display: flex; + align-items: center; + justify-content: flex-end; + width: 100%; + button { + padding: 4px 8px; + font-size: rem(13px); + } + } +} diff --git a/client/lib/checkout/payment-methods.tsx b/client/lib/checkout/payment-methods.tsx index b781946830a3c2..8b6a4a40611203 100644 --- a/client/lib/checkout/payment-methods.tsx +++ b/client/lib/checkout/payment-methods.tsx @@ -65,6 +65,7 @@ export interface StoredPaymentMethodCard extends StoredPaymentMethodBase { card_iin: string; card_last_4: string; card_zip: string; + display_brand: string | null; } export interface StoredPaymentMethodEbanx extends StoredPaymentMethodBase { @@ -117,7 +118,7 @@ interface ImagePathsMap { const CREDIT_CARD_SELECTED_PATHS: ImagePathsMap = { amex: creditCardAmexImage, - cb: creditCardCartesBancairesImage, + cartes_bancaires: creditCardCartesBancairesImage, diners: creditCardDinersImage, discover: creditCardDiscoverImage, jcb: creditCardJCBImage, @@ -160,8 +161,7 @@ export const PaymentMethodSummary = ( { displayType = translate( 'American Express' ); break; - case 'cartes bancaires': - case 'cb': + case 'cartes_bancaires': displayType = translate( 'Cartes Bancaires' ); break; diff --git a/client/lib/purchases/index.ts b/client/lib/purchases/index.ts index d474f2d578db1b..d9ebae4e99f1fa 100644 --- a/client/lib/purchases/index.ts +++ b/client/lib/purchases/index.ts @@ -851,12 +851,15 @@ export function subscribedWithinPastWeek( purchase: Purchase ) { /** * Returns the payment logo to display based on the payment method + * 'displayBrand' respects the customer's card brand choice if available * @param {Object} purchase - the purchase with which we are concerned * @returns {string|null} the payment logo type, or null if no payment type is set. */ export function paymentLogoType( purchase: Purchase ): string | null | undefined { if ( isPaidWithCreditCard( purchase ) ) { - return purchase.payment.creditCard?.type; + return purchase.payment.creditCard?.displayBrand + ? purchase.payment.creditCard?.displayBrand + : purchase.payment.creditCard?.type; } if ( isPaidWithPayPalDirect( purchase ) ) { diff --git a/client/login/magic-login/index.jsx b/client/login/magic-login/index.jsx index 2606efdfaf7580..7e69949a6c2ac7 100644 --- a/client/login/magic-login/index.jsx +++ b/client/login/magic-login/index.jsx @@ -612,6 +612,9 @@ class MagicLogin extends Component { const eventOptions = { client_id: oauth2Client.id, client_name: oauth2Client.title }; const isFromGravatar3rdPartyApp = isGravatarOAuth2Client( oauth2Client ) && query?.gravatar_from === '3rd-party'; + const isGravatarFlowWithEmail = !! ( + isGravatarFlowOAuth2Client( oauth2Client ) && query?.email_address + ); this.emailToSha256( usernameOrEmail ).then( ( email ) => this.setState( { hashedEmail: email } ) @@ -713,7 +716,7 @@ class MagicLogin extends Component { { translate( 'Continue' ) }
- { ! isFromGravatar3rdPartyApp && ( + { ! isFromGravatar3rdPartyApp && ! isGravatarFlowWithEmail && ( @@ -746,6 +749,9 @@ class MagicLogin extends Component { } = this.state; const isFromGravatar3rdPartyApp = isGravatarOAuth2Client( oauth2Client ) && query?.gravatar_from === '3rd-party'; + const isGravatarFlowWithEmail = !! ( + isGravatarFlowOAuth2Client( oauth2Client ) && query?.email_address + ); const isProcessingCode = isValidatingCode || isCodeValidated; let errorText = translate( 'Something went wrong. Please try again.' ); @@ -844,7 +850,7 @@ class MagicLogin extends Component { args: { countdown: resendEmailCountdown }, } ) } - { ! isFromGravatar3rdPartyApp && ( + { ! isFromGravatar3rdPartyApp && ! isGravatarFlowWithEmail && (
); }; diff --git a/client/me/purchases/cancel-purchase/style.scss b/client/me/purchases/cancel-purchase/style.scss index 41ee1f54d03ab4..dcce1f292c2372 100644 --- a/client/me/purchases/cancel-purchase/style.scss +++ b/client/me/purchases/cancel-purchase/style.scss @@ -53,6 +53,12 @@ font-size: $font-body-small; } +.cancel-purchase__support-information .support-link { + color: var(--color-primary); + font-weight: 600; + padding-inline-start: 0; +} + .cancel-purchase__site-title { font-size: $font-body-extra-small; text-transform: uppercase; diff --git a/client/me/purchases/confirm-cancel-domain/index.jsx b/client/me/purchases/confirm-cancel-domain/index.jsx index a05e0c434a5072..60d976aada6d1e 100644 --- a/client/me/purchases/confirm-cancel-domain/index.jsx +++ b/client/me/purchases/confirm-cancel-domain/index.jsx @@ -1,11 +1,12 @@ import { isDomainRegistration } from '@automattic/calypso-products'; import page from '@automattic/calypso-router'; import { Card, FormLabel } from '@automattic/components'; -import { localize } from 'i18n-calypso'; +import i18n, { getLocaleSlug, localize } from 'i18n-calypso'; import { map, find } from 'lodash'; import PropTypes from 'prop-types'; import { Component, Fragment } from 'react'; import { connect } from 'react-redux'; +import ActionPanelLink from 'calypso/components/action-panel/link'; import QueryUserPurchases from 'calypso/components/data/query-user-purchases'; import FormButton from 'calypso/components/forms/form-button'; import FormCheckbox from 'calypso/components/forms/form-checkbox'; @@ -122,12 +123,30 @@ class ConfirmCancelDomain extends Component { } if ( error ) { - this.props.errorNotice( - error.message || + if ( + getLocaleSlug() === 'en' || + getLocaleSlug() === 'en-gb' || + i18n.hasTranslation( + 'Unable to cancel your purchase. Please try again later or {{a}}contact support{{/a}}.' + ) + ) { + this.props.errorNotice( + translate( + 'Unable to cancel your purchase. Please try again later or {{a}}contact support{{/a}}.', + { + components: { + a: , + }, + } + ) + ); + } else { + this.props.errorNotice( translate( 'Unable to cancel your purchase. Please try again later or contact support.' ) - ); + ); + } return; } diff --git a/client/me/purchases/manage-purchase/change-payment-method/use-create-assignable-payment-methods.tsx b/client/me/purchases/manage-purchase/change-payment-method/use-create-assignable-payment-methods.tsx index a702512816a009..35f517c130f8fb 100644 --- a/client/me/purchases/manage-purchase/change-payment-method/use-create-assignable-payment-methods.tsx +++ b/client/me/purchases/manage-purchase/change-payment-method/use-create-assignable-payment-methods.tsx @@ -9,6 +9,8 @@ import { } from 'calypso/my-sites/checkout/src/hooks/use-create-payment-methods'; import { useStoredPaymentMethods } from 'calypso/my-sites/checkout/src/hooks/use-stored-payment-methods'; import { translateCheckoutPaymentMethodToWpcomPaymentMethod } from 'calypso/my-sites/checkout/src/lib/translate-payment-method-names'; +import { useSelector } from 'calypso/state'; +import { getCurrentUserCurrencyCode } from 'calypso/state/currency-code/selectors'; import { PaymentMethodSelectorSubmitButtonContent } from '../payment-method-selector/payment-method-selector-submit-button-content'; import useFetchAvailablePaymentMethods from './use-fetch-available-payment-methods'; import type { PaymentMethod } from '@automattic/composite-checkout'; @@ -26,6 +28,7 @@ export default function useCreateAssignablePaymentMethods( ): PaymentMethod[] { const translate = useTranslate(); const { isStripeLoading, stripeLoadingError } = useStripe(); + const currency = useSelector( getCurrentUserCurrencyCode ); const { isFetching: isLoadingAllowedPaymentMethods, @@ -47,6 +50,7 @@ export default function useCreateAssignablePaymentMethods( } ); const hasExistingCardMethods = existingCardMethods && existingCardMethods.length > 0; const stripeMethod = useCreateCreditCard( { + currency, isStripeLoading, stripeLoadingError, shouldUseEbanx: false, diff --git a/client/me/purchases/manage-purchase/style.scss b/client/me/purchases/manage-purchase/style.scss index 6651edde380e44..97b6261a17ef13 100644 --- a/client/me/purchases/manage-purchase/style.scss +++ b/client/me/purchases/manage-purchase/style.scss @@ -342,6 +342,7 @@ .payment-logo { margin-right: 5px; + border-radius: 2px; } a { @@ -354,7 +355,7 @@ } &.is-expiring .manage-purchase__detail-date-span { - color: var(--studio-pink-50); + color: var(--studio-red); } } diff --git a/client/me/purchases/payment-methods/payment-method-details.tsx b/client/me/purchases/payment-methods/payment-method-details.tsx index 28d45fd1171544..9e36afbf8479ba 100644 --- a/client/me/purchases/payment-methods/payment-method-details.tsx +++ b/client/me/purchases/payment-methods/payment-method-details.tsx @@ -12,6 +12,7 @@ import 'calypso/me/purchases/payment-methods/style.scss'; interface Props { lastDigits?: string; + displayBrand?: string | null; cardType?: string; name: string; expiry?: string; @@ -24,6 +25,7 @@ interface Props { const PaymentMethodDetails: FunctionComponent< Props > = ( { lastDigits, + displayBrand, cardType, name, expiry, @@ -39,7 +41,7 @@ const PaymentMethodDetails: FunctionComponent< Props > = ( { const expirationDate = expiry ? moment( expiry, moment.ISO_8601, true ) : null; const displayExpirationDate = expirationDate?.isValid() ? expirationDate.format( 'MM/YY' ) : null; - const type = cardType?.toLocaleLowerCase() || paymentPartner || ''; + const type = displayBrand ?? ( cardType?.toLocaleLowerCase() || paymentPartner || '' ); return (
diff --git a/client/me/purchases/payment-methods/payment-method.tsx b/client/me/purchases/payment-methods/payment-method.tsx index f6c22fdf0f08ab..212ebe28d43e42 100644 --- a/client/me/purchases/payment-methods/payment-method.tsx +++ b/client/me/purchases/payment-methods/payment-method.tsx @@ -21,6 +21,9 @@ export default function PaymentMethod( { paymentMethod }: { paymentMethod: Store { { purchase.payment.creditCard.number } diff --git a/client/me/style.scss b/client/me/style.scss index 7cda47ebfef403..6767c0d2246f69 100644 --- a/client/me/style.scss +++ b/client/me/style.scss @@ -35,7 +35,6 @@ body.is-section-me { .main { padding: 24px; border-block-end: 1px solid var(--studio-gray-0); - height: calc(100vh - var(--masterbar-height) - var(--content-padding-top) - var(--content-padding-bottom)); } background: var(--color-surface); border-radius: 8px; /* stylelint-disable-line scales/radii */ diff --git a/client/my-sites/checkout/checkout-main-wrapper.tsx b/client/my-sites/checkout/checkout-main-wrapper.tsx index 2bffaa88037073..c865591001616b 100644 --- a/client/my-sites/checkout/checkout-main-wrapper.tsx +++ b/client/my-sites/checkout/checkout-main-wrapper.tsx @@ -23,6 +23,10 @@ const logCheckoutError = ( error: Error ) => { const CheckoutMainWrapperStyles = styled.div` background-color: ${ colorStudio.colors[ 'White' ] }; + + a { + color: ${ colorStudio.colors[ 'WordPress Blue 50' ] }; + } `; export default function CheckoutMainWrapper( { diff --git a/client/my-sites/checkout/src/components/akismet-pro-quantity-dropdown/index.tsx b/client/my-sites/checkout/src/components/akismet-pro-quantity-dropdown/index.tsx index 8f12a89df05d8b..1b29c4209a74f2 100644 --- a/client/my-sites/checkout/src/components/akismet-pro-quantity-dropdown/index.tsx +++ b/client/my-sites/checkout/src/components/akismet-pro-quantity-dropdown/index.tsx @@ -73,11 +73,11 @@ const Option = styled.li` cursor: pointer; &:hover { - background: #e9f0f5; + background: var( --studio-wordpress-blue-5 ); } &.item-variant-option--selected { - background: #055d9c; + background: var( --studio-wordpress-blue-50 ); color: white; } `; diff --git a/client/my-sites/checkout/src/components/checkout-main-content.tsx b/client/my-sites/checkout/src/components/checkout-main-content.tsx index 59c6846b780104..ef220ce83150e2 100644 --- a/client/my-sites/checkout/src/components/checkout-main-content.tsx +++ b/client/my-sites/checkout/src/components/checkout-main-content.tsx @@ -1144,6 +1144,10 @@ const WPCheckoutMainContent = styled.div` padding: 0 24px 0 64px; } } + + .editor-checkout-modal & { + margin-top: 20px; + } `; const WPCheckoutSidebarContent = styled.div` @@ -1163,6 +1167,14 @@ const WPCheckoutSidebarContent = styled.div` padding: 144px 64px 0 24px; } } + + .editor-checkout-modal & { + padding: 68px 24px 144px 64px; + + .rtl & { + padding: 68px 64px 0 24px; + } + } `; const SitePreviewWrapper = styled.div` .home-site-preview { diff --git a/client/my-sites/checkout/src/components/checkout-main.tsx b/client/my-sites/checkout/src/components/checkout-main.tsx index 45f028682c0b52..721c48438af829 100644 --- a/client/my-sites/checkout/src/components/checkout-main.tsx +++ b/client/my-sites/checkout/src/components/checkout-main.tsx @@ -534,9 +534,9 @@ export default function CheckoutMain( { primaryOver: colors[ 'Jetpack Green 60' ], success: colors[ 'Jetpack Green' ], discount: colors[ 'Jetpack Green' ], - highlight: colors[ 'Blue 50' ], - highlightBorder: colors[ 'Blue 80' ], - highlightOver: colors[ 'Blue 60' ], + highlight: colors[ 'WordPress Blue 50' ], + highlightBorder: colors[ 'WordPress Blue 80' ], + highlightOver: colors[ 'WordPress Blue 60' ], } : {}; const theme = { ...checkoutTheme, colors: { ...checkoutTheme.colors, ...jetpackColors } }; diff --git a/client/my-sites/checkout/src/components/checkout-sidebar-plan-upsell/style.scss b/client/my-sites/checkout/src/components/checkout-sidebar-plan-upsell/style.scss index d08f494ae03f58..8e02bc19e2992e 100644 --- a/client/my-sites/checkout/src/components/checkout-sidebar-plan-upsell/style.scss +++ b/client/my-sites/checkout/src/components/checkout-sidebar-plan-upsell/style.scss @@ -8,7 +8,7 @@ } .promo-card.checkout-sidebar-plan-upsell .action-panel__title { - color: var(--studio-blue-50); + color: var(--studio-wordpress-blue-50); display: block; font-size: $font-title-medium; font-weight: 400; diff --git a/client/my-sites/checkout/src/components/item-variation-picker/styles.tsx b/client/my-sites/checkout/src/components/item-variation-picker/styles.tsx index 76b38962790244..ab9d329fecd34c 100644 --- a/client/my-sites/checkout/src/components/item-variation-picker/styles.tsx +++ b/client/my-sites/checkout/src/components/item-variation-picker/styles.tsx @@ -35,16 +35,17 @@ export const Option = styled.li< OptionProps >` font-weight: ${ ( props ) => props.theme.weights.normal }; cursor: pointer; flex-direction: row; - justify-content: space-between; align-items: center; + justify-content: space-between; + align-items: center; /* the calc aligns the price with the price in CurrentOption */ padding: 10px calc( 14px + 24px + 16px ) 10px 16px; &:hover { - var( --studio-blue-0 ); + background: var( --studio-wordpress-blue-5 ); } &.item-variant-option--selected { - background: #055d9c; + background: var( --studio-wordpress-blue-50 ); color: #fff; } `; diff --git a/client/my-sites/checkout/src/components/payment-logos.js b/client/my-sites/checkout/src/components/payment-logos.js index 7e299405e763c5..a0ee45d8453c3c 100644 --- a/client/my-sites/checkout/src/components/payment-logos.js +++ b/client/my-sites/checkout/src/components/payment-logos.js @@ -29,44 +29,65 @@ VisaLogo.propTypes = { }; export function CBLogo( { className } ) { + // We need to provide a unique ID to any svg that uses an id prop + // especially if we expect multiple instances of the component to render on the page + const uniqueID = `${ Math.floor( 10000 + Math.random() * 90000 ) }`; + return ( diff --git a/client/my-sites/checkout/src/components/payment-method-logos.js b/client/my-sites/checkout/src/components/payment-method-logos.js index 8bd42fe8a3f6a4..802cf3070fd85a 100644 --- a/client/my-sites/checkout/src/components/payment-method-logos.js +++ b/client/my-sites/checkout/src/components/payment-method-logos.js @@ -1,9 +1,11 @@ import styled from '@emotion/styled'; export const PaymentMethodLogos = styled.span` + display: flex; flex: 1; - transform: translateY( 3px ); text-align: right; + align-items: center; + justify-content: flex-end; .rtl & { text-align: left; @@ -11,5 +13,9 @@ export const PaymentMethodLogos = styled.span` svg { display: inline-block; + + &.has-background { + padding-inline-end: 5px; + } } `; diff --git a/client/my-sites/checkout/src/hooks/use-create-payment-methods/index.tsx b/client/my-sites/checkout/src/hooks/use-create-payment-methods/index.tsx index c4ecaeb5e9fefe..9b57e38f0840b7 100644 --- a/client/my-sites/checkout/src/hooks/use-create-payment-methods/index.tsx +++ b/client/my-sites/checkout/src/hooks/use-create-payment-methods/index.tsx @@ -65,6 +65,7 @@ export function useCreatePayPal( { } export function useCreateCreditCard( { + currency, isStripeLoading, stripeLoadingError, shouldUseEbanx, @@ -74,6 +75,7 @@ export function useCreateCreditCard( { allowUseForAllSubscriptions, hasExistingCardMethods, }: { + currency: string | null; isStripeLoading: boolean; stripeLoadingError: StripeLoadingError; shouldUseEbanx: boolean; @@ -96,6 +98,7 @@ export function useCreateCreditCard( { () => shouldLoadStripeMethod ? createCreditCardMethod( { + currency, store: stripePaymentMethodStore, shouldUseEbanx, shouldShowTaxFields, @@ -105,6 +108,7 @@ export function useCreateCreditCard( { } ) : null, [ + currency, shouldLoadStripeMethod, stripePaymentMethodStore, shouldUseEbanx, @@ -399,7 +403,7 @@ export default function useCreatePaymentMethods( { } ): PaymentMethod[] { const cartKey = useCartKey(); const { responseCart } = useShoppingCart( cartKey ); - + const { currency } = responseCart; const paypalMethod = useCreatePayPal( {} ); const idealMethod = useCreateIdeal( { @@ -459,6 +463,7 @@ export default function useCreatePaymentMethods( { // in the credit card form instead. const shouldShowTaxFields = contactDetailsType === 'none'; const stripeMethod = useCreateCreditCard( { + currency, shouldShowTaxFields, isStripeLoading, stripeLoadingError, diff --git a/client/my-sites/checkout/src/hooks/use-create-payment-methods/use-create-existing-cards.ts b/client/my-sites/checkout/src/hooks/use-create-payment-methods/use-create-existing-cards.ts index 3bb5471a83d97d..da93a6a6e94bc0 100644 --- a/client/my-sites/checkout/src/hooks/use-create-payment-methods/use-create-existing-cards.ts +++ b/client/my-sites/checkout/src/hooks/use-create-payment-methods/use-create-existing-cards.ts @@ -47,7 +47,9 @@ export default function useCreateExistingCards( { id: `existingCard-${ storedDetails.stored_details_id }`, cardholderName: storedDetails.name, cardExpiry: storedDetails.expiry, - brand: storedDetails.card_type, + brand: storedDetails?.display_brand + ? storedDetails.display_brand + : storedDetails.card_type, last4: storedDetails.card_last_4, storedDetailsId: storedDetails.stored_details_id, paymentMethodToken: storedDetails.mp_ref, diff --git a/client/my-sites/checkout/src/payment-methods/credit-card/assign-to-all-payment-methods.tsx b/client/my-sites/checkout/src/payment-methods/credit-card/assign-to-all-payment-methods.tsx index e1b4a07e2b0f9b..f5f668fa6211fc 100644 --- a/client/my-sites/checkout/src/payment-methods/credit-card/assign-to-all-payment-methods.tsx +++ b/client/my-sites/checkout/src/payment-methods/credit-card/assign-to-all-payment-methods.tsx @@ -1,4 +1,4 @@ -import styled from '@emotion/styled'; +import { styled } from '@automattic/wpcom-checkout'; import { CheckboxControl } from '@wordpress/components'; import { useTranslate } from 'i18n-calypso'; import InlineSupportLink from 'calypso/components/inline-support-link'; @@ -7,6 +7,15 @@ import { recordTracksEvent } from 'calypso/state/analytics/actions'; const CheckboxWrapper = styled.div` margin-top: 16px; + + .assign-to-all-payment-methods-checkbox input[type='checkbox']:checked { + background: ${ ( props ) => props.theme.colors.primary }; + border-color: ${ ( props ) => props.theme.colors.primary }; + } + + a.inline-support-link.assign-to-all-payment-methods-checkbox__link { + color: ${ ( props ) => props.theme.colors.primary }; + } `; export default function AssignToAllPaymentMethods( { @@ -34,6 +43,7 @@ export default function AssignToAllPaymentMethods( { return ( diff --git a/client/my-sites/checkout/src/payment-methods/credit-card/contact-fields.tsx b/client/my-sites/checkout/src/payment-methods/credit-card/contact-fields.tsx index 08fe7c650cef5a..a0d4a53d4be474 100644 --- a/client/my-sites/checkout/src/payment-methods/credit-card/contact-fields.tsx +++ b/client/my-sites/checkout/src/payment-methods/credit-card/contact-fields.tsx @@ -37,7 +37,7 @@ export default function ContactFields( { }; return ( -
+ <> { shouldUseEbanx && ! shouldShowTaxFields && ( ) } -
+ ); } diff --git a/client/my-sites/checkout/src/payment-methods/credit-card/credit-card-number-field.tsx b/client/my-sites/checkout/src/payment-methods/credit-card/credit-card-number-field.tsx index 6f5e04f3d9870f..601ad49109c13f 100644 --- a/client/my-sites/checkout/src/payment-methods/credit-card/credit-card-number-field.tsx +++ b/client/my-sites/checkout/src/payment-methods/credit-card/credit-card-number-field.tsx @@ -1,5 +1,4 @@ import { FormStatus, useFormStatus } from '@automattic/composite-checkout'; -import { PaymentLogo } from '@automattic/wpcom-checkout'; import { CardNumberElement } from '@stripe/react-stripe-js'; import { useSelect } from '@wordpress/data'; import { useI18n } from '@wordpress/react-i18n'; @@ -29,10 +28,7 @@ export default function CreditCardNumberField( { const { __ } = useI18n(); const { formStatus } = useFormStatus(); const isDisabled = formStatus !== FormStatus.READY; - const brand: string = useSelect( - ( select ) => ( select( 'wpcom-credit-card' ) as WpcomCreditCardSelectors ).getBrand(), - [] - ); + const { cardNumber: cardNumberError } = useSelect( ( select ) => ( select( 'wpcom-credit-card' ) as WpcomCreditCardSelectors ).getCardDataErrors(), [] @@ -71,6 +67,7 @@ export default function CreditCardNumberField( { options={ { style: stripeElementStyle, disabled: isDisabled, + showIcon: true, } } onReady={ () => { setIsStripeFullyLoaded( true ); @@ -79,7 +76,6 @@ export default function CreditCardNumberField( { handleStripeFieldChange( input ); } } /> - { cardNumberError && { cardNumberError } } diff --git a/client/my-sites/checkout/src/payment-methods/credit-card/index.tsx b/client/my-sites/checkout/src/payment-methods/credit-card/index.tsx index 1349f3ef6ed671..a4c43612949b0c 100644 --- a/client/my-sites/checkout/src/payment-methods/credit-card/index.tsx +++ b/client/my-sites/checkout/src/payment-methods/credit-card/index.tsx @@ -3,6 +3,7 @@ import { useSelect } from '@wordpress/data'; import { useI18n } from '@wordpress/react-i18n'; import { Fragment } from 'react'; import { + CBLogo, VisaLogo, MastercardLogo, AmexLogo, @@ -42,9 +43,10 @@ function CreditCardSummary() { ); } -const CreditCardLabel: React.FC< { hasExistingCardMethods: boolean | undefined } > = ( { - hasExistingCardMethods, -} ) => { +const CreditCardLabel: React.FC< { + hasExistingCardMethods: boolean | undefined; + currency: string | null; +} > = ( { hasExistingCardMethods, currency } ) => { const { __ } = useI18n(); return ( @@ -53,14 +55,15 @@ const CreditCardLabel: React.FC< { hasExistingCardMethods: boolean | undefined } ) : ( { __( 'Credit or debit card' ) } ) } - + ); }; -function CreditCardLogos() { +function CreditCardLogos( { currency }: { currency: string | null } ) { return ( - + + { currency === 'EUR' && } @@ -69,6 +72,7 @@ function CreditCardLogos() { } export function createCreditCardMethod( { + currency, store, shouldUseEbanx, shouldShowTaxFields, @@ -76,6 +80,7 @@ export function createCreditCardMethod( { allowUseForAllSubscriptions, hasExistingCardMethods, }: { + currency: string | null; store: CardStoreType; shouldUseEbanx?: boolean; shouldShowTaxFields?: boolean; @@ -86,7 +91,9 @@ export function createCreditCardMethod( { return { id: 'card', paymentProcessorId: 'card', - label: , + label: ( + + ), hasRequiredFields: true, activeContent: ( { ! disabled && (
diff --git a/client/my-sites/hosting/phpmyadmin-card/style.scss b/client/my-sites/hosting/phpmyadmin-card/style.scss index 4ed42be3824e41..d3dbfb3fc64b6f 100644 --- a/client/my-sites/hosting/phpmyadmin-card/style.scss +++ b/client/my-sites/hosting/phpmyadmin-card/style.scss @@ -1,4 +1,7 @@ .phpmyadmin-card button { + &.is-primary svg { + fill: var(--color-text-inverted); + } svg { vertical-align: text-bottom; margin-left: 10px; diff --git a/client/my-sites/hosting/restore-plan-software-card/index.js b/client/my-sites/hosting/restore-plan-software-card/index.js index d9e8dce2eb1a01..46f49e227bc368 100644 --- a/client/my-sites/hosting/restore-plan-software-card/index.js +++ b/client/my-sites/hosting/restore-plan-software-card/index.js @@ -43,9 +43,7 @@ export default function RestorePlanSoftwareCard() { 'If your website is missing plugins and themes that come with your plan, you may restore them here.' ) } - + ); } diff --git a/client/my-sites/hosting/sftp-card/index.js b/client/my-sites/hosting/sftp-card/index.js index dfe46f0adb96dc..0271d551396c19 100644 --- a/client/my-sites/hosting/sftp-card/index.js +++ b/client/my-sites/hosting/sftp-card/index.js @@ -289,7 +289,7 @@ export const SftpCard = ( { } ) } - diff --git a/client/my-sites/hosting/staging-site-card/card-content/staging-sync-card.tsx b/client/my-sites/hosting/staging-site-card/card-content/staging-sync-card.tsx index f19366e40cd91e..5f3af6db89f1f6 100644 --- a/client/my-sites/hosting/staging-site-card/card-content/staging-sync-card.tsx +++ b/client/my-sites/hosting/staging-site-card/card-content/staging-sync-card.tsx @@ -228,7 +228,6 @@ const StagingToProductionSync = ( { - + ); next(); diff --git a/client/my-sites/importer/newsletter/content-upload/author-mapping-pane.jsx b/client/my-sites/importer/newsletter/content-upload/author-mapping-pane.jsx index 1f2a8b34c53ef0..5ef24896c15cff 100644 --- a/client/my-sites/importer/newsletter/content-upload/author-mapping-pane.jsx +++ b/client/my-sites/importer/newsletter/content-upload/author-mapping-pane.jsx @@ -157,7 +157,7 @@ class AuthorMappingPane extends PureComponent { } ) }
- + Continue import diff --git a/client/my-sites/importer/newsletter/content-upload/file-importer.jsx b/client/my-sites/importer/newsletter/content-upload/file-importer.jsx index ce40157444f317..cbc7114b1fc592 100644 --- a/client/my-sites/importer/newsletter/content-upload/file-importer.jsx +++ b/client/my-sites/importer/newsletter/content-upload/file-importer.jsx @@ -44,6 +44,7 @@ class FileImporter extends PureComponent { icon: PropTypes.string.isRequired, description: PropTypes.node.isRequired, uploadDescription: PropTypes.node, + acceptedFileTypes: PropTypes.array, } ).isRequired, importerStatus: PropTypes.shape( { errorData: PropTypes.shape( { @@ -80,7 +81,8 @@ class FileImporter extends PureComponent { }; render() { - const { title, overrideDestination, uploadDescription, optionalUrl } = this.props.importerData; + const { title, overrideDestination, uploadDescription, optionalUrl, acceptedFileTypes } = + this.props.importerData; const { importerStatus, site, fromSite, nextStepUrl, skipNextStep } = this.props; const { errorData, importerState, summaryModalOpen } = importerStatus; const isEnabled = appStates.DISABLED !== importerState; @@ -153,6 +155,7 @@ class FileImporter extends PureComponent { site={ site } optionalUrl={ optionalUrl } fromSite={ fromSite } + acceptedFileTypes={ acceptedFileTypes } nextStepUrl={ nextStepUrl } skipNextStep={ skipNextStep } /> diff --git a/client/my-sites/importer/newsletter/content-upload/import-summary-modal.tsx b/client/my-sites/importer/newsletter/content-upload/import-summary-modal.tsx index 36b2970bd1a3c3..ed93969569548c 100644 --- a/client/my-sites/importer/newsletter/content-upload/import-summary-modal.tsx +++ b/client/my-sites/importer/newsletter/content-upload/import-summary-modal.tsx @@ -8,6 +8,68 @@ interface ImportSummaryModalProps { authorsNumber?: number; } +function getContent( postsNumber: number, pagesNumber: number, attachmentsNumber: number ) { + if ( postsNumber > 0 && pagesNumber > 0 && attachmentsNumber > 0 ) { + return ( +

+ We’ve found { postsNumber } posts, { pagesNumber } pages + and { attachmentsNumber } media to import. +

+ ); + } + + if ( postsNumber > 0 && pagesNumber > 0 ) { + return ( +

+ We’ve found { postsNumber } posts and{ ' ' } + { pagesNumber } pages to import. +

+ ); + } + + if ( postsNumber > 0 && attachmentsNumber > 0 ) { + return ( +

+ We’ve found { postsNumber } posts and{ ' ' } + { attachmentsNumber } media to import. +

+ ); + } + + if ( pagesNumber > 0 && attachmentsNumber > 0 ) { + return ( +

+ We’ve found { pagesNumber } pages and{ ' ' } + { attachmentsNumber } media to import. +

+ ); + } + + if ( postsNumber > 0 ) { + return ( +

+ We’ve found { postsNumber } posts to import. +

+ ); + } + + if ( pagesNumber > 0 ) { + return ( +

+ We’ve found { pagesNumber } pages to import. +

+ ); + } + + if ( attachmentsNumber > 0 ) { + return ( +

+ We’ve found { attachmentsNumber } media to import. +

+ ); + } +} + export default function ImportSummaryModal( { onRequestClose, postsNumber, @@ -17,17 +79,13 @@ export default function ImportSummaryModal( { }: ImportSummaryModalProps ) { return ( -

- We’ve found { postsNumber } posts, { pagesNumber } pages{ ' ' } - and { attachmentsNumber } media to import. - { authorsNumber && ( - <> -
- Your Substack publication has { authorsNumber } authors. Next, you can - match them with existing site users. - - ) } -

+ { getContent( postsNumber, pagesNumber, attachmentsNumber ) } + { authorsNumber && ( +

+ Your Substack publication has { authorsNumber } authors. Next, you can + match them with existing site users. +

+ ) } diff --git a/client/my-sites/importer/newsletter/content-upload/uploading-pane.jsx b/client/my-sites/importer/newsletter/content-upload/uploading-pane.jsx index 26394ee74adcb1..705b287f7ec826 100644 --- a/client/my-sites/importer/newsletter/content-upload/uploading-pane.jsx +++ b/client/my-sites/importer/newsletter/content-upload/uploading-pane.jsx @@ -1,4 +1,6 @@ -import { ProgressBar, FormInputValidation, FormLabel, Gridicon } from '@automattic/components'; +import { FormInputValidation, FormLabel } from '@automattic/components'; +import { ProgressBar } from '@wordpress/components'; +import { Icon, cloudUpload } from '@wordpress/icons'; import clsx from 'clsx'; import { localize } from 'i18n-calypso'; import { truncate } from 'lodash'; @@ -48,6 +50,7 @@ export class UploadingPane extends PureComponent { validate: PropTypes.func, } ), fromSite: PropTypes.string, + acceptedFileTypes: PropTypes.array, nextStepUrl: PropTypes.string, skipNextStep: PropTypes.func, }; @@ -106,9 +109,7 @@ export class UploadingPane extends PureComponent { case appStates.UPLOAD_PROCESSING: case appStates.UPLOADING: { const uploadPercent = percentComplete; - const progressClasses = clsx( 'importer__upload-progress', { - 'is-complete': uploadPercent > 95, - } ); + const uploaderPrompt = importerState === appStates.UPLOADING && uploadPercent < 99 ? this.props.translate( 'Uploading %(filename)s\u2026', { @@ -117,14 +118,9 @@ export class UploadingPane extends PureComponent { : this.props.translate( 'Processing uploaded file\u2026' ); return ( -
+

{ uploaderPrompt }

- 99 || importerState === appStates.UPLOAD_PROCESSING } - /> +
); } @@ -237,7 +233,7 @@ export class UploadingPane extends PureComponent { }; render() { - const { importerStatus, fromSite, nextStepUrl, skipNextStep } = this.props; + const { importerStatus, fromSite, acceptedFileTypes, nextStepUrl, skipNextStep } = this.props; const isReadyForImport = this.isReadyForImport(); const importerStatusClasses = clsx( 'importer__upload-content', @@ -260,13 +256,7 @@ export class UploadingPane extends PureComponent { onKeyPress={ isReadyForImport ? this.handleKeyPress : null } >
- + { this.getMessage() }
{ isReadyForImport && ( @@ -275,6 +265,7 @@ export class UploadingPane extends PureComponent { type="file" name="exportFile" onChange={ this.initiateFromForm } + accept={ acceptedFileTypes?.length ? acceptedFileTypes.join( ',' ) : undefined } /> ) } diff --git a/client/my-sites/importer/newsletter/content.tsx b/client/my-sites/importer/newsletter/content.tsx index 3164e6bee06744..d496699853a3da 100644 --- a/client/my-sites/importer/newsletter/content.tsx +++ b/client/my-sites/importer/newsletter/content.tsx @@ -1,5 +1,6 @@ -import { Card, Button, Gridicon } from '@automattic/components'; -import { QueryArgParsed } from '@wordpress/url/build-types/get-query-arg'; +import { Card } from '@automattic/components'; +import { Button } from '@wordpress/components'; +import { external } from '@wordpress/icons'; import { useEffect } from 'react'; import importerConfig from 'calypso/lib/importer/importer-config'; import { EVERY_FIVE_SECONDS, Interval } from 'calypso/lib/interval'; @@ -12,10 +13,9 @@ import type { SiteDetails } from '@automattic/data-stores'; type ContentProps = { nextStepUrl: string; - selectedSite?: SiteDetails; + selectedSite: SiteDetails; siteSlug: string; - fromSite: QueryArgParsed; - content: any; + fromSite: string; skipNextStep: () => void; }; @@ -26,8 +26,8 @@ export default function Content( { fromSite, skipNextStep, }: ContentProps ) { - const siteTitle = selectedSite?.title; - const siteId = selectedSite?.ID; + const siteTitle = selectedSite.title; + const siteId = selectedSite.ID; const siteImports = useSelector( ( state ) => getImporterStatusForSiteId( state, siteId ) ); @@ -40,10 +40,6 @@ export default function Content( { useEffect( fetchImporters, [ siteId, dispatch ] ); useEffect( startImporting, [ siteId, dispatch, siteImports ] ); - if ( ! selectedSite ) { - return null; - } - function startImporting() { siteId && siteImports.length === 0 && dispatch( startImport( siteId ) ); } @@ -78,8 +74,10 @@ export default function Content( { href={ `https://${ fromSite }/publish/settings?search=export` } target="_blank" rel="noreferrer noopener" + icon={ external } + variant="secondary" > - Export content + Export content

Step 2: Import your content to WordPress.com

@@ -90,7 +88,7 @@ export default function Content( { site={ selectedSite } importerStatus={ importerStatus } importerData={ importerData } - fromSite={ fromSite as string } + fromSite={ fromSite } nextStepUrl={ nextStepUrl } skipNextStep={ skipNextStep } /> diff --git a/client/my-sites/importer/newsletter/importer.scss b/client/my-sites/importer/newsletter/importer.scss index 15a3430b934eab..0115a20b2de7b7 100644 --- a/client/my-sites/importer/newsletter/importer.scss +++ b/client/my-sites/importer/newsletter/importer.scss @@ -40,47 +40,62 @@ box-shadow: none; } - .summary__content p { - display: flex; - gap: 8px; + + .summary__content input[type="checkbox"] { + margin-right: 8px; + } + + .summary__content svg { + float: left; + margin-right: 6px; + margin-top: -1px; + fill: var(--studio-gray-50); } .stripe-logo { width: 48px; padding-left: 8px; } -} -.select-newsletter-form__help { - margin-top: 8px; - font-size: 0.75rem; + .select-newsletter-form__help { + margin-top: 8px; + margin-bottom: 0; + font-size: 0.75rem; - &.is-error { - color: var(--color-error); + &.is-error { + color: var(--color-error); + } } } - +.content-upload-form__in-progress, .subscriber-upload-form__in-progress, .subscriber-upload-form .file-picker { - background: #f6f7f7; - height: 180px; width: 100%; - border: 1px dashed var(--studio-gray-50); - border-radius: 4px; display: flex; flex-direction: column; margin-bottom: 16px; align-items: center; - padding-top: 50px; color: var(--studio-gray-50); } + +.subscriber-upload-form__dropzone { + background: #f6f7f7; + padding-top: 50px; + cursor: pointer; + position: relative; + height: 180px; + border: 1px dashed var(--studio-gray-50); + border-radius: 4px; + margin-bottom: 20px; +} + .subscriber-upload-form__in-progress svg, .subscriber-upload-form .file-picker svg { width: 48px; height: 48px; text-align: center; padding: 8px; - fill: var(--studio-gray-50); + fill: var(--studio-gray-20); } .subscriber-upload-form__modal ul { @@ -88,13 +103,21 @@ padding: 0; list-style: none; } + .subscriber-upload-form__modal li { display: flex; } + +.subscriber-upload-form__modal strong { + margin-right: 8px; +} + .subscriber-upload-form__modal svg { fill: var(--studio-gray-50); margin-right: 8px; } + .select-newsletter-form .is-loading { @include placeholder( --color-neutral-10 ); + margin-bottom: 0; } diff --git a/client/my-sites/importer/newsletter/importer.tsx b/client/my-sites/importer/newsletter/importer.tsx index 6e928ba3d1ac01..5a3a9bd18dfb9a 100644 --- a/client/my-sites/importer/newsletter/importer.tsx +++ b/client/my-sites/importer/newsletter/importer.tsx @@ -1,3 +1,4 @@ +import page from '@automattic/calypso-router'; import { Spinner } from '@wordpress/components'; import { Icon, check } from '@wordpress/icons'; import { addQueryArgs, getQueryArg } from '@wordpress/url'; @@ -5,7 +6,10 @@ import { useState, useEffect } from 'react'; import { UrlData } from 'calypso/blocks/import/types'; import FormattedHeader from 'calypso/components/formatted-header'; import StepProgress, { ClickHandler } from 'calypso/components/step-progress'; -import { usePaidNewsletterQuery } from 'calypso/data/paid-newsletter/use-paid-newsletter-query'; +import { + StepId, + usePaidNewsletterQuery, +} from 'calypso/data/paid-newsletter/use-paid-newsletter-query'; import { useResetMutation } from 'calypso/data/paid-newsletter/use-reset-mutation'; import { useSkipNextStepMutation } from 'calypso/data/paid-newsletter/use-skip-next-step-mutation'; import { useAnalyzeUrlQuery } from 'calypso/data/site-profiler/use-analyze-url-query'; @@ -22,9 +26,7 @@ import './importer.scss'; const steps = [ Content, Subscribers, PaidSubscribers, Summary ]; -const stepSlugs = [ 'content', 'subscribers', 'paid-subscribers', 'summary' ]; - -const noop = () => {}; +const stepSlugs: StepId[] = [ 'content', 'subscribers', 'paid-subscribers', 'summary' ]; const logoChainLogos = [ { name: 'substack', color: 'var(--color-substack)' }, @@ -34,46 +36,91 @@ const logoChainLogos = [ type NewsletterImporterProps = { siteSlug: string; engine: string; - step: string; + step?: StepId; }; function getTitle( urlData?: UrlData ) { if ( urlData?.meta?.title ) { - return `Import ${ urlData?.meta?.title }`; + return `Import ${ urlData.meta.title }`; } return 'Import your newsletter'; } -export default function NewsletterImporter( { siteSlug, engine, step }: NewsletterImporterProps ) { +export default function NewsletterImporter( { + siteSlug, + engine, + step = 'content', +}: NewsletterImporterProps ) { + const fromSite = getQueryArg( window.location.href, 'from' ) as string; const selectedSite = useSelector( getSelectedSite ) ?? undefined; const [ validFromSite, setValidFromSite ] = useState( false ); + const [ autoFetchData, setAutoFetchData ] = useState( false ); const stepsProgress: ClickHandler[] = [ - { message: 'Content', onClick: noop }, - { message: 'Subscribers', onClick: noop }, - { message: 'Paid Subscribers', onClick: noop }, - { message: 'Summary', onClick: noop }, + { + message: 'Content', + onClick: () => { + page( + addQueryArgs( `/import/newsletter/${ engine }/${ siteSlug }/content`, { + from: fromSite, + } ) + ); + }, + show: 'onComplete', + }, + { + message: 'Subscribers', + onClick: () => { + page( + addQueryArgs( `/import/newsletter/${ engine }/${ siteSlug }/subscribers`, { + from: fromSite, + } ) + ); + }, + show: 'onComplete', + }, + { + message: 'Paid Subscribers', + onClick: () => { + page( + addQueryArgs( `/import/newsletter/${ engine }/${ siteSlug }/paid-subscribers`, { + from: fromSite, + } ) + ); + }, + show: 'onComplete', + }, + { message: 'Summary', onClick: () => {} }, ]; - let fromSite = getQueryArg( window.location.href, 'from' ) as string | string[]; - // Steps - fromSite = Array.isArray( fromSite ) ? fromSite[ 0 ] : fromSite; - if ( fromSite && ! step ) { - step = stepSlugs[ 0 ]; - } - let stepIndex = 0; let nextStep = stepSlugs[ 0 ]; const { data: paidNewsletterData, isFetching: isFetchingPaidNewsletter } = usePaidNewsletterQuery( engine, step, - selectedSite?.ID + selectedSite?.ID, + autoFetchData ); + useEffect( () => { + if ( + paidNewsletterData?.steps?.content?.status === 'importing' || + paidNewsletterData?.steps.subscribers?.status === 'importing' + ) { + setAutoFetchData( true ); + } else { + setAutoFetchData( false ); + } + }, [ + paidNewsletterData?.steps?.content?.status, + paidNewsletterData?.steps.subscribers?.status, + setAutoFetchData, + ] ); + stepSlugs.forEach( ( stepName, index ) => { if ( stepName === step ) { stepIndex = index; @@ -85,7 +132,7 @@ export default function NewsletterImporter( { siteSlug, engine, step }: Newslett if ( status === 'done' ) { stepsProgress[ index ].indicator = ; } - if ( status === 'processing' ) { + if ( status === 'importing' ) { stepsProgress[ index ].indicator = ; } } @@ -98,7 +145,12 @@ export default function NewsletterImporter( { siteSlug, engine, step }: Newslett let stepContent = {}; if ( paidNewsletterData?.steps ) { - stepContent = paidNewsletterData?.steps[ step ]?.content ?? {}; + // This is useful for the summary step. + if ( ! paidNewsletterData?.steps[ step ] ) { + stepContent = paidNewsletterData.steps; + } else { + stepContent = paidNewsletterData.steps[ step ]?.content ?? {}; + } } useEffect( () => { @@ -128,7 +180,6 @@ export default function NewsletterImporter( { siteSlug, engine, step }: Newslett stepUrl={ stepUrl } urlData={ urlData } isLoading={ isFetching || isResetPaidNewsletterPending } - validFromSite={ validFromSite } /> ) } @@ -145,10 +196,12 @@ export default function NewsletterImporter( { siteSlug, engine, step }: Newslett skipNextStep={ () => { skipNextStep( selectedSite.ID, engine, nextStep, step ); } } + // FIXME + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error cardData={ stepContent } engine={ engine } isFetchingContent={ isFetchingPaidNewsletter } - content={ stepContent } /> ) }
diff --git a/client/my-sites/importer/newsletter/paid-subscribers.tsx b/client/my-sites/importer/newsletter/paid-subscribers.tsx index 1bb189f8b75fe1..4138b56b627149 100644 --- a/client/my-sites/importer/newsletter/paid-subscribers.tsx +++ b/client/my-sites/importer/newsletter/paid-subscribers.tsx @@ -1,16 +1,19 @@ +import { SiteDetails } from '@automattic/data-stores'; import { hasQueryArg } from '@wordpress/url'; import { useEffect } from 'react'; +import { PaidSubscribersStepContent } from 'calypso/data/paid-newsletter/use-paid-newsletter-query'; import { useDispatch } from 'calypso/state'; import { infoNotice, successNotice } from 'calypso/state/notices/actions'; import ConnectStripe from './paid-subscribers/connect-stripe'; import MapPlans from './paid-subscribers/map-plans'; + type Props = { nextStepUrl: string; skipNextStep: () => void; fromSite: string; engine: string; - cardData: any; - selectedSite: any; + cardData: PaidSubscribersStepContent; + selectedSite: SiteDetails; isFetchingContent: boolean; }; @@ -54,7 +57,7 @@ export default function PaidSubscribers( { cardData={ cardData } skipNextStep={ skipNextStep } engine={ engine } - siteId={ selectedSite?.ID } + siteId={ selectedSite.ID } currentStep="paid-subscribers" /> ) } diff --git a/client/my-sites/importer/newsletter/paid-subscribers/connect-stripe.tsx b/client/my-sites/importer/newsletter/paid-subscribers/connect-stripe.tsx index 483fdd1670a613..2f3e0f160bd0ca 100644 --- a/client/my-sites/importer/newsletter/paid-subscribers/connect-stripe.tsx +++ b/client/my-sites/importer/newsletter/paid-subscribers/connect-stripe.tsx @@ -1,6 +1,7 @@ import { Card } from '@automattic/components'; import { getQueryArg, addQueryArgs } from '@wordpress/url'; import StripeLogo from 'calypso/assets/images/jetpack/stripe-logo-white.svg'; +import { PaidSubscribersStepContent } from 'calypso/data/paid-newsletter/use-paid-newsletter-query'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; import ImporterActionButton from '../../importer-action-buttons/action-button'; import ImporterActionButtonContainer from '../../importer-action-buttons/container'; @@ -25,7 +26,7 @@ function updateConnectUrl( connectUrl: string, fromSite: string, engine: string type Props = { nextStepUrl: string; skipNextStep: () => void; - cardData: any; + cardData: PaidSubscribersStepContent; fromSite: string; engine: string; isFetchingContent: boolean; diff --git a/client/my-sites/importer/newsletter/paid-subscribers/map-plan.scss b/client/my-sites/importer/newsletter/paid-subscribers/map-plan.scss index 6765f64bd1fe13..2c9e241aa46639 100644 --- a/client/my-sites/importer/newsletter/paid-subscribers/map-plan.scss +++ b/client/my-sites/importer/newsletter/paid-subscribers/map-plan.scss @@ -12,10 +12,11 @@ .map-plan__info { min-height: 80px; display: flex; - align-items: center; min-width: 230px; + align-items: center; } .map-plan__info { + align-items: start; flex-direction: column; justify-content: center; p { @@ -30,7 +31,7 @@ .map-plan__select-product .map-plan__selected { display: flex; height: auto; - + text-align: right; p { margin: 0; } diff --git a/client/my-sites/importer/newsletter/paid-subscribers/map-plan.tsx b/client/my-sites/importer/newsletter/paid-subscribers/map-plan.tsx index 07e0b05e997af5..54e2d86eeafd38 100644 --- a/client/my-sites/importer/newsletter/paid-subscribers/map-plan.tsx +++ b/client/my-sites/importer/newsletter/paid-subscribers/map-plan.tsx @@ -4,28 +4,10 @@ import { Fragment } from '@wordpress/element'; import { chevronDown, Icon, arrowRight } from '@wordpress/icons'; import { useState, KeyboardEvent } from 'react'; import { useMapStripePlanToProductMutation } from 'calypso/data/paid-newsletter/use-map-stripe-plan-to-product-mutation'; +import { Product, Plan } from 'calypso/data/paid-newsletter/use-paid-newsletter-query'; import './map-plan.scss'; -type Plan = { - plan_id: string; - name: string; - plan_interval: string; - active_subscriptions: number; - is_active: boolean; - plan_currency: string; - plan_amount_decimal: number; - product_id: string; -}; - -type Product = { - id: number; - price: string; - currency: string; - title: string; - interval: string; -}; - export type TierToAdd = { currency: string; price: number; @@ -42,16 +24,16 @@ export type TierToAdd = { type MapPlanProps = { plan: Plan; - products: Array< Product >; - map_plans: any; - siteId: string; + products: Product[]; + map_plans: Record< string, string >; + siteId: number; engine: string; currentStep: string; onProductAdd: ( arg0: TierToAdd | null ) => void; tierToAdd: TierToAdd; }; -function displayProduct( product: Product | undefined ) { +function displayProduct( product?: Product ) { if ( ! product ) { return 'Select a Newsletter Tier'; } @@ -66,7 +48,7 @@ function displayProduct( product: Product | undefined ) { ); } -function getProductChoices( products: Array< Product > ) { +function getProductChoices( products: Product[] ) { return products.map( ( product ) => ( { info: `${ formatCurrency( parseFloat( product.price ), product.currency ) } / ${ product.interval diff --git a/client/my-sites/importer/newsletter/paid-subscribers/map-plans.tsx b/client/my-sites/importer/newsletter/paid-subscribers/map-plans.tsx index 6aa1b7c1fe43cd..067d64456e9a0f 100644 --- a/client/my-sites/importer/newsletter/paid-subscribers/map-plans.tsx +++ b/client/my-sites/importer/newsletter/paid-subscribers/map-plans.tsx @@ -2,6 +2,7 @@ import { Card } from '@automattic/components'; import { formatCurrency } from '@automattic/format-currency'; import { useQueryClient } from '@tanstack/react-query'; import { useEffect, useState, useRef } from 'react'; +import { PaidSubscribersStepContent } from 'calypso/data/paid-newsletter/use-paid-newsletter-query'; import RecurringPaymentsPlanAddEditModal from 'calypso/my-sites/earn/components/add-edit-plan-modal'; import { PLAN_YEARLY_FREQUENCY, @@ -18,8 +19,8 @@ import { MapPlan, TierToAdd } from './map-plan'; type Props = { nextStepUrl: string; skipNextStep: () => void; - cardData: any; - siteId: string; + cardData: PaidSubscribersStepContent; + siteId: number; engine: string; currentStep: string; }; @@ -58,13 +59,17 @@ export default function MapPlans( { } sizeOfProductsRef.current = sizeOfProducts; queryClient.invalidateQueries( { - queryKey: [ 'paid-newsletter-importer', siteId, engine, currentStep ], + queryKey: [ 'paid-newsletter-importer', siteId, engine ], } ); }, [ sizeOfProducts, sizeOfProductsRef, siteId, engine, currentStep, queryClient ] ); - const monthyPlan = cardData.plans.find( ( plan: any ) => plan.plan_interval === 'month' ); + const monthyPlan = cardData.plans.find( ( plan ) => plan.plan_interval === 'month' ); + const annualPlan = cardData.plans.find( ( plan ) => plan.plan_interval === 'year' ); - const annualPlan = cardData.plans.find( ( plan: any ) => plan.plan_interval === 'year' ); + // TODO what if those plans are undefined? + if ( ! monthyPlan || ! annualPlan ) { + return; + } const tierToAdd = { currency: monthyPlan.plan_currency, diff --git a/client/my-sites/importer/newsletter/select-newsletter-form.tsx b/client/my-sites/importer/newsletter/select-newsletter-form.tsx index 5fea47165b89a8..0cbb34da198074 100644 --- a/client/my-sites/importer/newsletter/select-newsletter-form.tsx +++ b/client/my-sites/importer/newsletter/select-newsletter-form.tsx @@ -10,16 +10,10 @@ type Props = { stepUrl: string; urlData?: UrlData; isLoading: boolean; - validFromSite: boolean; }; -export default function SelectNewsletterForm( { - stepUrl, - urlData, - isLoading, - validFromSite, -}: Props ) { - const [ hasError, setHasError ] = useState( ! validFromSite ); +export default function SelectNewsletterForm( { stepUrl, urlData, isLoading }: Props ) { + const [ hasError, setHasError ] = useState( false ); const handleAction = ( fromSite: string ) => { if ( ! isValidUrl( fromSite ) ) { @@ -34,35 +28,29 @@ export default function SelectNewsletterForm( { if ( isLoading ) { return ( - -
-

-
+ +
); } return ( - -
- - { hasError && ( -

- Please enter a valid substack URL. -

- ) } - { ! hasError && ( -

- Enter the URL of the substack newsletter that you wish to import. -

- ) } -
+ + + { hasError && ( +

Please enter a valid Substack URL.

+ ) } + { ! hasError && ( +

+ Enter the URL of the Substack newsletter that you wish to import. +

+ ) }
); } diff --git a/client/my-sites/importer/newsletter/subscriber-upload-form.tsx b/client/my-sites/importer/newsletter/subscriber-upload-form.tsx index e74e41a2a1bb12..a5b07681322fca 100644 --- a/client/my-sites/importer/newsletter/subscriber-upload-form.tsx +++ b/client/my-sites/importer/newsletter/subscriber-upload-form.tsx @@ -1,11 +1,10 @@ import { FormInputValidation } from '@automattic/components'; import { Subscriber } from '@automattic/data-stores'; import { localizeUrl } from '@automattic/i18n-utils'; -import { useActiveJobRecognition } from '@automattic/subscriber'; -import { Button, ProgressBar, Modal } from '@wordpress/components'; +import { ProgressBar } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { Icon, cloudUpload, people, currencyEuro } from '@wordpress/icons'; -import { useCallback, useState, useEffect, useRef, FormEvent } from 'react'; +import { Icon, cloudUpload } from '@wordpress/icons'; +import { useCallback, useState, FormEvent } from 'react'; import DropZone from 'calypso/components/drop-zone'; import FilePicker from 'calypso/components/file-picker'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; @@ -16,13 +15,13 @@ type Props = { nextStepUrl: string; siteId: number; skipNextStep: () => void; + cardData: any; }; export default function SubscriberUploadForm( { nextStepUrl, siteId, skipNextStep }: Props ) { const [ selectedFile, setSelectedFile ] = useState< File >(); - const [ isOpen, setIsOpen ] = useState( false ); - const { importCsvSubscribers, getSubscribersImports } = useDispatch( Subscriber.store ); + const { importCsvSubscribers } = useDispatch( Subscriber.store ); const { importSelector } = useSelect( ( select ) => { const subscriber = select( Subscriber.store ); @@ -32,13 +31,11 @@ export default function SubscriberUploadForm( { nextStepUrl, siteId, skipNextSte }; }, [] ); - const prevInProgress = useRef( importSelector?.inProgress ); - const [ isSelectedFileValid, setIsSelectedFileValid ] = useState( false ); const onSubmit = useCallback( async ( event: FormEvent< HTMLFormElement > ) => { event.preventDefault(); - selectedFile && importCsvSubscribers( siteId, selectedFile ); + selectedFile && importCsvSubscribers( siteId, selectedFile, [], true ); }, [ siteId, selectedFile ] ); @@ -59,62 +56,18 @@ export default function SubscriberUploadForm( { nextStepUrl, siteId, skipNextSte return validExtensions.includes( match?.groups?.extension.toLowerCase() as string ); } - useActiveJobRecognition( siteId ); - - useEffect( () => { - getSubscribersImports( siteId ); - }, [ siteId, getSubscribersImports ] ); - - useEffect( () => { - if ( prevInProgress.current ) { - setIsOpen( true ); - } - prevInProgress.current = importSelector?.inProgress; - }, [ importSelector?.inProgress ] ); - const importSubscribersUrl = 'https://wordpress.com/support/launch-a-newsletter/import-subscribers-to-a-newsletter/'; if ( importSelector?.inProgress ) { return ( -
- -

Uploading...

- -
- ); - } - - if ( isOpen ) { - return ( - setIsOpen( false ) } - className="subscriber-upload-form__modal" - size="medium" - > -
- We’ve found 100 subscribers, where: -
    -
  • - - 82 are free subscribers -
  • -
  • - - 1 have a complimentary -
  • -
  • - - subscription 18 are paying subscribers -
  • -
+
+
+ +

Uploading...

+
- - +
); } @@ -125,17 +78,19 @@ export default function SubscriberUploadForm( { nextStepUrl, siteId, skipNextSte Sorry, you can only upload CSV files. Please try again with a valid file. ) } - - - - { ! selectedFile &&

Drag a file, or click to upload a file.

} - { selectedFile && ( -

- To replace this { selectedFile?.name } -
drag a file, or click to upload different one. -

- ) } -
+
+ + + + { ! selectedFile &&

Drag a file, or click to upload a file.

} + { selectedFile && ( +

+ To replace this { selectedFile?.name } +
drag a file, or click to upload different one. +

+ ) } +
+
{ isSelectedFileValid && selectedFile && (

By clicking "Continue," you represent that you've obtained the appropriate consent to diff --git a/client/my-sites/importer/newsletter/subscribers.tsx b/client/my-sites/importer/newsletter/subscribers.tsx index c1f41b4c4b6ce6..2c90cfb56f2b8c 100644 --- a/client/my-sites/importer/newsletter/subscribers.tsx +++ b/client/my-sites/importer/newsletter/subscribers.tsx @@ -1,47 +1,120 @@ -import { Button, Card, Gridicon } from '@automattic/components'; +import { Card } from '@automattic/components'; +import { Subscriber } from '@automattic/data-stores'; +import { useQueryClient } from '@tanstack/react-query'; +import { Modal, Button } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { Icon, people, currencyDollar, external } from '@wordpress/icons'; import { QueryArgParsed } from '@wordpress/url/build-types/get-query-arg'; +import { toInteger } from 'lodash'; +import { useEffect, useRef } from 'react'; +import { SubscribersStepContent } from 'calypso/data/paid-newsletter/use-paid-newsletter-query'; import SubscriberUploadForm from './subscriber-upload-form'; import type { SiteDetails } from '@automattic/data-stores'; type Props = { nextStepUrl: string; - selectedSite?: SiteDetails; + selectedSite: SiteDetails; fromSite: QueryArgParsed; skipNextStep: () => void; + cardData: SubscribersStepContent; + siteSlug: string; + engine: string; }; export default function Subscribers( { nextStepUrl, selectedSite, fromSite, + siteSlug, skipNextStep, + cardData, + engine, }: Props ) { - if ( ! selectedSite ) { - return null; - } + const queryClient = useQueryClient(); + const { importSelector } = useSelect( ( select ) => { + const subscriber = select( Subscriber.store ); + return { + importSelector: subscriber.getImportSubscribersSelector(), + }; + }, [] ); + + const prevInProgress = useRef( importSelector?.inProgress ); + useEffect( () => { + if ( ! prevInProgress.current && importSelector?.inProgress ) { + setTimeout( () => { + queryClient.invalidateQueries( { + queryKey: [ 'paid-newsletter-importer', selectedSite.ID, engine ], + } ); + }, 1500 ); // 1500ms = 1.5s delay so that we have enought time to propagate the changes. + } + + prevInProgress.current = importSelector?.inProgress; + }, [ importSelector?.inProgress ] ); + + const open = cardData?.meta?.status === 'pending' || false; + + const all_emails = toInteger( cardData?.meta?.email_count ) || 0; + const paid_emails = toInteger( cardData?.meta?.paid_subscribers_count ) || 0; + const free_emails = all_emails - paid_emails; + return ( - -

Step 1: Export your subscribers from Substack

-

- To generate a CSV file of all your Substack subscribers, go to the Subscribers tab and click - 'Export.' Once the CSV file is downloaded, upload it in the next step. -

- -
-

Step 2: Import your subscribers to WordPress.com

- { selectedSite.ID && ( - + <> + +

Step 1: Export your subscribers from Substack

+

+ To generate a CSV file of all your Substack subscribers, go to the Subscribers tab and + click 'Export.' Once the CSV file is downloaded, upload it in the next step. +

+ +
+

Step 2: Import your subscribers to WordPress.com

+ { selectedSite.ID && ( + + ) } +
+ { open && ( + {} } + className="subscriber-upload-form__modal" + size="medium" + > +
+ We’ve found { all_emails } subscribers. +
    + { free_emails !== 0 && ( +
  • + + { free_emails } are free subscribers +
  • + ) } + { paid_emails !== 0 && ( +
  • + + { paid_emails } are paying subscribers +
  • + ) } +
+
+ +
) } - + ); } diff --git a/client/my-sites/importer/newsletter/summary.tsx b/client/my-sites/importer/newsletter/summary.tsx index 96eac0f230f6a1..7cce37b3103486 100644 --- a/client/my-sites/importer/newsletter/summary.tsx +++ b/client/my-sites/importer/newsletter/summary.tsx @@ -1,28 +1,44 @@ import { Card, ConfettiAnimation } from '@automattic/components'; -import { Icon, post, people, currencyDollar } from '@wordpress/icons'; +import ContentSummary from './summary/content'; +import SubscribersSummary from './summary/subscribers'; +import type { SiteDetails } from '@automattic/data-stores'; -export default function Summary() { +type Props = { + cardData: any; + selectedSite: SiteDetails; +}; + +export default function Summary( { cardData, selectedSite }: Props ) { const prefersReducedMotion = window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches; + function shouldRenderConfetti( contentStatus: string, subscriberStatue: string ) { + if ( contentStatus === 'done' && subscriberStatue === 'done' ) { + return true; + } + if ( contentStatus === 'done' && subscriberStatue === 'skipped' ) { + return true; + } + + if ( contentStatus === 'skipped' && subscriberStatue === 'done' ) { + return true; + } + + return false; + } return ( - -

Success!

-
-

Here's an overview of what you'll migrate:

-

- - 47 posts -

-

- - 99 subscribers -

-

- - 17paid subscribers -

-
+ { shouldRenderConfetti( cardData.content.status, cardData.subscribers.status ) && ( + <> +

Success! 🎉

+ + ) } + +
); } diff --git a/client/my-sites/importer/newsletter/summary/content.tsx b/client/my-sites/importer/newsletter/summary/content.tsx new file mode 100644 index 00000000000000..b7faefb2aa18e4 --- /dev/null +++ b/client/my-sites/importer/newsletter/summary/content.tsx @@ -0,0 +1,67 @@ +import { Icon, post, media, comment, page } from '@wordpress/icons'; + +type Props = { + cardData: any; + status: string; +}; + +export default function ContentSummary( { status, cardData }: Props ) { + if ( status === 'skipped' ) { + return ( +
+

+ Content importing was skipped! +

+
+ ); + } + + if ( status === 'done' ) { + const progress = cardData.progress; + return ( +
+

We imported:

+ + { progress.post.completed !== 0 && ( +

+ + { progress.post.completed } posts +

+ ) } + + { progress.page.completed !== 0 && ( +

+ + { progress.page.completed } pages +

+ ) } + + { progress.attachment.completed !== 0 && ( +

+ + { progress.attachment.completed } media +

+ ) } + + { progress.comment.completed !== 0 && ( +

+ + { progress.comment.completed } comments +

+ ) } +
+ ); + } + + if ( status === 'importing' || status === 'processing' ) { + return ( +
+

+ Content is importing... +

+
+ ); + } + + return; +} diff --git a/client/my-sites/importer/newsletter/summary/subscribers.tsx b/client/my-sites/importer/newsletter/summary/subscribers.tsx new file mode 100644 index 00000000000000..12d2c806a39e01 --- /dev/null +++ b/client/my-sites/importer/newsletter/summary/subscribers.tsx @@ -0,0 +1,113 @@ +import { FormLabel } from '@automattic/components'; +import { Button } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { Icon, people, currencyDollar, atSymbol } from '@wordpress/icons'; +import { ChangeEvent } from 'react'; +import FormCheckbox from 'calypso/components/forms/form-checkbox'; +import { useSubscriberImportMutation } from 'calypso/data/paid-newsletter/use-subscriber-import-mutation'; + +type Props = { + cardData: any; + status: string; + proStatus: string; + siteId: number; +}; +export default function SubscriberSummary( { status, proStatus, cardData, siteId }: Props ) { + const paidSubscribers = cardData?.meta?.paid_subscribers_count ?? 0; + const hasPaidSubscribers = proStatus !== 'skipped' && parseInt( paidSubscribers ) > 0; + const [ isDisabled, setIsDisabled ] = useState( ! hasPaidSubscribers ); + + const { enqueueSubscriberImport } = useSubscriberImportMutation(); + + const importSubscribers = () => { + enqueueSubscriberImport( siteId, 'substack', 'summary' ); + }; + + const onChange = ( { target: { checked } }: ChangeEvent< HTMLInputElement > ) => + setIsDisabled( checked ); + + if ( status === 'skipped' ) { + return ( +
+

+ Subscriber importing was skipped! +

+
+ ); + } + + if ( status === 'pending' ) { + return ( +
+

Here's an overview of what you'll migrate:

+

+ + { cardData?.meta?.email_count } subscribers +

+ { hasPaidSubscribers && ( +

+ + { cardData?.meta?.paid_subscribers_count } paid subscribers +

+ ) } + { hasPaidSubscribers && ( + <> +

+ Before we import your newsletter +

+ +

+ To prevent any unexpected actions by your old provider, go to your + Stripe dashboard and click “Revoke access” for any service previously associated with + this subscription. +

+ +

+ + + I’ve disconnected other providers from the Stripe account + +

+ + ) } + +
+ ); + } + + if ( status === 'importing' ) { + return ( +
+

+ Importing subscribers... +

+
+ ); + } + + if ( status === 'done' ) { + const paid_subscribers = cardData?.meta?.paid_subscribers_count ?? 0; + const free_subscribers = cardData?.meta?.subscribed_count - paid_subscribers; + return ( +
+

We migrated { cardData.meta.subscribed_count } subscribers

+

+ + { free_subscribers } free subscribers +

+ { hasPaidSubscribers && ( +

+ + { cardData.meta.paid_subscribers_count } paid subscribers +

+ ) } +
+ ); + } +} diff --git a/client/my-sites/marketplace/components/progressbar/index.tsx b/client/my-sites/marketplace/components/progressbar/index.tsx index bb0788de14836f..58b6bf74d5826f 100644 --- a/client/my-sites/marketplace/components/progressbar/index.tsx +++ b/client/my-sites/marketplace/components/progressbar/index.tsx @@ -104,7 +104,7 @@ export default function MarketplaceProgressBar( { { stepValue } { steps.length > 1 &&
{ stepIndication }
} diff --git a/client/my-sites/plans-features-main/hooks/use-default-wpcom-plans-intent.ts b/client/my-sites/plans-features-main/hooks/use-default-wpcom-plans-intent.ts new file mode 100644 index 00000000000000..29f48a41ea6bc7 --- /dev/null +++ b/client/my-sites/plans-features-main/hooks/use-default-wpcom-plans-intent.ts @@ -0,0 +1,18 @@ +import type { PlansIntent } from '@automattic/plans-grid-next'; + +/** + * Used for defining the default plans intent for general WordPress.com plans UI. + * + * The default intent is used in various scenarios, such as: + * - signup flows / plans page that do not require a tailored plans mix + * - switching to the default plans through an escape hatch (button) when a tailored mix is rendered + * - showing the default plans in the comparison grid when a tailored mix is rendered otherwise + * + * When experimenting with different default plans, this hook can be used to define the intent. + * We will need an exclusion mechanism in that case (to not mix with other intents). + */ +const useDefaultWpcomPlansIntent = (): PlansIntent | undefined => { + return 'plans-default-wpcom'; +}; + +export default useDefaultWpcomPlansIntent; diff --git a/client/my-sites/plans-features-main/index.tsx b/client/my-sites/plans-features-main/index.tsx index 7687a36aafc90a..2462aa849054c2 100644 --- a/client/my-sites/plans-features-main/index.tsx +++ b/client/my-sites/plans-features-main/index.tsx @@ -68,6 +68,7 @@ import PlanUpsellModal from './components/plan-upsell-modal'; import { useModalResolutionCallback } from './components/plan-upsell-modal/hooks/use-modal-resolution-callback'; import PlansPageSubheader from './components/plans-page-subheader'; import useCheckPlanAvailabilityForPurchase from './hooks/use-check-plan-availability-for-purchase'; +import useDefaultWpcomPlansIntent from './hooks/use-default-wpcom-plans-intent'; import useFilteredDisplayedIntervals from './hooks/use-filtered-displayed-intervals'; import useGenerateActionHook from './hooks/use-generate-action-hook'; import usePlanBillingPeriod from './hooks/use-plan-billing-period'; @@ -288,9 +289,14 @@ const PlansFeaturesMain = ( { const intentFromSiteMeta = usePlanIntentFromSiteMeta(); const planFromUpsells = usePlanFromUpsells(); + const defaultWpcomPlansIntent = useDefaultWpcomPlansIntent(); const [ forceDefaultPlans, setForceDefaultPlans ] = useState( false ); - const [ intent, setIntent ] = useState< PlansIntent | undefined >( undefined ); + /** + * Keep the `useEffect` here strictly about intent resolution. + * This is fairly critical logic and may generate side effects if not handled properly. + * Let's be especially deliberate about making changes. + */ useEffect( () => { if ( intentFromSiteMeta.processing ) { return; @@ -299,13 +305,13 @@ const PlansFeaturesMain = ( { // TODO: plans from upsell takes precedence for setting intent right now // - this is currently set to the default wpcom set until we have updated tailored features for all plans // - at which point, we'll inject the upsell plan to the tailored plans mix instead - if ( 'plans-default-wpcom' !== intent && forceDefaultPlans ) { - setIntent( 'plans-default-wpcom' ); + if ( defaultWpcomPlansIntent !== intent && forceDefaultPlans ) { + setIntent( defaultWpcomPlansIntent ); } else if ( ! intent ) { setIntent( planFromUpsells - ? 'plans-default-wpcom' - : intentFromProps || intentFromSiteMeta.intent || 'plans-default-wpcom' + ? defaultWpcomPlansIntent + : intentFromProps || intentFromSiteMeta.intent || defaultWpcomPlansIntent ); } }, [ @@ -315,10 +321,11 @@ const PlansFeaturesMain = ( { planFromUpsells, forceDefaultPlans, intentFromSiteMeta.processing, + defaultWpcomPlansIntent, ] ); const showEscapeHatch = - intentFromSiteMeta.intent && ! isInSignup && 'plans-default-wpcom' !== intent; + intentFromSiteMeta.intent && ! isInSignup && defaultWpcomPlansIntent !== intent; const eligibleForFreeHostingTrial = useSelector( isUserEligibleForFreeHostingTrial ); @@ -372,7 +379,7 @@ const PlansFeaturesMain = ( { eligibleForFreeHostingTrial, hasRedeemedDomainCredit: currentPlan?.hasRedeemedDomainCredit, hiddenPlans, - intent, + intent: shouldForceDefaultPlansBasedOnIntent( intent ) ? defaultWpcomPlansIntent : intent, isDisplayingPlansNeededForFeature, isSubdomainNotGenerated: ! resolvedSubdomainName.result, selectedFeature, @@ -383,7 +390,6 @@ const PlansFeaturesMain = ( { term, useCheckPlanAvailabilityForPurchase, useFreeTrialPlanSlugs, - forceDefaultIntent: shouldForceDefaultPlansBasedOnIntent( intent ), } ); // we need only the visible ones for features grid (these should extend into plans-ui data store selectors) @@ -615,7 +621,10 @@ const PlansFeaturesMain = ( { } ); const isLoadingGridPlans = Boolean( - ! intent || ! gridPlansForFeaturesGrid || ! gridPlansForComparisonGrid + ! intent || + ! defaultWpcomPlansIntent || // this may be unnecessary, but just in case + ! gridPlansForFeaturesGrid || + ! gridPlansForComparisonGrid ); const isPlansGridReady = ! isLoadingGridPlans && ! resolvedSubdomainName.isLoading; diff --git a/client/my-sites/plans/jetpack-plans/product-store/items-list/all-items.tsx b/client/my-sites/plans/jetpack-plans/product-store/items-list/all-items.tsx index 15de5f6f4a2a17..7f3920bcb4abcc 100644 --- a/client/my-sites/plans/jetpack-plans/product-store/items-list/all-items.tsx +++ b/client/my-sites/plans/jetpack-plans/product-store/items-list/all-items.tsx @@ -1,6 +1,5 @@ import { isEnabled } from '@automattic/calypso-config'; import { - isJetpackAISlug, isJetpackPlanSlug, isJetpackSocialSlug, isJetpackStatsPaidProductSlug, @@ -105,7 +104,6 @@ export const AllItems: React.FC< AllItemsProps > = ( { const isMultiPlanSelectProduct = ( isJetpackSocialSlug( item.productSlug ) && ! isEnabled( 'jetpack/social-plans-v1' ) ) || - isJetpackAISlug( item.productSlug ) || isJetpackStatsPaidProductSlug( item.productSlug ); let ctaHref = getCheckoutURL( item ); diff --git a/client/my-sites/plans/jetpack-plans/slug-to-selector-product.ts b/client/my-sites/plans/jetpack-plans/slug-to-selector-product.ts index cd6d290bca36b7..a5e4c57930e896 100644 --- a/client/my-sites/plans/jetpack-plans/slug-to-selector-product.ts +++ b/client/my-sites/plans/jetpack-plans/slug-to-selector-product.ts @@ -32,6 +32,7 @@ import { getJetpackProductRecommendedFor, getJetpackPlanAlsoIncludedFeatures, TERM_TRIENNIALLY, + isJetpackAISlug, } from '@automattic/calypso-products'; import { getProductPartsFromAlias } from 'calypso/my-sites/checkout/src/hooks/use-prepare-products-for-cart'; import { @@ -103,7 +104,11 @@ function slugToItem( slug: string ): Plan | Product | SelectorProduct | null | u return null; } -function getDisclaimerLink() { +function getDisclaimerLink( item: Product | Plan ) { + if ( objectIsProduct( item ) && isJetpackAISlug( item.product_slug ) ) { + return 'https://jetpack.com/redirect/?source=ai-assistant-fair-usage-policy'; + } + const backupStorageFaqId = 'backup-storage-limits-lightbox-faq'; return `#${ backupStorageFaqId }`; } @@ -190,7 +195,11 @@ function itemToSelectorProduct( features: { items: features, }, - disclaimer: getJetpackProductDisclaimer( item.product_slug, features, getDisclaimerLink() ), + disclaimer: getJetpackProductDisclaimer( + item.product_slug, + features, + getDisclaimerLink( item ) + ), quantity, }; } @@ -234,7 +243,11 @@ function itemToSelectorProduct( features: { items: buildCardFeaturesFromItem( item ), }, - disclaimer: getJetpackProductDisclaimer( item.getStoreSlug(), features, getDisclaimerLink() ), + disclaimer: getJetpackProductDisclaimer( + item.getStoreSlug(), + features, + getDisclaimerLink( item ) + ), legacy: ! isResetPlan, }; } diff --git a/client/my-sites/plugins/controller-logged-in.js b/client/my-sites/plugins/controller-logged-in.js index e2d7f4aec6f6f5..1cde9fd01198f6 100644 --- a/client/my-sites/plugins/controller-logged-in.js +++ b/client/my-sites/plugins/controller-logged-in.js @@ -1,3 +1,6 @@ +import { removeQueryArgs } from '@wordpress/url'; +import { translate } from 'i18n-calypso'; +import { successNotice } from 'calypso/state/notices/actions'; import Plans from './plans'; import PluginUpload from './plugin-upload'; @@ -6,6 +9,22 @@ export function upload( context, next ) { next(); } +export function maybeShowUpgradeSuccessNotice( context, next ) { + if ( context.query.showUpgradeSuccessNotice ) { + // Bump the notice to the back of the callstack so it is called after client render. + setTimeout( () => { + context.store.dispatch( + successNotice( translate( 'Thank you for your purchase!' ), { + id: 'plugin-upload-upgrade-plan-success', + duration: 5000, + } ) + ); + }, 0 ); + context.page.replace( removeQueryArgs( context.canonicalPath, 'showUpgradeSuccessNotice' ) ); + } + next(); +} + export function plans( context, next ) { context.primary = ( isRequestingForAllSites( state ) ); + const toggleDisplayManageSitePluginsModal = useCallback( () => { setDisplayManageSitePluginsModal( ( value ) => ! value ); }, [] ); @@ -525,6 +528,7 @@ function ManageSitesButton( { plugin, installedOnSitesQuantity } ) { diff --git a/client/my-sites/plugins/plugins-browser-item/index.jsx b/client/my-sites/plugins/plugins-browser-item/index.jsx index ff029a45561d65..59d4645c3baa15 100644 --- a/client/my-sites/plugins/plugins-browser-item/index.jsx +++ b/client/my-sites/plugins/plugins-browser-item/index.jsx @@ -220,16 +220,18 @@ const PluginsBrowserListElement = ( props ) => { onClick={ onClickItem } >
- -
{ plugin.name }
- { variant === PluginsBrowserElementVariant.Extended && ( - <> -
- { translate( 'by ' ) } - { plugin.author_name } -
- - ) } +
+ +
{ plugin.name }
+ { variant === PluginsBrowserElementVariant.Extended && ( + <> +
+ { translate( 'by ' ) } + { plugin.author_name } +
+ + ) } +
{ plugin.short_description }
{ isUntestedVersion && ( @@ -434,9 +436,11 @@ function Placeholder( { variant } ) {
  • - -
    -
    +
    + +
    +
    +
    diff --git a/client/my-sites/plugins/plugins-browser-item/style.scss b/client/my-sites/plugins/plugins-browser-item/style.scss index ccde622053ac00..92312a7a2fc151 100644 --- a/client/my-sites/plugins/plugins-browser-item/style.scss +++ b/client/my-sites/plugins/plugins-browser-item/style.scss @@ -36,6 +36,14 @@ margin-left: calc(47px + 16px); // icon width + margin } + .plugins-browser-item__title { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + white-space: unset; + } + .plugins-browser-item__ratings { display: flex; justify-content: flex-end; @@ -62,7 +70,7 @@ } .plugins-browser-item__description { - margin: 24px 0; + margin: 12px 0 24px 0; font-family: "SF Pro Text", $sans; font-size: $font-body-small; font-weight: 400; @@ -70,9 +78,9 @@ color: $studio-gray-80; // limit to 2 lines - height: calc(20px * 2); // line height * number of lines + height: calc(20px * 3); // line height * number of lines display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } @@ -179,11 +187,15 @@ } } +.plugins-browser-item__header { + min-height: 66px; +} + .plugins-browser-item__title { color: var(--color-neutral-100); font-weight: 500; font-size: $font-body; - line-height: 24px; + line-height: 20px; } .plugins-browser-item__author { diff --git a/client/my-sites/purchases/payment-methods/index.tsx b/client/my-sites/purchases/payment-methods/index.tsx index bfdcf7dba8784f..93b3ec8f274e62 100644 --- a/client/my-sites/purchases/payment-methods/index.tsx +++ b/client/my-sites/purchases/payment-methods/index.tsx @@ -26,6 +26,7 @@ import { useCreateCreditCard } from 'calypso/my-sites/checkout/src/hooks/use-cre import { logStashLoadErrorEvent } from 'calypso/my-sites/checkout/src/lib/analytics'; import PurchasesNavigation from 'calypso/my-sites/purchases/navigation'; import { useDispatch, useSelector } from 'calypso/state'; +import { getCurrentUserCurrencyCode } from 'calypso/state/currency-code/selectors'; import { getCurrentUserLocale } from 'calypso/state/current-user/selectors'; import { errorNotice } from 'calypso/state/notices/actions'; import { getAddNewPaymentMethodUrlFor, getPaymentMethodsUrlFor } from '../paths'; @@ -84,9 +85,11 @@ function SiteLevelAddNewPaymentMethodForm( { siteSlug }: { siteSlug: string } ) const logPaymentMethodsError = useLogPaymentMethodsError( 'site level add new payment method load error' ); + const currency = useSelector( getCurrentUserCurrencyCode ); const { isStripeLoading, stripeLoadingError } = useStripe(); const stripeMethod = useCreateCreditCard( { + currency, isStripeLoading, stripeLoadingError, shouldUseEbanx: false, diff --git a/client/my-sites/sidebar/static-data/fallback-menu.js b/client/my-sites/sidebar/static-data/fallback-menu.js index 1c03f50a482116..78546c3917a64f 100644 --- a/client/my-sites/sidebar/static-data/fallback-menu.js +++ b/client/my-sites/sidebar/static-data/fallback-menu.js @@ -28,6 +28,8 @@ const WOOCOMMERCE_ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3 export default function buildFallbackResponse( { siteDomain = '', + isAtomic, + isPlanExpired, shouldShowMailboxes = false, shouldShowLinks = false, shouldShowTestimonials = false, @@ -600,7 +602,10 @@ export default function buildFallbackResponse( { { parent: 'options-general.php', slug: 'options-hosting-configuration-php', - title: translate( 'Hosting Configuration' ), + title: + isAtomic && ! isPlanExpired + ? translate( 'Server Settings' ) + : translate( 'Hosting Features' ), type: 'submenu-item', url: `/hosting-config/${ siteDomain }`, }, diff --git a/client/my-sites/sidebar/use-site-menu-items.js b/client/my-sites/sidebar/use-site-menu-items.js index 029309154c8a80..a4ba433d0b65b2 100644 --- a/client/my-sites/sidebar/use-site-menu-items.js +++ b/client/my-sites/sidebar/use-site-menu-items.js @@ -16,7 +16,7 @@ import isAtomicSite from 'calypso/state/selectors/is-site-automated-transfer'; import isSiteWpcomStaging from 'calypso/state/selectors/is-site-wpcom-staging'; import isSiteWPForTeams from 'calypso/state/selectors/is-site-wpforteams'; import { getSiteDomain, isJetpackSite } from 'calypso/state/sites/selectors'; -import { getSelectedSiteId } from 'calypso/state/ui/selectors'; +import { getSelectedSite, getSelectedSiteId } from 'calypso/state/ui/selectors'; import { requestAdminMenu } from '../../state/admin-menu/actions'; import allSitesMenu from './static-data/all-sites-menu'; import buildFallbackResponse from './static-data/fallback-menu'; @@ -32,6 +32,7 @@ const useSiteMenuItems = () => { const isJetpack = useSelector( ( state ) => isJetpackSite( state, selectedSiteId ) ); const isAtomic = useSelector( ( state ) => isAtomicSite( state, selectedSiteId ) ); const isStagingSite = useSelector( ( state ) => isSiteWpcomStaging( state, selectedSiteId ) ); + const isPlanExpired = useSelector( ( state ) => !! getSelectedSite( state )?.plan?.expired ); const locale = useLocale(); const isAllDomainsView = '/domains/manage' === currentRoute; const { currentSection } = useCurrentRoute(); @@ -109,6 +110,8 @@ const useSiteMenuItems = () => { */ const fallbackDataOverrides = { siteDomain, + isAtomic, + isPlanExpired, shouldShowWooCommerce, shouldShowThemes, shouldShowMailboxes, diff --git a/client/my-sites/sidebar/utils.js b/client/my-sites/sidebar/utils.js index 401a548709c0bb..4d54f772a557de 100644 --- a/client/my-sites/sidebar/utils.js +++ b/client/my-sites/sidebar/utils.js @@ -40,11 +40,11 @@ export const itemLinkMatches = ( path, currentPath ) => { } if ( pathIncludes( currentPath, 'plugins', 1 ) ) { - if ( pathIncludes( currentPath, 'browse', 2 ) ) { - return pathIncludes( path, 'plugins', 1 ) && ! pathIncludes( path, 'scheduled-updates', 2 ); + if ( pathIncludes( currentPath, 'scheduled-updates', 2 ) ) { + return pathIncludes( path, 'plugins', 1 ) && pathIncludes( path, 'scheduled-updates', 2 ); } - return pathIncludes( path, 'plugins', 1 ) && fragmentIsEqual( path, currentPath, 2 ); + return pathIncludes( path, 'plugins', 1 ) && fragmentIsEqual( path, currentPath, 1 ); } if ( pathIncludes( currentPath, 'settings', 1 ) ) { diff --git a/client/my-sites/site-monitoring/components/time-range-picker/index.jsx b/client/my-sites/site-monitoring/components/time-range-picker/index.jsx index 6a44bc4a128891..7c5eb6acfedeea 100644 --- a/client/my-sites/site-monitoring/components/time-range-picker/index.jsx +++ b/client/my-sites/site-monitoring/components/time-range-picker/index.jsx @@ -67,7 +67,7 @@ export const TimeDateChartControls = ( { onTimeRangeChange } ) => {
    { translate( 'Time range' ) }
    - + { options.map( ( option ) => { return ( , - footer: translate( '%(percent)d%% of views', { - args: { percent: bestViewsEverPercent || 0 }, + footer: translate( '%(percent)s of views', { + args: { percent: formatPercentage( bestViewsEverPercent, true ) }, context: 'Stats: Percentage of views', } ), }, diff --git a/client/my-sites/stats/components/stats-button/stats-button.tsx b/client/my-sites/stats/components/stats-button/stats-button.tsx index 619e7067bc94cc..0641828e306505 100644 --- a/client/my-sites/stats/components/stats-button/stats-button.tsx +++ b/client/my-sites/stats/components/stats-button/stats-button.tsx @@ -8,6 +8,7 @@ import { getSelectedSiteId } from 'calypso/state/ui/selectors'; interface StatsButtonProps extends React.ButtonHTMLAttributes< HTMLButtonElement > { children?: React.ReactNode; primary?: boolean; + busy?: boolean; } const StatsButton: React.FC< StatsButtonProps > = ( { children, primary, ...rest } ) => { @@ -23,6 +24,7 @@ const StatsButton: React.FC< StatsButtonProps > = ( { children, primary, ...rest } ) } variant="primary" primary={ isWPCOMSite ? true : undefined } + isBusy={ rest.busy } { ...rest } > { children } diff --git a/client/my-sites/stats/feedback/index.tsx b/client/my-sites/stats/feedback/index.tsx index 84b35c1f0b21c6..82c191dcf751f9 100644 --- a/client/my-sites/stats/feedback/index.tsx +++ b/client/my-sites/stats/feedback/index.tsx @@ -1,24 +1,124 @@ +import { Button } from '@wordpress/components'; +import { close } from '@wordpress/icons'; +import { useTranslate } from 'i18n-calypso'; import { useState } from 'react'; import FeedbackModal from './modal'; import './style.scss'; -function StatsFeedbackCard() { - // A simple card component with feedback buttons. - const [ isOpen, setIsOpen ] = useState( false ); +const FEEDBACK_ACTION_LEAVE_REVIEW = 'feedback-action-leave-review'; +const FEEDBACK_ACTION_SEND_FEEDBACK = 'feedback-action-send-feedback'; +const FEEDBACK_ACTION_DISMISS_FLOATING_PANEL = 'feedback-action-dismiss-floating-panel'; + +const FEEDBACK_LEAVE_REVIEW_URL = 'https://wordpress.org/support/plugin/jetpack/reviews/'; + +interface FeedbackProps { + siteId: number; +} + +interface FeedbackPropsInternal { + clickHandler: ( action: string ) => void; + isOpen?: boolean; +} + +function FeedbackContent( { clickHandler }: FeedbackPropsInternal ) { + const translate = useTranslate(); + + const ctaText = translate( 'How do you rate your overall experience with Jetpack Stats?' ); + const primaryButtonText = translate( 'Love it? Leave a review ↗' ); + const secondaryButtonText = translate( 'Not a fan? Help us improve' ); + + const handleLeaveReview = () => { + clickHandler( FEEDBACK_ACTION_LEAVE_REVIEW ); + }; + + const handleSendFeedback = () => { + clickHandler( FEEDBACK_ACTION_SEND_FEEDBACK ); + }; return ( -
    -
    -

    Hello from StatsFeedbackCard

    +
    +
    { ctaText }
    +
    + +
    -
    - - -
    - setIsOpen( false ) } />
    ); } -export default StatsFeedbackCard; +function FeedbackPanel( { isOpen, clickHandler }: FeedbackPropsInternal ) { + const translate = useTranslate(); + + const handleCloseButtonClicked = () => { + clickHandler( FEEDBACK_ACTION_DISMISS_FLOATING_PANEL ); + }; + + if ( ! isOpen ) { + return null; + } + + return ( +
    + +
    + ); +} + +function FeedbackCard( { clickHandler }: FeedbackPropsInternal ) { + return ( +
    + +
    + ); +} + +function StatsFeedbackController( { siteId }: FeedbackProps ) { + const [ isOpen, setIsOpen ] = useState( false ); + const [ isFloatingPanelOpen, setIsFloatingPanelOpen ] = useState( true ); + + const handleButtonClick = ( action: string ) => { + switch ( action ) { + case FEEDBACK_ACTION_SEND_FEEDBACK: + setIsOpen( true ); + break; + case FEEDBACK_ACTION_DISMISS_FLOATING_PANEL: + setIsFloatingPanelOpen( false ); + break; + case FEEDBACK_ACTION_LEAVE_REVIEW: + setIsFloatingPanelOpen( false ); + window.open( FEEDBACK_LEAVE_REVIEW_URL ); + break; + // Ignore other cases. + } + }; + + return ( +
    + + + { isOpen && setIsOpen( false ) } /> } +
    + ); +} + +export default StatsFeedbackController; diff --git a/client/my-sites/stats/feedback/modal/index.tsx b/client/my-sites/stats/feedback/modal/index.tsx index 7ec358ea08f533..3bf414bfdfbb7a 100644 --- a/client/my-sites/stats/feedback/modal/index.tsx +++ b/client/my-sites/stats/feedback/modal/index.tsx @@ -1,32 +1,66 @@ import { Button, Modal, TextareaControl } from '@wordpress/components'; import { close } from '@wordpress/icons'; import { useTranslate } from 'i18n-calypso'; -import React, { useState } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import StatsButton from 'calypso/my-sites/stats/components/stats-button'; +import { useDispatch } from 'calypso/state'; +import { recordTracksEvent } from 'calypso/state/analytics/actions'; +import { successNotice } from 'calypso/state/notices/actions'; +import useSubmitProductFeedback from './use-submit-product-feedback'; import './style.scss'; interface ModalProps { - isOpen: boolean; + siteId: number; onClose: () => void; } -const FeedbackModal: React.FC< ModalProps > = ( { isOpen, onClose } ) => { +const FeedbackModal: React.FC< ModalProps > = ( { siteId, onClose } ) => { const translate = useTranslate(); - const [ isAnimating, setIsAnimating ] = useState( false ); + const dispatch = useDispatch(); const [ content, setContent ] = useState( '' ); - const handleClose = () => { - setIsAnimating( true ); + const { isSubmittingFeedback, submitFeedback, isSubmissionSuccessful } = + useSubmitProductFeedback( siteId ); + + const handleClose = useCallback( () => { setTimeout( () => { - setIsAnimating( false ); onClose(); }, 200 ); - }; + }, [ onClose ] ); + + const onFormSubmit = useCallback( () => { + if ( ! content ) { + return; + } + + dispatch( + recordTracksEvent( 'calypso_jetpack_stats_user_feedback_form_submit', { + feedback: content, + } ) + ); + + const sourceUrl = `${ window.location.origin }${ window.location.pathname }`; + submitFeedback( { + source_url: sourceUrl, + product_name: 'Jetpack Stats', + feedback: content, + is_testing: true, + } ); + }, [ dispatch, content, submitFeedback ] ); + + useEffect( () => { + if ( isSubmissionSuccessful ) { + dispatch( + successNotice( translate( 'Thank you for your feedback!' ), { + id: 'submit-product-feedback-success', + duration: 5000, + } ) + ); - if ( ! isOpen && ! isAnimating ) { - return null; - } + handleClose(); + } + }, [ dispatch, isSubmissionSuccessful, handleClose, translate ] ); return ( @@ -56,7 +90,14 @@ const FeedbackModal: React.FC< ModalProps > = ( { isOpen, onClose } ) => { onChange={ setContent } />
    - { translate( 'Submit' ) } + + { translate( 'Submit' ) } +
    diff --git a/client/my-sites/stats/feedback/modal/use-submit-product-feedback-mutation.ts b/client/my-sites/stats/feedback/modal/use-submit-product-feedback-mutation.ts new file mode 100644 index 00000000000000..9586770bc2dbe2 --- /dev/null +++ b/client/my-sites/stats/feedback/modal/use-submit-product-feedback-mutation.ts @@ -0,0 +1,29 @@ +import { useMutation, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import wpcom from 'calypso/lib/wp'; +import type { SubmitJetpackStatsFeedbackParams } from './use-submit-product-feedback'; +import type { APIError } from 'calypso/jetpack-cloud/sections/agency-dashboard/sites-overview/types'; + +interface APIResponse { + success: boolean; +} + +function mutationSubmitProductFeedback( + params: SubmitJetpackStatsFeedbackParams, + siteId?: number +): Promise< APIResponse > { + return wpcom.req.post( { + apiNamespace: 'wpcom/v2', + path: `/sites/${ siteId }/jetpack-stats/user-feedback`, + body: params, + } ); +} + +export default function useSubmitProductFeedbackMutation< TContext = unknown >( + siteId?: number, + options?: UseMutationOptions< APIResponse, APIError, SubmitJetpackStatsFeedbackParams, TContext > +): UseMutationResult< APIResponse, APIError, SubmitJetpackStatsFeedbackParams, TContext > { + return useMutation< APIResponse, APIError, SubmitJetpackStatsFeedbackParams, TContext >( { + ...options, + mutationFn: ( params ) => mutationSubmitProductFeedback( params, siteId ), + } ); +} diff --git a/client/my-sites/stats/feedback/modal/use-submit-product-feedback.ts b/client/my-sites/stats/feedback/modal/use-submit-product-feedback.ts new file mode 100644 index 00000000000000..89333254e58625 --- /dev/null +++ b/client/my-sites/stats/feedback/modal/use-submit-product-feedback.ts @@ -0,0 +1,49 @@ +import { useTranslate } from 'i18n-calypso'; +import { useEffect } from 'react'; +import { useDispatch } from 'calypso/state'; +import { errorNotice } from 'calypso/state/notices/actions'; +import useSubmitProductFeedbackMutation from './use-submit-product-feedback-mutation'; + +export interface SubmitJetpackStatsFeedbackParams { + source_url: string; + product_name: string; + feedback: string; + // TODO: Remove this flag once we're ready to send real feedback. + is_testing?: boolean; +} + +export default function useSubmitProductFeedback( siteId: number ): { + isSubmittingFeedback: boolean; + submitFeedback: ( params: SubmitJetpackStatsFeedbackParams ) => void; + isSubmissionSuccessful: boolean; + resetMutation: () => void; +} { + const translate = useTranslate(); + const dispatch = useDispatch(); + + const { + isError, + isSuccess, + mutate, + isPending: isSubmittingFeedback, + reset, + } = useSubmitProductFeedbackMutation( siteId ); + + useEffect( () => { + if ( isError ) { + dispatch( + errorNotice( translate( 'Something went wrong. Please try again.' ), { + id: 'submit-product-feedback-failure', + duration: 5000, + } ) + ); + } + }, [ translate, isError, dispatch ] ); + + return { + isSubmittingFeedback, + submitFeedback: mutate, + isSubmissionSuccessful: isSuccess, + resetMutation: reset, + }; +} diff --git a/client/my-sites/stats/feedback/style.scss b/client/my-sites/stats/feedback/style.scss index 640544a4c02c55..05756925b6fa95 100644 --- a/client/my-sites/stats/feedback/style.scss +++ b/client/my-sites/stats/feedback/style.scss @@ -1,8 +1,127 @@ +@import "@automattic/components/src/styles/typography"; +@import "@wordpress/base-styles/breakpoints"; + +.stats-feedback-content { + font-family: $font-sf-pro-text; + font-size: $font-body-small; + font-weight: 400; + line-height: 21px; + letter-spacing: -0.24px; + color: var(--studio-gray-100); +} + +.stats-feedback-content__actions { + display: flex; + flex-direction: column; + + .components-button { + width: fit-content; + font-weight: 500; + font-size: $font-body-small; + border-radius: 4px; + + &:not(:last-child) { + margin-bottom: 8px; + } + } +} + +.stats-feedback-content__cta { + margin: 0 16px 16px 0; +} + +.stats-feedback-content__emoji { + font-size: larger; + margin-right: 6px; +} + .stats-feedback-card { background: var(--studio-white); border: 1px solid var(--studio-gray-5); border-radius: 5px; // stylelint-disable-line scales/radii padding: 20px; - margin: 32px; + + @media (max-width: $break-medium) { + border-radius: 0; + border-top: none; + border-left: none; + border-right: none; + } + + .stats-feedback-content { + display: flex; + flex-direction: column; + + @media (min-width: $break-wide) { + flex-direction: row; + justify-content: space-between; + align-items: center; + } + } + + .stats-feedback-content__cta { + @media (min-width: $break-wide) { + margin-right: 12px; + margin-bottom: 0; + } + } + + .stats-feedback-content__actions { + @media (min-width: $break-wide) { + flex-direction: row; + + .components-button { + margin-left: 6px; + margin-bottom: 0; + } + } + @media (max-width: $break-mobile) { + .components-button { + width: 100%; + } + } + } +} + +.stats-feedback-panel { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 10; + + border: 1px solid var(--studio-gray-5); + border-radius: 8px; // stylelint-disable-line scales/radii + padding: 24px; + width: 300px; + box-sizing: border-box; + box-shadow: 0 10px 20px 0 #00000014; + + background-color: var(--studio-white); + + .components-button.is-link { + color: var(--studio-gray-100); + } +} + +.stats-feedback-panel__close-button { + position: absolute; + top: 10px; + right: 10px; + + svg { + width: 14px; + height: 14px; + } +} + +.stats-feedback-panel__dismiss-button { + font-family: $font-sf-pro-text; + font-size: $font-body-small; + font-weight: 500; + line-height: 21px; + + &.components-button { + margin-top: 16px; + } } diff --git a/client/my-sites/stats/modernized-stats-table-styles.scss b/client/my-sites/stats/modernized-stats-table-styles.scss index dd1c278a7497d7..605424231a060c 100644 --- a/client/my-sites/stats/modernized-stats-table-styles.scss +++ b/client/my-sites/stats/modernized-stats-table-styles.scss @@ -108,7 +108,6 @@ $common-border-radius: 4px; } table { - table-layout: fixed; min-width: 900px; td, diff --git a/client/my-sites/stats/site.jsx b/client/my-sites/stats/site.jsx index 140ef34009499c..0f553d06b9a21a 100644 --- a/client/my-sites/stats/site.jsx +++ b/client/my-sites/stats/site.jsx @@ -58,7 +58,7 @@ import StatsModuleSearch from './features/modules/stats-search'; import StatsModuleTopPosts from './features/modules/stats-top-posts'; import StatsModuleUTM, { StatsModuleUTMOverlay } from './features/modules/stats-utm'; import StatsModuleVideos from './features/modules/stats-videos'; -import StatsFeedbackCard from './feedback'; +import StatsFeedbackController from './feedback'; import HighlightsSection from './highlights-section'; import { shouldGateStats } from './hooks/use-should-gate-stats'; import MiniCarousel from './mini-carousel'; @@ -242,7 +242,7 @@ class StatsSite extends Component { shouldForceDefaultDateRange, } = this.props; const isNewStateEnabled = config.isEnabled( 'stats/empty-module-traffic' ); - const isFeedbackCardEnabled = config.isEnabled( 'stats/user-feedback' ); + const isUserFeedbackEnabled = config.isEnabled( 'stats/user-feedback' ); let defaultPeriod = PAST_SEVEN_DAYS; const shouldShowUpsells = isOdysseyStats && ! isAtomic; @@ -810,7 +810,7 @@ class StatsSite extends Component { ) } - { isFeedbackCardEnabled && } + { isUserFeedbackEnabled && } { this.props.upsellModalView && } diff --git a/client/my-sites/stats/stats-subscribers-highlight-section/index.tsx b/client/my-sites/stats/stats-subscribers-highlight-section/index.tsx index 24a9b286467415..4867afa9639ab3 100644 --- a/client/my-sites/stats/stats-subscribers-highlight-section/index.tsx +++ b/client/my-sites/stats/stats-subscribers-highlight-section/index.tsx @@ -1,6 +1,6 @@ import { ComponentSwapper, - CountComparisonCard, + CountCard, MobileHighlightCardListing, Spinner, } from '@automattic/components'; @@ -87,12 +87,12 @@ function SubscriberHighlightsStandard( { return (
    { highlights.map( ( highlight ) => ( - ) ) }
    diff --git a/client/my-sites/stats/stats-subscribers-overview/index.tsx b/client/my-sites/stats/stats-subscribers-overview/index.tsx index f03c6aeca7c314..202d565e0cfff0 100644 --- a/client/my-sites/stats/stats-subscribers-overview/index.tsx +++ b/client/my-sites/stats/stats-subscribers-overview/index.tsx @@ -1,4 +1,4 @@ -import { CountComparisonCard } from '@automattic/components'; +import { CountCard } from '@automattic/components'; import React from 'react'; import useSubscribersOverview from 'calypso/my-sites/stats/hooks/use-subscribers-overview'; @@ -15,12 +15,11 @@ const SubscribersOverview: React.FC< SubscribersOverviewProps > = ( { siteId } ) { overviewData.map( ( { count, heading }, index ) => { return ( // TODO: Communicate loading vs error state to the user. - ); } ) } diff --git a/client/my-sites/stats/wordads/highlights-section.jsx b/client/my-sites/stats/wordads/highlights-section.jsx index ec6919396b5a36..e4af02d3c92561 100644 --- a/client/my-sites/stats/wordads/highlights-section.jsx +++ b/client/my-sites/stats/wordads/highlights-section.jsx @@ -131,7 +131,7 @@ function HighlightsListing( { highlights } ) { } value={ highlight.value } /> ) ) } diff --git a/client/package.json b/client/package.json index 0f9e56a62bf335..500045e15cb3f5 100644 --- a/client/package.json +++ b/client/package.json @@ -27,7 +27,7 @@ "@automattic/calypso-sentry": "workspace:^", "@automattic/calypso-stripe": "workspace:^", "@automattic/calypso-url": "workspace:^", - "@automattic/color-studio": "2.6.0", + "@automattic/color-studio": "^3.0.1", "@automattic/command-palette": "workspace:^", "@automattic/components": "workspace:^", "@automattic/composite-checkout": "workspace:^", @@ -94,7 +94,7 @@ "@wordpress/components": "^28.2.0", "@wordpress/compose": "^7.2.0", "@wordpress/data": "^10.2.0", - "@wordpress/dataviews": "patch:@wordpress/dataviews@npm%3A0.4.1#~/.yarn/patches/@wordpress-dataviews-npm-0.4.1-2c01fa0792.patch", + "@wordpress/dataviews": "^4.2.0", "@wordpress/dom": "^4.2.0", "@wordpress/edit-post": "^8.2.0", "@wordpress/element": "^6.2.0", diff --git a/client/performance-profiler/components/charts/history-chart.jsx b/client/performance-profiler/components/charts/history-chart.jsx index d62d9a5365327c..db0ee8dcdafd51 100644 --- a/client/performance-profiler/components/charts/history-chart.jsx +++ b/client/performance-profiler/components/charts/history-chart.jsx @@ -1,3 +1,4 @@ +import { useResizeObserver } from '@wordpress/compose'; import { Icon, info } from '@wordpress/icons'; import { extent as d3Extent, max as d3Max } from 'd3-array'; import { axisBottom as d3AxisBottom, axisLeft as d3AxisLeft } from 'd3-axis'; @@ -140,8 +141,8 @@ const showTooltip = ( tooltip, data, ev = null ) => { tooltip.style( 'opacity', 1 ); tooltip .html( data ) - .style( 'left', event.pageX - 28 + 'px' ) - .style( 'top', event.pageY - 50 + 'px' ); + .style( 'left', event.layerX - 28 + 'px' ) + .style( 'top', event.layerY - 50 + 'px' ); }; // Hide tooltip on mouse out @@ -180,7 +181,7 @@ const generateSampleData = ( range ) => { return data; }; -const HistoryChart = ( { data, range, height, width } ) => { +const HistoryChart = ( { data, range, height } ) => { const svgRef = createRef(); const tooltipRef = createRef(); const dataAvailable = data && data.some( ( e ) => e.value !== null ); @@ -189,11 +190,17 @@ const HistoryChart = ( { data, range, height, width } ) => { data = generateSampleData( range ); } + const [ resizeObserverRef, entry ] = useResizeObserver(); + useEffect( () => { + if ( ! entry ) { + return; + } // Clear previous chart d3Select( svgRef.current ).selectAll( '*' ).remove(); - const margin = { top: 20, right: 0, bottom: 40, left: 40 }; + const width = entry.width; + const margin = { top: 20, right: 20, bottom: 40, left: 40 }; const { xScale, yScale, colorScale } = createScales( data, range, margin, width, height ); @@ -208,7 +215,7 @@ const HistoryChart = ( { data, range, height, width } ) => { const tooltip = d3Select( tooltipRef.current ).attr( 'class', 'tooltip' ); dataAvailable && drawDots( svg, data, xScale, yScale, colorScale, range, tooltip ); - }, [ dataAvailable, data, range, height, width ] ); + }, [ dataAvailable, data, range, svgRef, tooltipRef, height, entry ] ); const handleInfoToolTip = ( event ) => { const tooltip = d3Select( tooltipRef.current ); @@ -223,6 +230,7 @@ const HistoryChart = ( { data, range, height, width } ) => { return (
    + { resizeObserverRef }
    diff --git a/client/performance-profiler/components/charts/style.scss b/client/performance-profiler/components/charts/style.scss index 49b9f141fcde6c..664c8704fc8d8b 100644 --- a/client/performance-profiler/components/charts/style.scss +++ b/client/performance-profiler/components/charts/style.scss @@ -2,7 +2,8 @@ .chart-container { cursor: pointer; - display: inline-block; + position: relative; + max-width: 550px; .tick { line { diff --git a/client/performance-profiler/components/core-web-vitals-accordion/index.tsx b/client/performance-profiler/components/core-web-vitals-accordion/index.tsx new file mode 100644 index 00000000000000..734487ad1b0cf9 --- /dev/null +++ b/client/performance-profiler/components/core-web-vitals-accordion/index.tsx @@ -0,0 +1,90 @@ +import { FoldableCard } from '@automattic/components'; +import { useTranslate } from 'i18n-calypso'; +import { Metrics } from 'calypso/data/site-profiler/types'; +import { + metricsNames, + mapThresholdsToStatus, + displayValue, +} from 'calypso/performance-profiler/utils/metrics'; +import { StatusIndicator } from '../status-indicator'; + +import './styles.scss'; + +type Props = Record< Metrics, number > & { + activeTab: Metrics | null; + setActiveTab: ( tab: Metrics | null ) => void; + children: React.ReactNode; +}; +type HeaderProps = { + displayName: string; + metricKey: Metrics; + metricValue: number; +}; + +const CardHeader = ( props: HeaderProps ) => { + const { displayName, metricKey, metricValue } = props; + return ( +
    + +
    + { displayName } + + { displayValue( metricKey, metricValue ) } + +
    +
    + ); +}; + +export const CoreWebVitalsAccordion = ( props: Props ) => { + const { activeTab, setActiveTab, children } = props; + const translate = useTranslate(); + + const onClick = ( key: Metrics ) => { + // If the user clicks the current tab, close it. + if ( key === activeTab ) { + setActiveTab( null ); + } else { + setActiveTab( key as Metrics ); + } + }; + + return ( +
    + { Object.entries( metricsNames ).map( ( [ key, { displayName } ] ) => { + if ( props[ key as Metrics ] === undefined || props[ key as Metrics ] === null ) { + return null; + } + + // Only display TBT if INP is not available + if ( key === 'tbt' && props[ 'inp' ] !== undefined && props[ 'inp' ] !== null ) { + return null; + } + + return ( + + } + hideSummary + screenReaderText={ translate( 'More' ) } + compact + clickableHeader + smooth + iconSize={ 18 } + onClick={ () => onClick( key as Metrics ) } + expanded={ key === activeTab } + > + { children } + + ); + } ) } +
    + ); +}; diff --git a/client/performance-profiler/components/core-web-vitals-accordion/styles.scss b/client/performance-profiler/components/core-web-vitals-accordion/styles.scss new file mode 100644 index 00000000000000..74147294368f72 --- /dev/null +++ b/client/performance-profiler/components/core-web-vitals-accordion/styles.scss @@ -0,0 +1,42 @@ +$blueberry-color: #3858e9; + +.core-web-vitals-accordion { + .core-web-vitals-accordion__card { + border-top: 0; + .foldable-card__content { + border-top: 0; + } + &.foldable-card { + box-shadow: none; + border-top: 1px solid var(--studio-gray-5); + + &:last-child { + border-bottom: 1px solid var(--studio-gray-5); + } + + &.is-expanded .foldable-card__content { + border-top: 0; + max-height: fit-content; + } + } + .core-web-vitals-display__details { + border: 0; + } + } + +} + +.core-web-vitals-accordion__header { + display: flex; + align-items: center; + gap: 6px; + width: 100%; +} + +.core-web-vitals-accordion__header-text { + display: flex; + justify-content: space-between; + flex-basis: 100%; + font-weight: 500; +} + diff --git a/client/performance-profiler/components/core-web-vitals-display/core-web-vitals-details.tsx b/client/performance-profiler/components/core-web-vitals-display/core-web-vitals-details.tsx new file mode 100644 index 00000000000000..77cf07b086ccaf --- /dev/null +++ b/client/performance-profiler/components/core-web-vitals-display/core-web-vitals-details.tsx @@ -0,0 +1,168 @@ +import { useTranslate } from 'i18n-calypso'; +import { Metrics, PerformanceMetricsHistory } from 'calypso/data/site-profiler/types'; +import { + metricsNames, + metricsTresholds, + mapThresholdsToStatus, + metricValuations, +} from 'calypso/performance-profiler/utils/metrics'; +import HistoryChart from '../charts/history-chart'; +import { MetricScale } from '../metric-scale'; +import { StatusIndicator } from '../status-indicator'; + +type CoreWebVitalsDetailsProps = Record< Metrics, number > & { + history: PerformanceMetricsHistory; + activeTab: Metrics | null; +}; + +export const CoreWebVitalsDetails: React.FC< CoreWebVitalsDetailsProps > = ( { + activeTab, + history, + ...metrics +} ) => { + const translate = useTranslate(); + + if ( ! activeTab ) { + return null; + } + + const { displayName } = metricsNames[ activeTab ]; + const value = metrics[ activeTab ]; + const valuation = mapThresholdsToStatus( activeTab, value ); + + const { good, needsImprovement } = metricsTresholds[ activeTab ]; + + const formatUnit = ( value: number ) => { + if ( [ 'lcp', 'fcp', 'ttfb' ].includes( activeTab ) ) { + return +( value / 1000 ).toFixed( 2 ); + } + return value; + }; + + const displayUnit = () => { + if ( [ 'lcp', 'fcp', 'ttfb' ].includes( activeTab ) ) { + return translate( 's', { comment: 'Used for displaying a time range in seconds, eg. 1-2s' } ); + } + if ( [ 'inp', 'tbt' ].includes( activeTab ) ) { + return translate( 'ms', { + comment: 'Used for displaying a range in milliseconds, eg. 100-200ms', + } ); + } + return ''; + }; + + let metricsData: number[] = history?.metrics[ activeTab ] ?? []; + let dates = history?.collection_period ?? []; + + // last 8 weeks only + metricsData = metricsData.slice( -8 ); + dates = dates.slice( -8 ); + + // the comparison is inverse here because the last value is the most recent + const positiveTendency = metricsData[ metricsData.length - 1 ] < metricsData[ 0 ]; + + const dataAvailable = metricsData.length > 0 && metricsData.some( ( item ) => item !== null ); + const historicalData = metricsData.map( ( item, index ) => { + let formattedDate: unknown; + const date = dates[ index ]; + if ( 'string' === typeof date ) { + formattedDate = date; + } else { + const { year, month, day } = date; + formattedDate = `${ year }-${ month }-${ day }`; + } + + return { + date: formattedDate, + value: formatUnit( item ), + }; + } ); + + return ( +
    +
    + + { metricValuations[ activeTab ][ valuation ] } + + +
    +
    + +
    +
    { translate( 'Fast' ) }
    +
    + { translate( '0–%(to)s%(unit)s', { + args: { to: formatUnit( good ), unit: displayUnit() }, + comment: 'Displaying a time range, eg. 0-1s', + } ) } +
    +
    +
    +
    + +
    +
    { translate( 'Moderate' ) }
    +
    + { translate( '%(from)s–%(to)s%(unit)s', { + args: { + from: formatUnit( good ), + to: formatUnit( needsImprovement ), + unit: displayUnit(), + }, + comment: 'Displaying a time range, eg. 2-3s', + } ) } +
    +
    +
    +
    + +
    +
    { translate( 'Slow' ) }
    +
    + { translate( '>%(from)s%(unit)s', { + args: { + from: formatUnit( needsImprovement ), + unit: displayUnit(), + }, + comment: 'Displaying a time range, eg. >2s', + } ) } +
    +
    +
    +
    + + { metricValuations[ activeTab ].heading }  + + + { metricValuations[ activeTab ].aka } + +

    + { metricValuations[ activeTab ].explanation } +   + { translate( 'Learn more ↗' ) } +

    +
    +
    + { dataAvailable && ( + + { positiveTendency + ? translate( '%s has improved over the past eight weeks', { + args: [ displayName ], + } ) + : translate( '%s has declined over the past eight weeks', { + args: [ displayName ], + } ) } + + ) } + +
    +
    + ); +}; diff --git a/client/performance-profiler/components/core-web-vitals-display/index.tsx b/client/performance-profiler/components/core-web-vitals-display/index.tsx index 763212995dcd8c..dadd83c44f9c47 100644 --- a/client/performance-profiler/components/core-web-vitals-display/index.tsx +++ b/client/performance-profiler/components/core-web-vitals-display/index.tsx @@ -1,16 +1,10 @@ -import { useTranslate } from 'i18n-calypso'; +import { useDesktopBreakpoint } from '@automattic/viewport-react'; import { useState } from 'react'; import { Metrics, PerformanceMetricsHistory } from 'calypso/data/site-profiler/types'; -import { - metricsNames, - metricsTresholds, - mapThresholdsToStatus, - metricValuations, -} from 'calypso/performance-profiler/utils/metrics'; -import HistoryChart from '../charts/history-chart'; -import { MetricScale } from '../metric-scale'; +import { CoreWebVitalsAccordion } from '../core-web-vitals-accordion'; import { MetricTabBar } from '../metric-tab-bar'; -import { StatusIndicator } from '../status-indicator'; +import { CoreWebVitalsDetails } from './core-web-vitals-details'; + import './style.scss'; type CoreWebVitalsDisplayProps = Record< Metrics, number > & { @@ -18,155 +12,33 @@ type CoreWebVitalsDisplayProps = Record< Metrics, number > & { }; export const CoreWebVitalsDisplay = ( props: CoreWebVitalsDisplayProps ) => { - const translate = useTranslate(); - const [ activeTab, setActiveTab ] = useState< Metrics >( 'fcp' ); - - const { displayName } = metricsNames[ activeTab as keyof typeof metricsNames ]; - const value = props[ activeTab ]; - const valuation = mapThresholdsToStatus( activeTab as keyof typeof metricsTresholds, value ); - - const { good, needsImprovement } = metricsTresholds[ activeTab as keyof typeof metricsTresholds ]; - const formatUnit = ( value: number ) => { - if ( [ 'lcp', 'fcp', 'ttfb' ].includes( activeTab ) ) { - return +( value / 1000 ).toFixed( 2 ); - } - - return value; - }; - - const displayUnit = () => { - if ( [ 'lcp', 'fcp', 'ttfb' ].includes( activeTab ) ) { - return translate( 's', { comment: 'Used for displaying a time range in seconds, eg. 1-2s' } ); - } - - if ( [ 'inp', 'tbt' ].includes( activeTab ) ) { - return translate( 'ms', { - comment: 'Used for displaying a range in milliseconds, eg. 100-200ms', - } ); - } - - return ''; - }; - - const { history } = props; - let metrics: number[] = history?.metrics[ activeTab ] ?? []; - let dates = history?.collection_period ?? []; - - // last 8 weeks only - metrics = metrics.slice( -8 ); - dates = dates.slice( -8 ); - - // the comparison is inverse here because the last value is the most recent - const positiveTendency = metrics[ metrics.length - 1 ] < metrics[ 0 ]; - - const dataAvailable = metrics.length > 0 && metrics.some( ( item ) => item !== null ); - const historicalData = metrics.map( ( item, index ) => { - let formattedDate: unknown; - const date = dates[ index ]; - if ( 'string' === typeof date ) { - formattedDate = date; // this is to ensure compability with reports before https://code.a8c.com/D159137 - } else { - const { year, month, day } = date; - formattedDate = `${ year }-${ month }-${ day }`; - } - - return { - date: formattedDate, - value: formatUnit( item ), - }; - } ); + const defaultTab = 'fcp'; + const [ activeTab, setActiveTab ] = useState< Metrics | null >( defaultTab ); + const isDesktop = useDesktopBreakpoint(); return ( -
    - -
    -
    - - { metricValuations[ activeTab ][ valuation ] } - - -
    -
    - -
    -
    { translate( 'Fast' ) }
    -
    - { translate( '0–%(to)s%(unit)s', { - args: { to: formatUnit( good ), unit: displayUnit() }, - comment: 'Displaying a time range, eg. 0-1s', - } ) } -
    -
    -
    -
    - -
    -
    { translate( 'Moderate' ) }
    -
    - { translate( '%(from)s–%(to)s%(unit)s', { - args: { - from: formatUnit( good ), - to: formatUnit( needsImprovement ), - unit: displayUnit(), - }, - comment: 'Displaying a time range, eg. 2-3s', - } ) } -
    -
    -
    -
    - -
    -
    { translate( 'Slow' ) }
    -
    - { translate( '>%(from)s%(unit)s', { - args: { - from: formatUnit( needsImprovement ), - unit: displayUnit(), - }, - comment: 'Displaying a time range, eg. >2s', - } ) } -
    -
    -
    -
    - - { metricValuations[ activeTab ].heading }  - - - { metricValuations[ activeTab ].aka } - -

    - { metricValuations[ activeTab ].explanation } -   - - { translate( 'Learn more ↗' ) } - -

    -
    -
    - { dataAvailable && ( - - { positiveTendency - ? translate( '%s has improved over the past eight weeks', { - args: [ displayName ], - } ) - : translate( '%s has declined over the past eight weeks', { - args: [ displayName ], - } ) } - - ) } - + { isDesktop && ( +
    + + +
    + ) } + { ! isDesktop && ( +
    + + +
    -
    -
    + ) } + ); }; diff --git a/client/performance-profiler/components/core-web-vitals-display/style.scss b/client/performance-profiler/components/core-web-vitals-display/style.scss index c9ef91affa0c3f..21c21aab8660e8 100644 --- a/client/performance-profiler/components/core-web-vitals-display/style.scss +++ b/client/performance-profiler/components/core-web-vitals-display/style.scss @@ -6,6 +6,7 @@ $blueberry-color: #3858e9; .core-web-vitals-display { display: flex; flex-direction: column; + width: 100%; } .core-web-vitals-display__ranges { @@ -43,15 +44,18 @@ $blueberry-color: #3858e9; border: 1.5px solid var(--studio-gray-5); /* stylelint-disable-next-line scales/radii */ border-radius: 0 0 6px 6px; - border-top: none; padding: 24px; display: flex; flex-direction: row; column-gap: 32px; min-height: 360px; + flex-wrap: wrap; + margin-top: -1.5px; & > div { - flex: 50%; + flex-grow: 1; + flex-basis: 0; + min-width: 300px; } & > p { diff --git a/client/performance-profiler/components/dashboard-content/index.tsx b/client/performance-profiler/components/dashboard-content/index.tsx index 42681524832663..b326a689ccba23 100644 --- a/client/performance-profiler/components/dashboard-content/index.tsx +++ b/client/performance-profiler/components/dashboard-content/index.tsx @@ -13,11 +13,13 @@ import './style.scss'; type PerformanceProfilerDashboardContentProps = { performanceReport: PerformanceReport; url: string; + hash: string; }; export const PerformanceProfilerDashboardContent = ( { performanceReport, url, + hash, }: PerformanceProfilerDashboardContentProps ) => { const { overall_score, fcp, lcp, cls, inp, ttfb, tbt, audits, history, screenshots, is_wpcom } = performanceReport; @@ -41,9 +43,11 @@ export const PerformanceProfilerDashboardContent = ( { tbt={ tbt } history={ history } /> - + - { audits && } + { audits && ( + + ) }
    diff --git a/client/performance-profiler/components/disclaimer-section/style.scss b/client/performance-profiler/components/disclaimer-section/style.scss index dbd0019453d433..6de50c637f4f06 100644 --- a/client/performance-profiler/components/disclaimer-section/style.scss +++ b/client/performance-profiler/components/disclaimer-section/style.scss @@ -1,16 +1,20 @@ .performance-profiler-disclaimer { display: flex; padding: 64px 0; - justify-content: center; + justify-content: space-between; align-items: flex-end; gap: 10px; + flex-wrap: wrap; color: var(--studio-gray-70); font-size: $font-body-extra-small; line-height: $font-title-small; + .content { + max-width: 600px; + } + .link { - width: 470px; flex-shrink: 0; text-align: right; diff --git a/client/performance-profiler/components/header/index.tsx b/client/performance-profiler/components/header/index.tsx index 90d4b0e9b5a79d..931979204c9845 100644 --- a/client/performance-profiler/components/header/index.tsx +++ b/client/performance-profiler/components/header/index.tsx @@ -51,7 +51,9 @@ export const PerformanceProfilerHeader = ( props: HeaderProps ) => {
    - +

    { urlParts.hostname ?? '' }

    @@ -69,7 +71,7 @@ export const PerformanceProfilerHeader = ( props: HeaderProps ) => {
    { showNavigationTabs && ( - + onTabChange( TabType.mobile ) } selected={ activeTab === TabType.mobile } diff --git a/client/performance-profiler/components/header/style.scss b/client/performance-profiler/components/header/style.scss index 1e277322eaf964..751d977ac57fac 100644 --- a/client/performance-profiler/components/header/style.scss +++ b/client/performance-profiler/components/header/style.scss @@ -1,6 +1,12 @@ @import "@wordpress/base-styles/breakpoints"; @import "@automattic/typography/styles/variables"; +.is-group-performance-profiler.is-logged-in { + .profiler-header { + padding-top: 72px; // 40px + 32px (masterbar height) + } +} + .profiler-header { color: #fff; padding-top: 40px; @@ -15,6 +21,22 @@ padding-bottom: 20px; } + .profiler-header__badge { + font-size: $root-font-size; + text-decoration: none; + color: var(--studio-white); + padding: 0; + font-family: $brand-serif; + + &:hover { + color: var(--studio-white); + text-decoration: none; + } + &:focus { + box-shadow: none; + } + } + .profiler-header__site-url { display: flex; flex-direction: row; @@ -80,6 +102,18 @@ box-shadow: none; margin: 0; + .section-nav__mobile-header { + display: none; + } + + .section-nav__panel { + display: flex; + align-items: center; + flex-wrap: nowrap; + overflow-wrap: normal; + justify-content: space-between; + } + .section-nav-tabs__list { // Add padding to display the rounded border and remove the same distance to keep it aligned vertically padding-left: 6px; @@ -115,6 +149,7 @@ padding: 0 16px; align-items: center; gap: 10px; + width: auto; span { font-size: $font-body-small; @@ -168,6 +203,7 @@ align-items: center; gap: 16px; font-size: $font-body-small; + margin-left: 25px; .report-site-details { display: flex; diff --git a/client/performance-profiler/components/insights-section/index.tsx b/client/performance-profiler/components/insights-section/index.tsx index 61ff87fa048e14..17fb63fd0cc5e3 100644 --- a/client/performance-profiler/components/insights-section/index.tsx +++ b/client/performance-profiler/components/insights-section/index.tsx @@ -7,11 +7,12 @@ type InsightsSectionProps = { audits: Record< string, PerformanceMetricsItemQueryResponse >; url: string; isWpcom: boolean; + hash: string; }; export const InsightsSection = ( props: InsightsSectionProps ) => { const translate = useTranslate(); - const { audits, isWpcom } = props; + const { audits, isWpcom, hash } = props; return (
    @@ -26,6 +27,7 @@ export const InsightsSection = ( props: InsightsSectionProps ) => { index={ index } url={ props.url } isWpcom={ isWpcom } + hash={ hash } /> ) ) }
    diff --git a/client/performance-profiler/components/insights-section/style.scss b/client/performance-profiler/components/insights-section/style.scss index 00403671bf1900..fc0a0a59fb7adc 100644 --- a/client/performance-profiler/components/insights-section/style.scss +++ b/client/performance-profiler/components/insights-section/style.scss @@ -65,6 +65,10 @@ $blueberry-color: #3858e9; color: var(--studio-orange-40); } } + + .header-code { + color: #3858e9; + } } &.is-expanded .foldable-card__main { @@ -118,6 +122,40 @@ $blueberry-color: #3858e9; align-items: flex-start; gap: 32px; align-self: stretch; + + p { + line-height: 24px; + } + } + + .generated-with-ia { + font-weight: 500; + } + + .metrics-insight-content { + .survey { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + } + + .options { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + + &.good { + color: var(--studio-green-50); + fill: var(--studio-green-50); + } + + &.bad { + color: var(--studio-red-50); + fill: var(--studio-red-50); + } + } } .metrics-insight-detailed-content { diff --git a/client/performance-profiler/components/llm-message/index.tsx b/client/performance-profiler/components/llm-message/index.tsx new file mode 100644 index 00000000000000..33514192517e82 --- /dev/null +++ b/client/performance-profiler/components/llm-message/index.tsx @@ -0,0 +1,30 @@ +import clsx from 'clsx'; +import { useTranslate } from 'i18n-calypso'; +import { ReactNode } from 'react'; +import IAIcon from 'calypso/assets/images/performance-profiler/ia-icon.svg'; + +import './style.scss'; + +interface LLMMessageProps { + message: string | ReactNode; + secondaryArea?: ReactNode; + rotate?: boolean; +} + +export const LLMMessage = ( { message, rotate, secondaryArea }: LLMMessageProps ) => { + const translate = useTranslate(); + + return ( +
    +
    + { + { message } +
    + { secondaryArea } +
    + ); +}; diff --git a/client/performance-profiler/components/llm-message/style.scss b/client/performance-profiler/components/llm-message/style.scss new file mode 100644 index 00000000000000..9457b135c568c5 --- /dev/null +++ b/client/performance-profiler/components/llm-message/style.scss @@ -0,0 +1,37 @@ +.performance-profiler-llm-message { + box-sizing: border-box; + display: flex; + padding: 16px; + align-items: center; + align-self: stretch; + justify-content: space-between; + background: linear-gradient(0deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.95) 100%), linear-gradient(90deg, #4458e4 0%, #069e08 100%); + /* stylelint-disable-next-line scales/radii */ + border-radius: 6px; + + color: #000; + font-family: "SF Pro Text", $sans; + font-size: $font-body-small; + line-height: 20px; + flex-wrap: wrap; + gap: 10px; + + .content { + display: flex; + align-items: center; + gap: 8px; + } + + .rotate { + animation: rotation 3s infinite linear; + } + + @keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } +} diff --git a/client/performance-profiler/components/message-display/index.tsx b/client/performance-profiler/components/message-display/index.tsx new file mode 100644 index 00000000000000..3b04fb9ee7580e --- /dev/null +++ b/client/performance-profiler/components/message-display/index.tsx @@ -0,0 +1,55 @@ +import { Gridicon } from '@automattic/components'; +import { Button, IconType } from '@wordpress/components'; +import clsx from 'clsx'; +import { ReactNode } from 'react'; +import { Badge } from 'calypso/performance-profiler/components/badge'; + +import './style.scss'; + +type Props = { + title?: string; + message: string | ReactNode; + ctaText?: string; + ctaHref?: string; + secondaryMessage?: string; + displayBadge?: boolean; + ctaIcon?: string; + isErrorMessage?: boolean; +}; + +export const MessageDisplay = ( { + displayBadge = false, + title, + message, + ctaText, + ctaHref, + secondaryMessage, + ctaIcon = '', + isErrorMessage = false, +}: Props ) => { + return ( +
    +
    +
    + { displayBadge && } +
    + { isErrorMessage && } + { title &&

    { title }

    } +

    { message }

    + { ctaText && ctaHref && ( + + ) } +
    + { secondaryMessage &&

    { secondaryMessage }

    } +
    +
    +
    + ); +}; diff --git a/client/performance-profiler/components/message-display/style.scss b/client/performance-profiler/components/message-display/style.scss new file mode 100644 index 00000000000000..2c714d930c4ba6 --- /dev/null +++ b/client/performance-profiler/components/message-display/style.scss @@ -0,0 +1,105 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@automattic/typography/styles/variables"; + +$blueberry-color: #3858e9; + +.message-display { + color: #fff; + padding-top: 40px; + background: linear-gradient(180deg, var(--studio-gray-100) 25.44%, rgba(16, 21, 23, 0) 100%), url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iOCIgY3k9IjgiIHI9IjEiIGZpbGw9IndoaXRlIiBmaWxsLW9wYWNpdHk9IjAuMjUiLz4KPC9zdmc+Cg==) repeat, var(--studio-gray-100); + + a { + color: $blueberry-color; + + &:hover { + color: darken($blueberry-color, 10%); + text-decoration: underline; + } + + &.is-primary { + color: #fff; + background-color: $blueberry-color; + border-radius: 4px; + + &:hover:not(:disabled), + &:active:not(:disabled), + &:focus:not(:disabled) { + background-color: darken($blueberry-color, 10%); + border-color: darken($blueberry-color, 10%); + box-shadow: none; + } + } + } + + p { + margin: 0; + } + + .l-block-wrapper { + height: 100%; + max-width: 1056px; + } + + .message-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 64px; + min-height: 700px; + width: 50%; + + .main-message { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 16px; + + &.error { + border-radius: 4px; + border: 1px solid var(--studio-red-70); + background-color: var(--studio-red-70); + padding: 16px; + padding-left: 52px; + position: relative; + + .gridicon { + position: absolute; + top: 16px; + left: 16px; + } + + .cta-button { + margin: 0; + } + } + + .title { + font-family: $brand-serif; + font-size: $font-headline-medium; + line-height: $font-headline-large; + color: #fff; + } + + .message { + font-size: $font-body; + font-weight: 500; + line-height: $font-title-medium; + } + + .cta-button { + margin-top: 16px; + font-size: $font-body-small; + line-height: $font-title-small; + padding: 20px; + } + } + + .secondary-message { + font-size: $font-body-small; + line-height: $font-title-small; + color: var(--studio-gray-20); + } + } +} diff --git a/client/performance-profiler/components/metric-tab-bar/style.scss b/client/performance-profiler/components/metric-tab-bar/style.scss index 6ba4b44da50760..540517d439f3d7 100644 --- a/client/performance-profiler/components/metric-tab-bar/style.scss +++ b/client/performance-profiler/components/metric-tab-bar/style.scss @@ -13,8 +13,8 @@ $blueberry-color: #3858e9; display: flex; gap: 6px; padding: 12px 16px; + background: var(--studio-white); border: 1.5px solid var(--studio-white); - border-bottom: 1.5px solid var(--studio-gray-5); text-align: initial; flex-grow: 1; diff --git a/client/performance-profiler/components/metrics-insight/index.tsx b/client/performance-profiler/components/metrics-insight/index.tsx index 737dee0341104d..6ba2e8fbc0e927 100644 --- a/client/performance-profiler/components/metrics-insight/index.tsx +++ b/client/performance-profiler/components/metrics-insight/index.tsx @@ -16,6 +16,7 @@ interface MetricsInsightProps { index: number; url?: string; isWpcom: boolean; + hash: string; } const Card = styled( FoldableCard )` @@ -53,21 +54,25 @@ const Header = styled.div` font-size: 16px; font-weight: 500; margin-right: 8px; + width: 15px; + text-align: right; } `; const Content = styled.div` - padding: 24px; + padding: 15px 22px; `; export const MetricsInsight: React.FC< MetricsInsightProps > = ( props ) => { const translate = useTranslate(); - const { insight, onClick, index, isWpcom } = props; + const { insight, onClick, index, isWpcom, hash } = props; const [ retrieveInsight, setRetrieveInsight ] = useState( false ); const { data: llmAnswer, isLoading: isLoadingLlmAnswer } = useSupportChatLLMQuery( insight.description ?? '', + hash, + isWpcom, isEnabled( 'performance-profiler/llm' ) && retrieveInsight ); const tip = tips[ insight.id ]; @@ -103,6 +108,7 @@ export const MetricsInsight: React.FC< MetricsInsightProps > = ( props ) => { } } secondaryArea={ tip && } isLoading={ isEnabled( 'performance-profiler/llm' ) && isLoadingLlmAnswer } + IAGenerated={ isEnabled( 'performance-profiler/llm' ) } /> diff --git a/client/performance-profiler/components/metrics-insight/insight-content.tsx b/client/performance-profiler/components/metrics-insight/insight-content.tsx index 2261deed506336..756f89a4bf8536 100644 --- a/client/performance-profiler/components/metrics-insight/insight-content.tsx +++ b/client/performance-profiler/components/metrics-insight/insight-content.tsx @@ -1,23 +1,37 @@ import { useTranslate } from 'i18n-calypso'; +import { useState } from 'react'; import Markdown from 'react-markdown'; import { PerformanceMetricsItemQueryResponse } from 'calypso/data/site-profiler/types'; +import { recordTracksEvent } from 'calypso/lib/analytics/tracks'; +import { LLMMessage } from 'calypso/performance-profiler/components/llm-message'; +import { ThumbsUpIcon, ThumbsDownIcon } from 'calypso/performance-profiler/icons/thumbs'; import { InsightDetailedContent } from './insight-detailed-content'; interface InsightContentProps { data: PerformanceMetricsItemQueryResponse; secondaryArea?: React.ReactNode; isLoading?: boolean; + IAGenerated: boolean; } export const InsightContent: React.FC< InsightContentProps > = ( props ) => { const translate = useTranslate(); - const { data, isLoading } = props; + const { data, isLoading, IAGenerated } = props; const { description = '' } = data ?? {}; + const [ feedbackSent, setFeedbackSent ] = useState( false ); + const onSurveyClick = ( rating: string ) => { + recordTracksEvent( 'calypso_performance_profiler_llm_survey_click', { + rating, + description, + } ); + + setFeedbackSent( true ); + }; return (
    { isLoading ? ( - translate( 'Looking for the best solution…' ) + ) : ( <>
    @@ -34,6 +48,47 @@ export const InsightContent: React.FC< InsightContentProps > = ( props ) => {
    { props.secondaryArea }
    + + { IAGenerated && ( + { translate( 'Generated with IA' ) } + } + secondaryArea={ +
    + { feedbackSent ? ( + translate( 'Thanks for the feedback!' ) + ) : ( + <> + { translate( 'How did we do?' ) } +
    onSurveyClick( 'good' ) } + onKeyUp={ () => onSurveyClick( 'good' ) } + role="button" + tabIndex={ 0 } + > + + + { translate( "Good, it's helpful" ) } +
    +
    onSurveyClick( 'bad' ) } + onKeyUp={ () => onSurveyClick( 'bad' ) } + role="button" + tabIndex={ 0 } + > + + { translate( 'Not helpful' ) } +
    + + ) } +
    + } + /> + ) } + { data.details?.type && (
    diff --git a/client/performance-profiler/components/metrics-insight/insight-header.tsx b/client/performance-profiler/components/metrics-insight/insight-header.tsx index 899831aade18d9..37941d36e1131b 100644 --- a/client/performance-profiler/components/metrics-insight/insight-header.tsx +++ b/client/performance-profiler/components/metrics-insight/insight-header.tsx @@ -23,7 +23,7 @@ export const InsightHeader: React.FC< InsightHeaderProps > = ( props ) => { return

    { props.children }

    ; }, code( props ) { - return { props.children }; + return { props.children }; }, } } > diff --git a/client/performance-profiler/components/metrics-insight/insight-table.tsx b/client/performance-profiler/components/metrics-insight/insight-table.tsx index 857234bfe341f8..3332afa3015a9f 100644 --- a/client/performance-profiler/components/metrics-insight/insight-table.tsx +++ b/client/performance-profiler/components/metrics-insight/insight-table.tsx @@ -73,9 +73,7 @@ function Cell( { return (

    { data?.nodeLabel }

    -
    -							{ data?.snippet }
    -						
    + { data?.snippet }
    ); diff --git a/client/performance-profiler/components/migration-banner/index.tsx b/client/performance-profiler/components/migration-banner/index.tsx index 680a5af80c1c91..1f76cf772654d8 100644 --- a/client/performance-profiler/components/migration-banner/index.tsx +++ b/client/performance-profiler/components/migration-banner/index.tsx @@ -1,4 +1,5 @@ import { Gridicon } from '@automattic/components'; +import { useDesktopBreakpoint } from '@automattic/viewport-react'; import { Button } from '@wordpress/components'; import { useTranslate } from 'i18n-calypso'; import MigrationBannerImg from 'calypso/assets/images/performance-profiler/migration-banner-img.png'; @@ -8,6 +9,7 @@ import './style.scss'; export const MigrationBanner = ( props: { url: string } ) => { const translate = useTranslate(); + const isDesktop = useDesktopBreakpoint(); return (
    @@ -62,15 +64,17 @@ export const MigrationBanner = ( props: { url: string } ) => {
    -
    - { -
    + { isDesktop && ( +
    + { +
    + ) }

    { translate( 'Trusted by 160 million worldwide' ) }

    diff --git a/client/performance-profiler/components/newsletter-banner.tsx b/client/performance-profiler/components/newsletter-banner.tsx index 5bd635ccf77c1b..c4dd01d6ec5afb 100644 --- a/client/performance-profiler/components/newsletter-banner.tsx +++ b/client/performance-profiler/components/newsletter-banner.tsx @@ -1,18 +1,30 @@ import styled from '@emotion/styled'; import { Button } from '@wordpress/components'; import { useTranslate } from 'i18n-calypso'; +import { useSelector } from 'calypso/state'; +import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; const Container = styled.div` - background-color: #f7f8fe; + background: + linear-gradient( 90deg, var( --studio-gray-100 ) 50%, rgba( 16, 21, 23, 0 ) 100% ), + fixed 10px 10px /16px 16px radial-gradient( var( --studio-gray-50 ) 1px, transparent 0 ), + var( --studio-gray-100 ); + border-radius: 6px; display: flex; flex-direction: row; align-items: center; - gap: 24px; + gap: 10px; justify-content: space-between; width: 100%; + box-sizing: border-box; + flex-wrap: wrap; + padding: 24px; - & > * { - margin: 24px; + @media ( max-width: 600px ) { + background: + linear-gradient( 180deg, var( --studio-gray-100 ) 50%, rgba( 16, 21, 23, 0 ) 100% ), + fixed 10px 10px /16px 16px radial-gradient( var( --studio-gray-50 ) 1px, transparent 0 ), + var( --studio-gray-100 ); } `; @@ -20,10 +32,12 @@ const Heading = styled.div` font-weight: 500; line-height: 24px; text-align: left; + color: var( --studio-white ); `; const Body = styled.div` - color: var( --studio-gray-70 ); + color: var( --studio-gray-20 ); + text-wrap: balance; `; const BlueberryButton = styled( Button )` @@ -31,25 +45,42 @@ const BlueberryButton = styled( Button )` && { background: #3858e9; border-color: #3858e9; + + &:hover:not( :disabled ), + &:active:not( :disabled ), + &:focus:not( :disabled ) { + background-color: darken( #3858e9, 10% ); + border-color: darken( #3858e9, 10% ); + box-shadow: none; + } } `; -export const NewsletterBanner = () => { +export const NewsletterBanner = ( { link }: { link: string } ) => { const translate = useTranslate(); + const isLoggedIn = useSelector( isUserLoggedIn ); return (
    - { translate( 'Sign up for weekly performance reports—it’s free!' ) } + + { translate( 'Get notified about changes to your site’s performance—it’s free!' ) } + { translate( - 'Monitor your site’s key performance metrics with a free report delivered to your inbox each week.' + "Monitor your site's key performance metrics with a free report delivered to your inbox each week." ) } - { translate( 'All you need is a free WordPress.com account to get started.' ) } + { ! isLoggedIn && ( + + { translate( 'All you need is a free WordPress.com account to get started.' ) } + + ) }
    - - { translate( 'Sign up for email reports' ) } + + { isLoggedIn + ? translate( 'Enable email alerts' ) + : translate( 'Sign up for email reports' ) }
    ); diff --git a/client/performance-profiler/components/screenshot-timeline.tsx b/client/performance-profiler/components/screenshot-timeline.tsx index 5114c8b53f63d3..3070e594d90137 100644 --- a/client/performance-profiler/components/screenshot-timeline.tsx +++ b/client/performance-profiler/components/screenshot-timeline.tsx @@ -2,11 +2,17 @@ import styled from '@emotion/styled'; import { translate } from 'i18n-calypso'; import { ScreenShotsTimeLine } from 'calypso/data/site-profiler/types'; +const Container = styled.div` + max-width: 100%; +`; + const Timeline = styled.div` display: flex; flex-direction: row; gap: 1.5rem; text-align: center; + overflow: auto; + padding: 0 2px; `; const H2 = styled.h2` @@ -17,6 +23,8 @@ const H2 = styled.h2` const Thumbnail = styled.img` border: 1px solid var( --studio-gray-0 ); border-radius: 6px; + width: 100%; + min-width: 60px; `; type Props = { screenshots: ScreenShotsTimeLine[] }; @@ -27,9 +35,9 @@ export const ScreenshotTimeline = ( { screenshots }: Props ) => { } return ( -
    -

    Timeline

    -

    { translate( 'Screenshots of your site loading taken while loading the page.' ) }

    + +

    { translate( 'Timeline' ) }

    +

    { translate( 'How your site appears to users while loading.' ) }

    { screenshots.map( ( screenshot, index ) => { const timing = `${ ( screenshot.timing / 1000 ).toFixed( 1 ) }s`; @@ -41,6 +49,6 @@ export const ScreenshotTimeline = ( { screenshots }: Props ) => { ); } ) } -
    + ); }; diff --git a/client/performance-profiler/components/tip/style.scss b/client/performance-profiler/components/tip/style.scss index 020cba5dcde367..87f047f8ca2eb3 100644 --- a/client/performance-profiler/components/tip/style.scss +++ b/client/performance-profiler/components/tip/style.scss @@ -1,8 +1,10 @@ .performance-profiler-tip { max-width: 460px; - background-color: #e7f0fa; + background-color: #f7f8fe; padding: 25px; min-width: 300px; + /* stylelint-disable-next-line scales/radii */ + border-radius: 6px; h4 { font-size: $font-body-small; diff --git a/client/performance-profiler/controller.tsx b/client/performance-profiler/controller.tsx index e9378d65770cd6..fd3e5622ec5c00 100644 --- a/client/performance-profiler/controller.tsx +++ b/client/performance-profiler/controller.tsx @@ -7,6 +7,7 @@ import Main from 'calypso/components/main'; import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; import { TabType } from './components/header'; import { PerformanceProfilerDashboard } from './pages/dashboard'; +import { WeeklyReport } from './pages/weekly-report'; export function PerformanceProfilerDashboardContext( context: Context, next: () => void ): void { const isLoggedIn = isUserLoggedIn( context.store.getState() ); @@ -41,6 +42,36 @@ export function PerformanceProfilerDashboardContext( context: Context, next: () next(); } +export function WeeklyReportContext( context: Context, next: () => void ): void { + const isLoggedIn = isUserLoggedIn( context.store.getState() ); + + if ( ! config.isEnabled( 'performance-profiler' ) ) { + page.redirect( '/' ); + return; + } + + if ( ! isLoggedIn ) { + window.location.href = '/log-in?redirect_to=' + encodeURIComponent( context.path ); + return; + } + + const url = context.query?.url?.startsWith( 'http' ) + ? context.query.url + : `https://${ context.query?.url ?? '' }`; + + context.primary = ( + <> +
    + +
    + + + + ); + + next(); +} + export const notFound = ( context: Context, next: () => void ) => { context.primary = ( { +export const useSupportChatLLMQuery = ( + description: string, + hash: string, + is_wpcom: boolean, + enable: boolean +) => { const question = `I need to fix the following issue to improve the performance of site: ${ description }.`; const howToAnswer = 'Answer me in two topics in bold: "Why is this important?" and "How to fix this?"'; @@ -13,14 +18,14 @@ export const useSupportChatLLMQuery = ( description: string, enable: boolean ) = return useQuery( { // eslint-disable-next-line @tanstack/query/exhaustive-deps - queryKey: [ 'support', 'chat', description ], + queryKey: [ 'support', 'chat', description, is_wpcom ], queryFn: () => wp.req.post( { - path: '/odie/chat/wpcom-support-chat/', + path: `/odie/assistant/performance-profiler?hash=${ hash }`, apiNamespace: 'wpcom/v2', }, - { message } + { message, is_wpcom } ), meta: { persist: false, diff --git a/client/performance-profiler/icons/thumbs.tsx b/client/performance-profiler/icons/thumbs.tsx new file mode 100644 index 00000000000000..123339a05dfd2e --- /dev/null +++ b/client/performance-profiler/icons/thumbs.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +export const ThumbsUpIcon = ( { className }: { className?: string } ) => ( + + + +); + +export const ThumbsDownIcon = ( { className }: { className?: string } ) => ( + + + +); diff --git a/client/performance-profiler/index.web.ts b/client/performance-profiler/index.web.ts index 05deecca75f4be..35f356daa045ea 100644 --- a/client/performance-profiler/index.web.ts +++ b/client/performance-profiler/index.web.ts @@ -1,8 +1,9 @@ import page from '@automattic/calypso-router'; import { makeLayout, render as clientRender } from 'calypso/controller/index.web'; -import { PerformanceProfilerDashboardContext, notFound } from './controller'; +import { PerformanceProfilerDashboardContext, WeeklyReportContext, notFound } from './controller'; export default function () { page( '/speed-test-tool/', PerformanceProfilerDashboardContext, makeLayout, clientRender ); + page( '/speed-test-tool/weekly-report', WeeklyReportContext, makeLayout, clientRender ); page( '/speed-test-tool*', notFound, makeLayout, clientRender ); } diff --git a/client/performance-profiler/pages/dashboard/index.tsx b/client/performance-profiler/pages/dashboard/index.tsx index 199ae8f9387ae4..d8e2b1153c1e0a 100644 --- a/client/performance-profiler/pages/dashboard/index.tsx +++ b/client/performance-profiler/pages/dashboard/index.tsx @@ -101,6 +101,7 @@ export const PerformanceProfilerDashboard = ( props: PerformanceProfilerDashboar ) }
    diff --git a/client/performance-profiler/pages/weekly-report/index.tsx b/client/performance-profiler/pages/weekly-report/index.tsx new file mode 100644 index 00000000000000..aa6583c802444b --- /dev/null +++ b/client/performance-profiler/pages/weekly-report/index.tsx @@ -0,0 +1,141 @@ +import styled from '@emotion/styled'; +import { useTranslate } from 'i18n-calypso'; +import { useEffect } from 'react'; +import DocumentHead from 'calypso/components/data/document-head'; +import { useLeadMutation } from 'calypso/data/site-profiler/use-lead-query'; +import { MessageDisplay } from 'calypso/performance-profiler/components/message-display'; + +type WeeklyReportProps = { + url: string; + hash: string; +}; + +const LoaderText = styled.span` + display: flex; + align-items: center; + font-size: 16px; + font-weight: 400; + line-height: 24px; + position: relative; + + &:before { + content: ''; + display: inline-block; + border-radius: 50%; + margin-right: 10px; + content: ''; + width: 16px; + height: 16px; + border: solid 2px #074ee8; + border-radius: 50%; + border-bottom-color: transparent; + -webkit-transition: all 0.5s ease-in; + -webkit-animation-name: rotate; + -webkit-animation-duration: 1s; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + + transition: all 0.5s ease-in; + animation-name: rotate; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + + @keyframes rotate { + from { + transform: rotate( 0deg ); + } + to { + transform: rotate( 360deg ); + } + } + + @-webkit-keyframes rotate { + from { + -webkit-transform: rotate( 0deg ); + } + to { + -webkit-transform: rotate( 360deg ); + } + } +`; + +const ErrorSecondLine = styled.span` + color: var( --studio-red-5 ); + font-weight: 400; + line-height: 20px; +`; + +export const WeeklyReport = ( props: WeeklyReportProps ) => { + const translate = useTranslate(); + const { url, hash } = props; + + const siteUrl = new URL( url ); + + const { mutate, isPending, isError, isSuccess } = useLeadMutation( url, hash ); + + useEffect( () => { + mutate(); + }, [ mutate ] ); + + const secondaryMessage = translate( + 'You can stop receiving performance reports at any time by clicking the Unsubscribe link in the email footer.' + ); + + return ( +
    + + + { isPending && ( + + { translate( 'Enabling email reports for %s', { + args: [ siteUrl.host ], + } ) } + + } + secondaryMessage={ secondaryMessage } + /> + ) } + { isError && ( + + { translate( 'Email reports could not be enabled for %s', { + args: [ siteUrl.host ], + } ) } +
    + + { translate( + 'Please try again or contact support if you continue to experience problems.' + ) } + + + } + ctaText={ translate( 'Enable email reports' ) } + ctaHref={ `/speed-test-tool/weekly-report?url=${ url }&hash=${ hash }` } + secondaryMessage={ secondaryMessage } + /> + ) } + { isSuccess && ( + } } + ) } + ctaText={ translate( '← Back to speed test' ) } + ctaHref="/speed-test" + ctaIcon="arrow-left" + secondaryMessage={ secondaryMessage } + /> + ) } +
    + ); +}; diff --git a/client/reader/tags/controller.tsx b/client/reader/tags/controller.tsx index 7fdbcd6e1df33b..6721d0ceec07a4 100644 --- a/client/reader/tags/controller.tsx +++ b/client/reader/tags/controller.tsx @@ -94,7 +94,7 @@ export const fetchAlphabeticTags = ( context: PageJSContext, next: ( e?: Error ) } performanceMark( context as PartialContext, 'fetchAlphabeticTags' ); - const currentUserLocale = getCurrentUserLocale( context.store.getState() ); + const currentUserLocale = getCurrentUserLocale( context.store.getState() ) || context.lang; context.queryClient .fetchQuery( { @@ -102,7 +102,7 @@ export const fetchAlphabeticTags = ( context: PageJSContext, next: ( e?: Error ) queryFn: () => { return wpcom.req.get( '/read/tags/alphabetic', { apiVersion: '1.2', - lang: currentUserLocale, // Note: undefined will be omitted by the query string builder. + locale: currentUserLocale, // Note: undefined will be omitted by the query string builder. } ); }, staleTime: 86400000, // 24 hours diff --git a/client/sections.js b/client/sections.js index 275a171dd61a60..c2c771e588061e 100644 --- a/client/sections.js +++ b/client/sections.js @@ -252,15 +252,6 @@ const sections = [ enableLoggedOut: true, isomorphic: true, }, - { - name: 'start-with', - paths: [ '/start-with' ], - module: 'calypso/start-with', - enableLoggedOut: true, - group: 'start-with', - isomorphic: true, - trackLoadPerformance: true, - }, { name: 'jetpack-app', paths: [ '/jetpack-app' ], diff --git a/client/signup/steps/domains/index.jsx b/client/signup/steps/domains/index.jsx index 5351057ffca6c8..668b16d63e23a4 100644 --- a/client/signup/steps/domains/index.jsx +++ b/client/signup/steps/domains/index.jsx @@ -16,12 +16,14 @@ import PropTypes from 'prop-types'; import { parse } from 'qs'; import { Component } from 'react'; import { connect } from 'react-redux'; +import AsyncLoad from 'calypso/components/async-load'; import QueryProductsList from 'calypso/components/data/query-products-list'; import { useMyDomainInputMode as inputMode } from 'calypso/components/domains/connect-domain-step/constants'; import RegisterDomainStep from 'calypso/components/domains/register-domain-step'; import { recordUseYourDomainButtonClick } from 'calypso/components/domains/register-domain-step/analytics'; import ReskinSideExplainer from 'calypso/components/domains/reskin-side-explainer'; import UseMyDomain from 'calypso/components/domains/use-my-domain'; +import FormattedHeader from 'calypso/components/formatted-header'; import Notice from 'calypso/components/notice'; import { SIGNUP_DOMAIN_ORIGIN } from 'calypso/lib/analytics/signup'; import { @@ -45,7 +47,6 @@ import { getSitePropertyDefaults } from 'calypso/lib/signup/site-properties'; import CalypsoShoppingCartProvider from 'calypso/my-sites/checkout/calypso-shopping-cart-provider'; import withCartKey from 'calypso/my-sites/checkout/with-cart-key'; import { domainManagementRoot } from 'calypso/my-sites/domains/paths'; -import StepWrapper from 'calypso/signup/step-wrapper'; import { getStepUrl, isPlanSelectionAvailableLaterInFlow, @@ -359,8 +360,14 @@ export class RenderDomainsStep extends Component { }; handleUseYourDomainClick = () => { - page( this.getUseYourDomainUrl() ); + // Stepper doesn't support page.js + const navigate = this.props.page || page; this.props.recordUseYourDomainButtonClick( this.getAnalyticsSection() ); + if ( this.props.useStepperWrapper ) { + this.props.goToNextStep( { navigateToUseMyDomain: true } ); + } else { + navigate( this.getUseYourDomainUrl() ); + } }; handleDomainToDomainCart = async ( previousState ) => { @@ -1329,6 +1336,7 @@ export class RenderDomainsStep extends Component { isReskinned, userSiteCount, previousStepName, + useStepperWrapper, } = this.props; const siteUrl = this.props.selectedSite?.URL; const siteSlug = this.props.queryObject?.siteSlug; @@ -1337,8 +1345,17 @@ export class RenderDomainsStep extends Component { let backLabelText; let isExternalBackUrl = false; - // Hide "Back" button in domains step if the user has no sites. - const shouldHideBack = ! userSiteCount && previousStepName?.startsWith( 'user' ); + /** + * Hide "Back" button in domains step if: + * 1. The user has no sites + * 2. This step was rendered immediately after account creation + * 3. The user is on the root domains step and not a child step section like use-your-domain + */ + const shouldHideBack = + ! userSiteCount && + previousStepName?.startsWith( 'user' ) && + stepSectionName !== 'use-your-domain'; + const hideBack = flowName === 'domain' || shouldHideBack; const previousStepBackUrl = this.getPreviousStepUrl(); @@ -1399,8 +1416,42 @@ export class RenderDomainsStep extends Component { const headerText = this.getHeaderText(); const fallbackSubHeaderText = this.getSubHeaderText(); + if ( useStepperWrapper ) { + return ( + + + { this.renderContent() } +
    + } + formattedHeader={ + + } + backLabelText={ backLabelText } + hideSkip + align="center" + isWideLayout + /> + ); + } + return ( - { +export const submitDomainStepSelection = ( suggestion, section ) => { let domainType = 'domain_reg'; if ( suggestion.is_free ) { domainType = 'wpcom_subdomain'; @@ -1462,7 +1513,7 @@ const submitDomainStepSelection = ( suggestion, section ) => { }; const RenderDomainsStepConnect = connect( - ( state, { steps, flowName, stepName } ) => { + ( state, { steps, flowName, stepName, previousStepName } ) => { const productsList = getAvailableProductsList( state ); const productsLoaded = ! isEmpty( productsList ); const isPlanStepSkipped = isPlanStepExistsAndSkipped( state ); @@ -1483,7 +1534,7 @@ const RenderDomainsStepConnect = connect( [ 'pro', 'starter' ].includes( flowName ), userLoggedIn, multiDomainDefaultPlan, - previousStepName: getPreviousStepName( flowName, stepName, userLoggedIn ), + previousStepName: previousStepName || getPreviousStepName( flowName, stepName, userLoggedIn ), }; }, { diff --git a/client/signup/steps/domains/style.scss b/client/signup/steps/domains/style.scss index 0c46b7d87cb5f2..67f5f2b7fc8d35 100644 --- a/client/signup/steps/domains/style.scss +++ b/client/signup/steps/domains/style.scss @@ -15,6 +15,7 @@ } } +body.is-section-stepper .domains__step-content, .is-section-signup .domains__step-content { margin-bottom: 50px; @@ -53,7 +54,6 @@ } .search-component.is-open.has-focus { - border: none; border-radius: 2px; box-shadow: 0 0 0 3px var(--color-accent-light); } @@ -236,6 +236,7 @@ * Styles for design reskin * The `is-white-signup` class is attached to the body when the user is assigned the `reskinned` group of the `reskinSignupFlow` a/b test */ +body.is-section-stepper.is-group-stepper, body.is-section-signup.is-white-signup { $light-white: #f3f4f5; @@ -456,6 +457,10 @@ body.is-section-signup.is-white-signup { .signup__step.is-mailbox-domain { padding: 0 12px; } + + .step-container__content .domains__step-content-domain-step { + padding: 0 12px; + } } // blue signup flow-specific styles diff --git a/client/signup/style.scss b/client/signup/style.scss index 742a23c5709add..ddeb6b7c00a1d3 100644 --- a/client/signup/style.scss +++ b/client/signup/style.scss @@ -368,8 +368,8 @@ body.is-section-signup .layout:not(.dops):not(.is-wccom-oauth-flow) .formatted-h } .social-buttons__button { - border: 1px solid var(--studio-pink-50); - color: var(--studio-pink-50); + border: 1px solid var(--studio-wordpress-blue); + color: var(--studio-wordpress-blue); box-shadow: none; max-width: 250px; height: 48px; @@ -393,6 +393,7 @@ body.is-section-signup .layout:not(.dops):not(.is-wccom-oauth-flow) .formatted-h /** * Common styles for reskinSignupFlow a/b test */ +body.is-section-stepper, body.is-section-signup.is-white-signup .layout:not(.dops):not(.is-wccom-oauth-flow) { $gray-100: #101517; $gray-60: #50575e; @@ -433,14 +434,6 @@ body.is-section-signup.is-white-signup .layout:not(.dops):not(.is-wccom-oauth-fl } } - .navigation-link.button.is-borderless { - color: var(--color-accent); - - svg { - fill: var(--color-accent); - } - } - .step-wrapper__navigation { .navigation-link.button.is-borderless { color: $gray-100; @@ -460,7 +453,11 @@ body.is-section-signup.is-white-signup .layout:not(.dops):not(.is-wccom-oauth-fl } } - .signup:not(.is-onboarding-2023-pricing-grid) .step-wrapper:not(.is-horizontal-layout) { + .signup:not(.is-onboarding-2023-pricing-grid) .step-wrapper:not(.is-horizontal-layout), + // Stepper's header. + .step-container:not(.is-horizontal-layout) { + // Stepper's header. + .step-container__header, .step-wrapper__header { margin: 24px 20px; diff --git a/client/signup/utils.js b/client/signup/utils.js index ff4372bec857fc..0716f96e7f189d 100644 --- a/client/signup/utils.js +++ b/client/signup/utils.js @@ -47,12 +47,16 @@ export function getStepUrl( flowName, stepName, stepSectionName, localeSlug, par const step = stepName ? `/${ stepName }` : ''; const section = stepSectionName ? `/${ stepSectionName }` : ''; const locale = localeSlug ? `/${ localeSlug }` : ''; + const framework = + typeof window !== 'undefined' && window.location.pathname.startsWith( '/setup' ) + ? '/setup' + : '/start'; const url = - flowName === defaultFlowName - ? // we don't include the default flow name in the route - '/start' + step + section + locale - : '/start' + flow + step + section + locale; + flowName === defaultFlowName && framework === '/start' + ? // we don't include the default flow name in the route in /start + framework + step + section + locale + : framework + flow + step + section + locale; return addQueryArgs( params, url ); } diff --git a/client/site-logs/components/logs-header/index.tsx b/client/site-logs/components/logs-header/index.tsx index 6dc12718cd8bab..1922754d807999 100644 --- a/client/site-logs/components/logs-header/index.tsx +++ b/client/site-logs/components/logs-header/index.tsx @@ -37,7 +37,7 @@ export function LogsHeader( { logType }: { logType: string } ) { />
    { translate( 'Log type' ) }
    - + { options.map( ( option ) => { return ( { - recordTracksEvent( PLAN_RENEW_NAG_EVENT_NAMES.IN_VIEW, { - is_site_owner: isSiteOwner, - product_slug: productSlug, - display_mode: 'grid', - } ); - }, [ isSiteOwner, productSlug ] ); - - const { ref } = useInView( { - onChange: ( inView ) => inView && trackCallback(), - } ); - - return ( - - - - { - /* translators: %s - the plan's product name, such as Creator or Explorer. */ - sprintf( __( '%s Plan expired.' ), site.plan?.product_name_short ) - } - - { isSiteOwner && ( - { - recordTracksEvent( PLAN_RENEW_NAG_EVENT_NAMES.ON_CLICK, { - product_slug: productSlug, - display_mode: 'grid', - } ); - } } - > - { isUpgradeable ? __( 'Upgrade' ) : __( 'Renew' ) } - - ) } - - - ); -} diff --git a/client/sites-dashboard/components/sites-grid-item-select/README.md b/client/sites-dashboard/components/sites-grid-item-select/README.md deleted file mode 100644 index 890adb4f0d3f1b..00000000000000 --- a/client/sites-dashboard/components/sites-grid-item-select/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# SitesGridItem - -Renders a SitesGridItem component with site selection option. - -## How to use - -```jsx -import SitesGridItem from 'calypso/sites-dashboard/components/sites-grid-item'; -import type { SiteExcerptData } from '@automattic/sites'; - -function render() { - const site = {}; - return ( -
    - {} } // optional - > -
    - ); -} -``` - -## Props - -- `site`: a site data e.g. SiteExcerptData object. -- `key`: unique key eg. Site ID. -- `showLaunchNag`: boolean, optional, default: true. -- `showBadgeSection`: boolean, optional, default: true. -- `showThumbnailLink`: boolean, optional, default: true. -- `showSiteRenewLink`: boolean, optional, default: true. -- `onSiteSelectBtnClick`: function, optional, default: undefined. diff --git a/client/sites-dashboard/components/sites-grid-item-select/docs/example.jsx b/client/sites-dashboard/components/sites-grid-item-select/docs/example.jsx deleted file mode 100644 index 80a035939f38c1..00000000000000 --- a/client/sites-dashboard/components/sites-grid-item-select/docs/example.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { css } from '@emotion/css'; -import clsx from 'clsx'; -import { MEDIA_QUERIES } from 'calypso/sites-dashboard/utils'; -import sampleSiteData from '../../docs/sample-site-data'; -import { SitesGridItem } from '../../sites-grid-item'; - -const container = css( { - display: 'grid', - gap: '32px', - - gridTemplateColumns: '1fr', - - [ MEDIA_QUERIES.mediumOrLarger ]: { - gridTemplateColumns: 'repeat(2, 1fr)', - }, - - [ MEDIA_QUERIES.large ]: { - gridTemplateColumns: 'repeat(3, 1fr)', - }, -} ); - -const className = css( { - marginBlockStart: 0, - marginInline: 0, - marginBlockEnd: '1.5em', -} ); - -const itemClassName = css( { - minWidth: 0, -} ); - -const SitesGridItemSelectExample = () => { - return ( -
    - { sampleSiteData.map( ( site ) => ( -
    - { - // console.log( _site ); - } } - /> -
    - ) ) } -
    - ); -}; - -SitesGridItemSelectExample.displayName = 'SitesGridItemSelect'; - -export default SitesGridItemSelectExample; diff --git a/client/sites-dashboard/components/sites-grid-item.tsx b/client/sites-dashboard/components/sites-grid-item.tsx index 662b7ac74ae80e..f6650756bbe021 100644 --- a/client/sites-dashboard/components/sites-grid-item.tsx +++ b/client/sites-dashboard/components/sites-grid-item.tsx @@ -1,32 +1,15 @@ import { Button } from '@automattic/components'; -import { useSiteLaunchStatusLabel, getSiteLaunchStatus } from '@automattic/sites'; import { css } from '@emotion/css'; import styled from '@emotion/styled'; import { useI18n } from '@wordpress/react-i18n'; import { memo } from 'react'; import { useInView } from 'react-intersection-observer'; -import SitesMigrationTrialBadge from 'calypso/sites-dashboard/components/sites-migration-trial-badge'; -import { useSelector } from 'calypso/state'; -import { isTrialSite } from 'calypso/state/sites/plans/selectors'; -import { - displaySiteUrl, - getDashboardUrl, - isStagingSite, - siteDefaultInterface, - getSiteWpAdminUrl, -} from '../utils'; -import { SitesEllipsisMenu } from './sites-ellipsis-menu'; -import { SitesGridActionRenew } from './sites-grid-action-renew'; +import { displaySiteUrl } from '../utils'; import { SitesGridTile } from './sites-grid-tile'; -import SitesLaunchStatusBadge from './sites-launch-status-badge'; -import SitesP2Badge from './sites-p2-badge'; import { SiteItemThumbnail } from './sites-site-item-thumbnail'; -import { SiteLaunchNag } from './sites-site-launch-nag'; import { SiteName } from './sites-site-name'; import { SiteUrl, Truncated } from './sites-site-url'; -import SitesStagingBadge from './sites-staging-badge'; import TransferNoticeWrapper from './sites-transfer-notice-wrapper'; -import { ThumbnailLink } from './thumbnail-link'; import { WithAtomicTransfer } from './with-atomic-transfer'; import type { SiteExcerptData } from '@automattic/sites'; @@ -45,13 +28,6 @@ const THUMBNAIL_DIMENSION = { height: 401 / ASPECT_RATIO, }; -const badges = css( { - display: 'flex', - gap: '8px', - alignItems: 'center', - marginInlineStart: 'auto', -} ); - const selectAction = css( { display: 'flex', gap: '8px', @@ -76,61 +52,16 @@ const SitesGridItemSecondary = styled.div( { justifyContent: 'space-between', } ); -const EllipsisMenuContainer = styled.div( { - width: '24px', -} ); - -const ellipsis = css( { - '.button.ellipsis-menu__toggle': { - padding: 0, - }, - - '.gridicon.ellipsis-menu__toggle-icon': { - width: '24px', - height: '16px', - insetBlockStart: '4px', - }, -} ); - interface SitesGridItemProps { site: SiteExcerptData; - showLaunchNag?: boolean; - showBadgeSection?: boolean; - showThumbnailLink?: boolean; - showSiteRenewLink?: boolean; - onSiteSelectBtnClick?: ( site: SiteExcerptData ) => void; + onSiteSelectBtnClick: ( site: SiteExcerptData ) => void; } export const SitesGridItem = memo( ( props: SitesGridItemProps ) => { const { __ } = useI18n(); - const { - site, - showLaunchNag = true, - showBadgeSection = true, - showThumbnailLink = true, - showSiteRenewLink = true, - onSiteSelectBtnClick, - } = props; - - const isP2Site = site.options?.is_wpforteams_site; - const isWpcomStagingSite = isStagingSite( site ); - const translatedStatus = useSiteLaunchStatusLabel( site ); - const isTrialSitePlan = useSelector( ( state ) => isTrialSite( state, site.ID ) ); - const wpAdminUrl = getSiteWpAdminUrl( site ); + const { site, onSiteSelectBtnClick } = props; const { ref, inView } = useInView( { triggerOnce: true } ); - const ThumbnailWrapper = showThumbnailLink ? ThumbnailLink : 'div'; - - const siteDashboardUrlProps = showThumbnailLink - ? { - href: - siteDefaultInterface( site ) === 'wp-admin' - ? wpAdminUrl || getDashboardUrl( site.slug ) - : getDashboardUrl( site.slug ), - title: __( 'Visit Dashboard' ), - } - : {}; - let siteUrl = site.URL; if ( site.options?.is_redirect && site.options?.unmapped_url ) { siteUrl = site.options?.unmapped_url; @@ -140,7 +71,7 @@ export const SitesGridItem = memo( ( props: SitesGridItemProps ) => { ref={ ref } leading={ <> - +
    { height={ THUMBNAIL_DIMENSION.height } sizesAttr={ SIZES_ATTR } /> - - { showSiteRenewLink && site.plan?.expired && ( - - ) } +
    } primary={ <> - - { site.title } - - - { showBadgeSection && ( -
    - { isP2Site && P2 } - { isWpcomStagingSite && { __( 'Staging' ) } } - { isTrialSitePlan && ( - { __( 'Trial' ) } - ) } - { getSiteLaunchStatus( site ) !== 'public' && ! isTrialSitePlan && ( - { translatedStatus } - ) } - - { inView && } - -
    - ) } - { onSiteSelectBtnClick && ( -
    - -
    - ) } + { site.title } +
    + +
    } secondary={ @@ -195,8 +103,6 @@ export const SitesGridItem = memo( ( props: SitesGridItemProps ) => { { ( result ) => { if ( result.wasTransferring ) { return ; - } else if ( showLaunchNag && 'unlaunched' === site.launch_status ) { - return ; } return <>; } } diff --git a/client/sites-dashboard/components/sites-grid-item/README.md b/client/sites-dashboard/components/sites-grid-item/README.md index 9918e66c829179..81a9433375e1a6 100644 --- a/client/sites-dashboard/components/sites-grid-item/README.md +++ b/client/sites-dashboard/components/sites-grid-item/README.md @@ -1,11 +1,12 @@ # SitesGridItem -Renders a SitesGridItem component. +Renders a SitesGridItem component with site selection option. ## How to use ```jsx import SitesGridItem from 'calypso/sites-dashboard/components/sites-grid-item'; +import type { SiteExcerptData } from '@automattic/sites'; function render() { const site = {}; @@ -14,7 +15,8 @@ function render() { + onSiteSelectBtnClick={ ( s: SiteExcerptData ) => {} } + />
    ); } @@ -24,8 +26,4 @@ function render() { - `site`: a site data e.g. SiteExcerptData object. - `key`: unique key eg. Site ID. -- `showLaunchNag`: boolean, optional, default: true. -- `showBadgeSection`: boolean, optional, default: true. -- `showThumbnailLink`: boolean, optional, default: true. -- `showSiteRenewLink`: boolean, optional, default: true. -- `onSiteSelectBtnClick`: function, optional, default: undefined. +- `onSiteSelectBtnClick`: function. diff --git a/client/sites-dashboard/components/sites-grid-item/docs/example.jsx b/client/sites-dashboard/components/sites-grid-item/docs/example.jsx index f4af6ac558ddca..9cdad31acddd23 100644 --- a/client/sites-dashboard/components/sites-grid-item/docs/example.jsx +++ b/client/sites-dashboard/components/sites-grid-item/docs/example.jsx @@ -29,18 +29,24 @@ const itemClassName = css( { minWidth: 0, } ); -const SitesGridItemExample = () => { +const SitesGridItemSelectExample = () => { return (
    { sampleSiteData.map( ( site ) => (
    - + { + console.log( _site ); + } } + />
    ) ) }
    ); }; -SitesGridItemExample.displayName = 'SitesGridItem'; +SitesGridItemSelectExample.displayName = 'SitesGridItemSelect'; -export default SitesGridItemExample; +export default SitesGridItemSelectExample; diff --git a/client/sites-dashboard/components/sites-grid.tsx b/client/sites-dashboard/components/sites-grid.tsx index 3b5d063d85a1f9..c6223e4da734a8 100644 --- a/client/sites-dashboard/components/sites-grid.tsx +++ b/client/sites-dashboard/components/sites-grid.tsx @@ -26,21 +26,11 @@ interface SitesGridProps { className?: string; isLoading: boolean; sites: SiteExcerptData[]; - siteSelectorMode?: boolean; - onSiteSelectBtnClick?: ( site: SiteExcerptData ) => void; + onSiteSelectBtnClick: ( site: SiteExcerptData ) => void; } export const SitesGrid = ( props: SitesGridProps ) => { - const { sites, isLoading, className, siteSelectorMode = false, onSiteSelectBtnClick } = props; - const additionalProps = siteSelectorMode - ? { - showLaunchNag: false, - showBadgeSection: false, - showThumbnailLink: false, - showSiteRenewLink: false, - onSiteSelectBtnClick, - } - : {}; + const { sites, isLoading, className, onSiteSelectBtnClick } = props; return (
    @@ -49,7 +39,11 @@ export const SitesGrid = ( props: SitesGridProps ) => { .fill( null ) .map( ( _, i ) => ) : sites.map( ( site ) => ( - + ) ) }
    ); diff --git a/client/sites-dashboard/components/sites-plan-renew-nag.tsx b/client/sites-dashboard/components/sites-plan-renew-nag.tsx index 0bc361577f6bd4..8f2561b7ea1237 100644 --- a/client/sites-dashboard/components/sites-plan-renew-nag.tsx +++ b/client/sites-dashboard/components/sites-plan-renew-nag.tsx @@ -22,12 +22,14 @@ const PlanRenewContainer = styled.div( { marginTop: '-2px', } ); -const PlanRenewLink = styled.a( { - whiteSpace: 'nowrap', - textDecoration: 'underline', - fontSize: '12px', - paddingTop: '2px', -} ); +const PlanRenewLink = styled.a` + white-space: nowrap; + text-decoration: underline !important; + font-size: 12px; + font-weight: 400 !important; + padding-top: 2px; + color: var( --color-link ) !important; +`; const IconContainer = styled.div( { color: '#ea303f', diff --git a/client/sites-dashboard/components/sites-site-launch-nag.tsx b/client/sites-dashboard/components/sites-site-launch-nag.tsx index cb477210416be4..05387929222fa3 100644 --- a/client/sites-dashboard/components/sites-site-launch-nag.tsx +++ b/client/sites-dashboard/components/sites-site-launch-nag.tsx @@ -37,6 +37,7 @@ const SiteLaunchNagLink = styled.a( { fontSize: '12px', lineHeight: '16px', whiteSpace: 'nowrap', + color: 'var(--color-link) !important', } ); const SiteLaunchNagText = styled.span( { diff --git a/client/sites-dashboard/components/sites-site-plan.tsx b/client/sites-dashboard/components/sites-site-plan.tsx index 7fff2ea43d6e04..79dcacbd3b80cd 100644 --- a/client/sites-dashboard/components/sites-site-plan.tsx +++ b/client/sites-dashboard/components/sites-site-plan.tsx @@ -6,6 +6,7 @@ import type { SiteExcerptData } from '@automattic/sites'; const SitePlanContainer = styled.div` display: inline; + overflow: hidden; > * { vertical-align: middle; line-height: normal; diff --git a/client/sites-dashboard/components/test/__snapshots__/sites-grid-item.tsx.snap b/client/sites-dashboard/components/test/__snapshots__/sites-grid-item.tsx.snap deleted file mode 100644 index d2ff9bc628b73a..00000000000000 --- a/client/sites-dashboard/components/test/__snapshots__/sites-grid-item.tsx.snap +++ /dev/null @@ -1,224 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Custom render 1`] = ` - -`; - -exports[` Custom render 2 1`] = ` -
    -
    -
    -
    -
    - T -
    -
    -
    -
    - -
    -`; - -exports[` Default render 1`] = ` -
    - -
    -
    -
    - T -
    -
    -
    -
    - -`; diff --git a/client/sites-dashboard/components/test/sites-grid-item.tsx b/client/sites-dashboard/components/test/sites-grid-item.tsx deleted file mode 100644 index 9a5d3417456d0d..00000000000000 --- a/client/sites-dashboard/components/test/sites-grid-item.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @jest-environment jsdom - */ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { SitesGridItem } from 'calypso/sites-dashboard/components/sites-grid-item'; -import { useCheckSiteTransferStatus } from '../../hooks/use-check-site-transfer-status'; -import type { SiteExcerptData } from '@automattic/sites'; - -function makeTestSite( { title = 'test', is_coming_soon = false, lang = 'en' } = {} ) { - return { - ID: 1, - title, - slug: 'test_slug', - URL: '', - launch_status: 'launched', - options: {}, - jetpack: false, - is_coming_soon, - lang, - }; -} - -jest.mock( 'calypso/sites-dashboard/hooks/use-check-site-transfer-status.tsx', () => ( { - __esModule: true, - useCheckSiteTransferStatus: jest.fn(), -} ) ); - -jest.mock( 'react-redux', () => ( { - ...jest.requireActual( 'react-redux' ), - useSelector: jest.fn(), -} ) ); - -describe( '', () => { - beforeEach( () => { - ( useCheckSiteTransferStatus as jest.Mock ).mockReturnValue( { - isTransferInProgress: false, - } ); - } ); - test( 'Default render', () => { - const tree = renderer - .create( - - ) - .toJSON(); - expect( tree ).toMatchSnapshot(); - } ); - - test( 'Custom render', () => { - const tree = renderer - .create( - - ) - .toJSON(); - expect( tree ).toMatchSnapshot(); - } ); - - test( 'Custom render 2', () => { - const tree = renderer - .create( - - ) - .toJSON(); - expect( tree ).toMatchSnapshot(); - } ); -} ); diff --git a/client/sites-dashboard/utils.ts b/client/sites-dashboard/utils.ts index 771965c284074e..cece6fe30f8a69 100644 --- a/client/sites-dashboard/utils.ts +++ b/client/sites-dashboard/utils.ts @@ -106,10 +106,6 @@ export const generateSiteInterfaceLink = ( return targetLink; }; -export const getSiteWpAdminUrl = ( site: SiteExcerptNetworkData ) => { - return site?.options?.admin_url ?? ''; -}; - export const SMALL_MEDIA_QUERY = 'screen and ( max-width: 600px )'; export const MEDIA_QUERIES = { diff --git a/client/start-with/controller.tsx b/client/start-with/controller.tsx deleted file mode 100644 index 2898d2f977e159..00000000000000 --- a/client/start-with/controller.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import config from '@automattic/calypso-config'; -import page, { Context } from '@automattic/calypso-router'; -import { translate } from 'i18n-calypso'; -import EmptyContent from 'calypso/components/empty-content'; -import Main from 'calypso/components/main'; -import { StartWithSquarePayments } from './square-payments'; - -export function startWithSquarePaymentsContext( context: Context, next: () => void ): void { - if ( ! config.isEnabled( 'start-with/square-payments' ) ) { - page.redirect( '/' ); - return; - } - - context.primary = ( -
    - -
    - ); - - next(); -} - -export const notFound = ( context: Context, next: () => void ) => { - context.primary = ( - - ); - - next(); -}; diff --git a/client/start-with/index.node.ts b/client/start-with/index.node.ts deleted file mode 100644 index d67ee135296e8b..00000000000000 --- a/client/start-with/index.node.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getLanguageRouteParam } from '@automattic/i18n-utils'; -import { makeLayout, setLocaleMiddleware } from 'calypso/controller'; -import { serverRouter } from 'calypso/server/isomorphic-routing'; - -/** - * Using the server routing for this section has the sole purpose of defining - * a named route parameter for the language, that is used to set `context.lang` - * via the `setLocaleMiddleware()`. - * - * The `context.lang` value is then used in the server renderer to properly - * attach the translation files to the page. - * @see https://github.com/Automattic/wp-calypso/blob/trunk/client/server/render/index.js#L171. - */ -export default ( router: ReturnType< typeof serverRouter > ) => { - const lang = getLanguageRouteParam(); - - router( [ `/${ lang }/start-with(/*)?` ], setLocaleMiddleware(), makeLayout ); -}; diff --git a/client/start-with/index.web.ts b/client/start-with/index.web.ts deleted file mode 100644 index 9f4c099115d284..00000000000000 --- a/client/start-with/index.web.ts +++ /dev/null @@ -1,8 +0,0 @@ -import page from '@automattic/calypso-router'; -import { makeLayout, render as clientRender } from 'calypso/controller/index.web'; -import { notFound, startWithSquarePaymentsContext } from 'calypso/start-with/controller'; - -export default function () { - page( '/start-with/square-payments', startWithSquarePaymentsContext, makeLayout, clientRender ); - page( '/start-with*', notFound, makeLayout, clientRender ); -} diff --git a/client/start-with/package.json b/client/start-with/package.json deleted file mode 100644 index 937fc8ce401241..00000000000000 --- a/client/start-with/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "index.node.ts", - "browser": "index.web.ts" -} diff --git a/client/start-with/square-payments/index.tsx b/client/start-with/square-payments/index.tsx deleted file mode 100644 index b4089efa9f9704..00000000000000 --- a/client/start-with/square-payments/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { recordTracksEvent } from '@automattic/calypso-analytics'; -import page from '@automattic/calypso-router'; -import { Button } from '@automattic/components'; -import { useTranslate } from 'i18n-calypso'; -import React from 'react'; -import BloombergLogo from 'calypso/assets/images/start-with/Bloomberg.svg'; -import CNNLogo from 'calypso/assets/images/start-with/CNN.svg'; -import CondeNast from 'calypso/assets/images/start-with/Conde_Nast.svg'; -import DisneyLogo from 'calypso/assets/images/start-with/Disney.svg'; -import MetaLogo from 'calypso/assets/images/start-with/Meta.svg'; -import NewsCorpLogo from 'calypso/assets/images/start-with/News_Corp.svg'; -import SlackLogo from 'calypso/assets/images/start-with/Slack.svg'; -import TechCrunchImage from 'calypso/assets/images/start-with/Tech_Crunch.svg'; -import TimeLogo from 'calypso/assets/images/start-with/Time.svg'; -import USATodayImage from 'calypso/assets/images/start-with/USA_Today.svg'; -import DotcomWooSquareImage from 'calypso/assets/images/start-with/dotcom-woo-square.png'; -import DocumentHead from 'calypso/components/data/document-head'; -import PageViewTracker from 'calypso/lib/analytics/page-view-tracker'; -import './style.scss'; - -export const StartWithSquarePayments: React.FC = () => { - const translate = useTranslate(); - const onCTAClick = () => { - recordTracksEvent( 'calypso_start_with_cta_click', { partner_bundle: 'square_payments' } ); - page( '/setup/entrepreneur/start?partnerBundle=square' ); - }; - - return ( -
    - - -
    -
    -

    - { translate( 'Get Started with WordPress.com and Square Payments' ) } -

    -

    - { translate( - 'Partnering with Square Payments, WordPress.com offers you an easy way to build and manage your online store. Click below to begin your quick and easy setup process.' - ) } -

    - -
    -
    - { -
    -
    -
    -
    { translate( 'Trusted by 160 million worldwide' ) }
    -
    -
    - { - { - { - { - { - { - { - { - { - { -
    -
    -
    -
    - ); -}; diff --git a/client/start-with/square-payments/style.scss b/client/start-with/square-payments/style.scss deleted file mode 100644 index bd7b05e9906bf0..00000000000000 --- a/client/start-with/square-payments/style.scss +++ /dev/null @@ -1,274 +0,0 @@ -@import "@automattic/components/src/styles/typography"; -@import "@wordpress/base-styles/breakpoints"; - -.layout.is-section-start-with { - height: 100%; - display: grid; - grid-template-rows: max-content 1fr; - - .layout__content { - height: 100%; - width: 100%; - padding: 0; - background: #fdfdfd; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - - .layout__primary, - .main { - height: 100%; - width: 100%; - padding: 0; - } - } - - // stylelint-disable declaration-property-unit-allowed-list - .container { - height: 100%; - display: grid; - place-items: center; - grid-template-rows: 1fr 128px; - - .content { - max-width: 1280px; - display: flex; - flex-direction: row; - - .left-column { - display: flex; - flex-direction: column; - gap: 46px; - - .title { - color: var(--studio-gray-80); - font-family: $font-recoleta; - font-size: 79px; - line-height: 105%; - } - - .subtitle { - color: #000; - font-family: $font-sf-pro-display; - font-size: $font-body; - text-wrap: pretty; - font-style: normal; - line-height: 162.5%; - width: 530px; - margin-bottom: 0; - } - - .start-store-cta { - width: fit-content; - background: #3858e9; - color: #fff; - border-radius: 3px; - padding: 9px 20px; - font-size: $font-body; - font-weight: 500; - line-height: 150%; - } - } - - .right-column { - flex-shrink: 0; - - img { - width: 500px; - } - } - } - - .footer { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - - .text { - width: 1358px; - color: var(--studio-gray-40); - font-family: $font-sf-pro-display; - font-size: $font-body; - font-style: normal; - line-height: 150%; - margin-bottom: 18px; - } - - .brands { - width: 100%; - background-color: var(--studio-gray-80); - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; - - - .brands-container { - padding: 25px 0; - display: flex; - justify-content: center; - align-items: center; - gap: 50px; - } - } - } - } - // stylelint-enable declaration-property-unit-allowed-list - - @media (max-width: $break-huge) { - .container { - .content { - max-width: calc(100vw - 100px); - .left-column { - .title { - font-size: $font-headline-large; - } - } - - .right-column { - img { - width: 400px; - } - } - } - - .footer { - .text { - width: 1182px; - } - - .brands .brands-container { - img:nth-last-of-type(-n+1) { - display: none; - } - } - } - } - } - - - @media (max-width: $break-wide) { - .container { - .footer { - .text { - width: 1020px; - } - - .brands .brands-container { - img:nth-last-of-type(-n+2) { - display: none; - } - } - } - } - } - - @media (max-width: $break-xlarge) { - .container { - .footer { - .text { - width: 884px; - } - - .brands .brands-container { - img:nth-last-of-type(-n+3) { - display: none; - } - } - } - } - } - - @media (max-width: $break-large) { - .container { - .content { - flex-direction: column-reverse; - align-items: center; - gap: 100px; - align-self: start; - - .left-column { - padding-bottom: 20px; - - .title { - width: calc(100vw - 100px); - font-size: $font-headline-medium; - } - - .subtitle { - width: calc(100vw - 100px); - } - } - - .right-column { - padding-top: 100px; - } - } - - .footer { - .text { - width: 708px; - } - - .brands .brands-container { - gap: 20px; - } - } - } - } - - @media (max-width: $break-medium) { - .container { - .content { - gap: 5px; - - .left-column { - gap: 16px; - - .title { - font-size: $font-headline-small; - } - - img { - width: 250px; - } - } - - .right-column { - padding-top: 30px; - } - } - - .footer { - .text { - width: 330px; - } - - .brands .brands-container { - img:nth-last-of-type(-n+6) { - display: none; - } - } - } - } - } - - @media (max-height: $break-medium) { - .container .content { - display: flex; - flex-direction: column; - justify-content: center; - height: 100%; - - .right-column { - display: none; - } - } - } -} - diff --git a/client/state/a8c-for-agencies/agency/selectors.ts b/client/state/a8c-for-agencies/agency/selectors.ts index c0f56955191ab3..2156274bcad21d 100644 --- a/client/state/a8c-for-agencies/agency/selectors.ts +++ b/client/state/a8c-for-agencies/agency/selectors.ts @@ -1,5 +1,6 @@ // Required for modular state. import 'calypso/state/a8c-for-agencies/init'; +import { isEnabled } from '@automattic/calypso-config'; import { A4AStore, APIError, Agency } from '../types'; export function getActiveAgency( state: A4AStore ): Agency | null { @@ -34,3 +35,13 @@ export function hasAgency( state: A4AStore ): boolean { export function isAgencyClientUser( state: A4AStore ): boolean { return state.a8cForAgencies.agencies.isAgencyClientUser; } + +export function hasAgencyCapability( state: A4AStore, capability: string ): boolean { + if ( ! isEnabled( 'a4a-multi-user-support' ) ) { + // This is always true if the feature is not enabled to bypass restrictions. + return true; + } + + const agency = getActiveAgency( state ); + return agency?.user?.capabilities?.includes( capability ) ?? false; +} diff --git a/client/state/billing-transactions/schema.js b/client/state/billing-transactions/schema.js index 0125e09bb2b4a2..cf656d6af6d16c 100644 --- a/client/state/billing-transactions/schema.js +++ b/client/state/billing-transactions/schema.js @@ -20,6 +20,7 @@ export const billingTransactionsSchema = { pay_ref: { type: 'string' }, pay_part: { type: 'string' }, cc_type: { type: 'string' }, + cc_display_type: { type: [ 'string', 'null' ] }, cc_num: { type: 'string' }, cc_name: { type: 'string' }, cc_email: { type: 'string' }, diff --git a/client/state/billing-transactions/types.ts b/client/state/billing-transactions/types.ts index 7c2c91b9d79198..ec915534ed3018 100644 --- a/client/state/billing-transactions/types.ts +++ b/client/state/billing-transactions/types.ts @@ -32,6 +32,7 @@ export interface BillingTransaction { cc_name: string; cc_num: string; cc_type: string; + cc_display_brand: string | null; credit: string; date: string; desc: string; diff --git a/client/state/jetpack-agency-dashboard/actions.ts b/client/state/jetpack-agency-dashboard/actions.ts index 11d5989087f41e..5f48ec9dbd7475 100644 --- a/client/state/jetpack-agency-dashboard/actions.ts +++ b/client/state/jetpack-agency-dashboard/actions.ts @@ -69,7 +69,7 @@ export const updateFilter = ( filter: AgencyDashboardFilterOption[] ) => () => { updateDashboardURLQueryArgs( { filter } ); }; -export const updateSort = ( sort: DashboardSortInterface ) => () => { +export const updateSort = ( sort?: DashboardSortInterface ) => () => { updateDashboardURLQueryArgs( { sort } ); }; diff --git a/config/development.json b/config/development.json index bc1ad51a0f1e4a..65a832bc612a57 100644 --- a/config/development.json +++ b/config/development.json @@ -167,7 +167,7 @@ "page/export": true, "pattern-assembler/v2": true, "performance-profiler": true, - "performance-profiler/llm": false, + "performance-profiler/llm": true, "plans/hosting-trial": true, "plans/migration-trial": true, "plans/personal-plan": true, @@ -196,8 +196,6 @@ "redirect-fallback-browsers": false, "rum-tracking/logstash": true, "safari-idb-mitigation": true, - "start-with/square-payments": true, - "start-with/stripe": true, "security/security-checkup": true, "seller-experience": true, "server-side-rendering": true, diff --git a/config/horizon.json b/config/horizon.json index 49f763c63a8f63..da5cb990be28cb 100644 --- a/config/horizon.json +++ b/config/horizon.json @@ -78,7 +78,7 @@ "layout/app-banner": true, "layout/guided-tours": true, "layout/query-selected-editor": true, - "layout/site-level-user-profile": false, + "layout/site-level-user-profile": true, "layout/support-article-dialog": true, "legal-updates-banner": false, "livechat_solution": true, @@ -122,8 +122,6 @@ "readymade-templates/showcase": false, "redirect-fallback-browsers": true, "safari-idb-mitigation": true, - "start-with/square-payments": true, - "start-with/stripe": false, "security/security-checkup": true, "seller-experience": true, "server-side-rendering": true, diff --git a/config/production.json b/config/production.json index 2abb84209dab5d..0aef0ddea73775 100644 --- a/config/production.json +++ b/config/production.json @@ -102,7 +102,7 @@ "layout/app-banner": true, "layout/guided-tours": true, "layout/query-selected-editor": true, - "layout/site-level-user-profile": false, + "layout/site-level-user-profile": true, "layout/support-article-dialog": true, "legal-updates-banner": false, "livechat_solution": true, @@ -137,6 +137,7 @@ "p2-enabled": false, "pattern-assembler/v2": true, "performance-profiler": false, + "performance-profiler/llm": true, "plans/hosting-trial": true, "plans/migration-trial": true, "plans/personal-plan": true, @@ -164,8 +165,6 @@ "redirect-fallback-browsers": true, "rum-tracking/logstash": true, "safari-idb-mitigation": true, - "start-with/square-payments": true, - "start-with/stripe": false, "security/security-checkup": true, "seller-experience": true, "server-side-rendering": true, diff --git a/config/stage.json b/config/stage.json index 001e6f7737703b..cb63d3da45e4f5 100644 --- a/config/stage.json +++ b/config/stage.json @@ -98,7 +98,7 @@ "layout/app-banner": true, "layout/guided-tours": true, "layout/query-selected-editor": true, - "layout/site-level-user-profile": false, + "layout/site-level-user-profile": true, "layout/support-article-dialog": true, "legal-updates-banner": false, "livechat_solution": true, @@ -133,6 +133,7 @@ "page/export": true, "pattern-assembler/v2": true, "performance-profiler": false, + "performance-profiler/llm": true, "plans/hosting-trial": true, "plans/migration-trial": true, "plans/personal-plan": true, @@ -160,8 +161,6 @@ "redirect-fallback-browsers": true, "rum-tracking/logstash": true, "safari-idb-mitigation": true, - "start-with/square-payments": true, - "start-with/stripe": false, "security/security-checkup": true, "seller-experience": true, "server-side-rendering": true, diff --git a/config/wpcalypso.json b/config/wpcalypso.json index a56635e4ffa798..c6a97143ffac3c 100644 --- a/config/wpcalypso.json +++ b/config/wpcalypso.json @@ -132,6 +132,7 @@ "p2-enabled": false, "pattern-assembler/v2": true, "performance-profiler": true, + "performance-profiler/llm": true, "plans/hosting-trial": true, "plans/migration-trial": true, "plans/personal-plan": true, @@ -158,8 +159,7 @@ "redirect-fallback-browsers": true, "rum-tracking/logstash": true, "safari-idb-mitigation": true, - "start-with/square-payments": true, - "start-with/stripe": true, + "security/security-checkup": true, "seller-experience": true, "server-side-rendering": true, diff --git a/package.json b/package.json index 9515ed0329aa70..162389cf777080 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "@automattic/calypso-products": "workspace:^", "@automattic/calypso-razorpay": "workspace:^", "@automattic/calypso-router": "workspace:^", - "@automattic/color-studio": "2.6.0", + "@automattic/color-studio": "^3.0.1", "@automattic/command-palette": "workspace:^", "@automattic/components": "workspace:^", "@automattic/data-stores": "workspace:^", @@ -350,7 +350,7 @@ "@wordpress/customize-widgets": "5.2.0", "@wordpress/data-controls": "4.2.0", "@wordpress/data": "^10.2.0", - "@wordpress/dataviews": "patch:@wordpress/dataviews@npm%3A0.4.1#~/.yarn/patches/@wordpress-dataviews-npm-0.4.1-2c01fa0792.patch", + "@wordpress/dataviews": "4.2.0", "@wordpress/date": "5.2.0", "@wordpress/dependency-extraction-webpack-plugin": "5.9.0", "@wordpress/deprecated": "4.2.0", diff --git a/packages/calypso-color-schemes/package.json b/packages/calypso-color-schemes/package.json index b861cc14e2570c..d1a5e6a39a6f07 100644 --- a/packages/calypso-color-schemes/package.json +++ b/packages/calypso-color-schemes/package.json @@ -28,7 +28,7 @@ "devDependencies": { "@automattic/calypso-eslint-overrides": "workspace:^", "@automattic/calypso-typescript-config": "workspace:^", - "@automattic/color-studio": "2.6.0", + "@automattic/color-studio": "^3.0.1", "postcss": "^8.4.5", "postcss-custom-properties": "^11.0.0", "sass": "^1.37.5" diff --git a/packages/calypso-color-schemes/src/shared/color-schemes/_default.scss b/packages/calypso-color-schemes/src/shared/color-schemes/_default.scss index 8f3d810e623d99..6e1634d1accafc 100644 --- a/packages/calypso-color-schemes/src/shared/color-schemes/_default.scss +++ b/packages/calypso-color-schemes/src/shared/color-schemes/_default.scss @@ -31,36 +31,36 @@ --color-primary-100: var(--studio-blue-100); --color-primary-100-rgb: var(--studio-blue-100-rgb); - --color-accent: var(--studio-pink-50); - --color-accent-rgb: var(--studio-pink-50-rgb); - --color-accent-dark: var(--studio-pink-70); - --color-accent-dark-rgb: var(--studio-pink-70-rgb); - --color-accent-light: var(--studio-pink-30); - --color-accent-light-rgb: var(--studio-pink-30-rgb); - --color-accent-0: var(--studio-pink-0); - --color-accent-0-rgb: var(--studio-pink-0-rgb); - --color-accent-5: var(--studio-pink-5); - --color-accent-5-rgb: var(--studio-pink-5-rgb); - --color-accent-10: var(--studio-pink-10); - --color-accent-10-rgb: var(--studio-pink-10-rgb); - --color-accent-20: var(--studio-pink-20); - --color-accent-20-rgb: var(--studio-pink-20-rgb); - --color-accent-30: var(--studio-pink-30); - --color-accent-30-rgb: var(--studio-pink-30-rgb); - --color-accent-40: var(--studio-pink-40); - --color-accent-40-rgb: var(--studio-pink-40-rgb); - --color-accent-50: var(--studio-pink-50); - --color-accent-50-rgb: var(--studio-pink-50-rgb); - --color-accent-60: var(--studio-pink-60); - --color-accent-60-rgb: var(--studio-pink-60-rgb); - --color-accent-70: var(--studio-pink-70); - --color-accent-70-rgb: var(--studio-pink-70-rgb); - --color-accent-80: var(--studio-pink-80); - --color-accent-80-rgb: var(--studio-pink-80-rgb); - --color-accent-90: var(--studio-pink-90); - --color-accent-90-rgb: var(--studio-pink-90-rgb); - --color-accent-100: var(--studio-pink-100); - --color-accent-100-rgb: var(--studio-pink-100-rgb); + --color-accent: var(--studio-wordpress-blue-50); + --color-accent-rgb: var(--studio-wordpress-blue-50-rgb); + --color-accent-dark: var(--studio-wordpress-blue-70); + --color-accent-dark-rgb: var(--studio-wordpress-blue-70-rgb); + --color-accent-light: var(--studio-wordpress-blue-30); + --color-accent-light-rgb: var(--studio-wordpress-blue-30-rgb); + --color-accent-0: var(--studio-wordpress-blue-0); + --color-accent-0-rgb: var(--studio-wordpress-blue-0-rgb); + --color-accent-5: var(--studio-wordpress-blue-5); + --color-accent-5-rgb: var(--studio-wordpress-blue-5-rgb); + --color-accent-10: var(--studio-wordpress-blue-10); + --color-accent-10-rgb: var(--studio-wordpress-blue-10-rgb); + --color-accent-20: var(--studio-wordpress-blue-20); + --color-accent-20-rgb: var(--studio-wordpress-blue-20-rgb); + --color-accent-30: var(--studio-wordpress-blue-30); + --color-accent-30-rgb: var(--studio-wordpress-blue-30-rgb); + --color-accent-40: var(--studio-wordpress-blue-40); + --color-accent-40-rgb: var(--studio-wordpress-blue-40-rgb); + --color-accent-50: var(--studio-wordpress-blue-50); + --color-accent-50-rgb: var(--studio-wordpress-blue-50-rgb); + --color-accent-60: var(--studio-wordpress-blue-60); + --color-accent-60-rgb: var(--studio-wordpress-blue-60-rgb); + --color-accent-70: var(--studio-wordpress-blue-70); + --color-accent-70-rgb: var(--studio-wordpress-blue-70-rgb); + --color-accent-80: var(--studio-wordpress-blue-80); + --color-accent-80-rgb: var(--studio-wordpress-blue-80-rgb); + --color-accent-90: var(--studio-wordpress-blue-90); + --color-accent-90-rgb: var(--studio-wordpress-blue-90-rgb); + --color-accent-100: var(--studio-wordpress-blue-100); + --color-accent-100-rgb: var(--studio-wordpress-blue-100-rgb); --color-neutral: var(--studio-gray-50); --color-neutral-rgb: var(--studio-gray-50-rgb); diff --git a/packages/calypso-products/src/products-list.ts b/packages/calypso-products/src/products-list.ts index 81add7b9f15de2..5f19335cd298ef 100644 --- a/packages/calypso-products/src/products-list.ts +++ b/packages/calypso-products/src/products-list.ts @@ -206,7 +206,6 @@ export const JETPACK_SITE_PRODUCTS_WITH_FEATURES: Record< [ PRODUCT_JETPACK_AI_MONTHLY ]: { product_name: PRODUCT_SHORT_NAMES[ PRODUCT_JETPACK_AI_MONTHLY ], product_slug: PRODUCT_JETPACK_AI_MONTHLY, - product_alias: PRODUCT_JETPACK_AI_MONTHLY_100, type: PRODUCT_JETPACK_AI_MONTHLY, term: TERM_MONTHLY, bill_period: PLAN_MONTHLY_PERIOD, @@ -278,7 +277,6 @@ export const JETPACK_SITE_PRODUCTS_WITH_FEATURES: Record< [ PRODUCT_JETPACK_AI_YEARLY ]: { product_name: PRODUCT_SHORT_NAMES[ PRODUCT_JETPACK_AI_YEARLY ], product_slug: PRODUCT_JETPACK_AI_YEARLY, - product_alias: PRODUCT_JETPACK_AI_YEARLY_100, type: PRODUCT_JETPACK_AI_YEARLY, term: TERM_ANNUALLY, bill_period: PLAN_ANNUAL_PERIOD, @@ -350,7 +348,6 @@ export const JETPACK_SITE_PRODUCTS_WITH_FEATURES: Record< [ PRODUCT_JETPACK_AI_BI_YEARLY ]: { product_name: PRODUCT_SHORT_NAMES[ PRODUCT_JETPACK_AI_BI_YEARLY ], product_slug: PRODUCT_JETPACK_AI_BI_YEARLY, - product_alias: PRODUCT_JETPACK_AI_BI_YEARLY_100, type: PRODUCT_JETPACK_AI_BI_YEARLY, term: TERM_BIENNIALLY, bill_period: PLAN_BIENNIAL_PERIOD, diff --git a/packages/calypso-products/src/translations.tsx b/packages/calypso-products/src/translations.tsx index 2e64a67e631fee..aa30e5eec6724b 100644 --- a/packages/calypso-products/src/translations.tsx +++ b/packages/calypso-products/src/translations.tsx @@ -615,6 +615,15 @@ export const getJetpackProductDisclaimers = ( <> ); + const aiAssistantDisclaimer = translate( + 'Limits apply for high request capacity. {{link}}Learn more about it here.{{/link}}', + { + components: { + link: getLink(), + }, + } + ); + const monitorDisclaimer = translate( 'Limit of 20 SMS per site, each month.' ); return { @@ -635,6 +644,9 @@ export const getJetpackProductDisclaimers = ( [ PLAN_JETPACK_COMPLETE_MONTHLY ]: backupDisclaimer, [ PRODUCT_JETPACK_MONITOR_YEARLY ]: monitorDisclaimer, [ PRODUCT_JETPACK_MONITOR_MONTHLY ]: monitorDisclaimer, + [ PRODUCT_JETPACK_AI_MONTHLY ]: aiAssistantDisclaimer, + [ PRODUCT_JETPACK_AI_YEARLY ]: aiAssistantDisclaimer, + [ PRODUCT_JETPACK_AI_BI_YEARLY ]: aiAssistantDisclaimer, }; }; @@ -1545,7 +1557,7 @@ export const getJetpackProductsWhatIsIncluded = (): Record< string, Array< Trans return { [ PRODUCT_JETPACK_AI_MONTHLY ]: [ - translate( '100 monthly requests (upgradeable)' ), + translate( 'High request capacity *' ), ...aiAssistantIncludesInfo, ], [ PRODUCT_JETPACK_AI_MONTHLY_100 ]: [ @@ -1569,7 +1581,7 @@ export const getJetpackProductsWhatIsIncluded = (): Record< string, Array< Trans ...aiAssistantIncludesInfo, ], [ PRODUCT_JETPACK_AI_YEARLY ]: [ - translate( '100 monthly requests (upgradeable)' ), + translate( 'High request capacity *' ), ...aiAssistantIncludesInfo, ], [ PRODUCT_JETPACK_AI_YEARLY_100 ]: [ @@ -1593,7 +1605,7 @@ export const getJetpackProductsWhatIsIncluded = (): Record< string, Array< Trans ...aiAssistantIncludesInfo, ], [ PRODUCT_JETPACK_AI_BI_YEARLY ]: [ - translate( '100 monthly requests (upgradeable)' ), + translate( 'High request capacity *' ), ...aiAssistantIncludesInfo, ], [ PRODUCT_JETPACK_AI_BI_YEARLY_100 ]: [ diff --git a/packages/components/src/highlight-cards/annual-highlight-cards.tsx b/packages/components/src/highlight-cards/annual-highlight-cards.tsx index 826b2455803f11..b391450964f4d5 100644 --- a/packages/components/src/highlight-cards/annual-highlight-cards.tsx +++ b/packages/components/src/highlight-cards/annual-highlight-cards.tsx @@ -1,8 +1,9 @@ import { comment, Icon, paragraph, people, postContent, starEmpty } from '@wordpress/icons'; import clsx from 'clsx'; -import { useTranslate } from 'i18n-calypso'; +import { translate, useTranslate } from 'i18n-calypso'; import ComponentSwapper from '../component-swapper'; -import CountComparisonCard from './count-comparison-card'; +import CountCard from './count-card'; +import HighlightCardsHeading from './highlight-cards-heading'; import MobileHighlightCardListing from './mobile-highlight-cards'; import './style.scss'; @@ -27,54 +28,33 @@ type AnnualHighlightCardsProps = { navigation?: React.ReactNode; }; -function AnnualHighlightsMobile( { counts }: AnnualHighlightsProps ) { - const translate = useTranslate(); - - const highlights = [ +function getCardProps( counts: AnnualHighlightCounts ) { + return [ { heading: translate( 'Posts' ), count: counts?.posts, icon: postContent }, { heading: translate( 'Words' ), count: counts?.words, icon: paragraph }, { heading: translate( 'Likes' ), count: counts?.likes, icon: starEmpty }, { heading: translate( 'Comments' ), count: counts?.comments, icon: comment }, { heading: translate( 'Subscribers' ), count: counts?.followers, icon: people }, ]; +} - return ; +function AnnualHighlightsMobile( { counts }: AnnualHighlightsProps ) { + return ; } function AnnualHighlightsStandard( { counts }: AnnualHighlightsProps ) { - const translate = useTranslate(); + const props = getCardProps( counts ); return (
    - } - count={ counts?.posts ?? null } - showValueTooltip - /> - } - count={ counts?.words ?? null } - showValueTooltip - /> - } - count={ counts?.likes ?? null } - showValueTooltip - /> - } - count={ counts?.comments ?? null } - showValueTooltip - /> - } - count={ counts?.followers ?? null } - showValueTooltip - /> + { props.map( ( { count, heading, icon }, index ) => ( + } + showValueTooltip + /> + ) ) }
    ); } @@ -89,7 +69,7 @@ export default function AnnualHighlightCards( { const translate = useTranslate(); const header = ( -

    + { year != null && Number.isFinite( year ) ? translate( '%(year)s in review', { args: { year } } ) : translate( 'Year in review' ) }{ ' ' } @@ -100,7 +80,7 @@ export default function AnnualHighlightCards( { ) : null } -

    + ); return ( diff --git a/packages/components/src/highlight-cards/count-card.tsx b/packages/components/src/highlight-cards/count-card.tsx index cc9209af96a97d..eea7423717d285 100644 --- a/packages/components/src/highlight-cards/count-card.tsx +++ b/packages/components/src/highlight-cards/count-card.tsx @@ -1,42 +1,67 @@ -import { Icon } from '@wordpress/icons'; -import { useMemo } from 'react'; -import BaseCard from './base-card'; +import clsx from 'clsx'; +import { useRef, useState } from 'react'; +import { Card } from '../'; +import Popover from '../popover'; +import { formatNumber } from './lib/numbers'; interface CountCardProps { - heading: React.ReactNode; - icon: JSX.Element; - value: number | string; + heading?: React.ReactNode; + icon?: JSX.Element; + note?: string; + showValueTooltip?: boolean; + value: number | string | null; } -function useDisplayValue( value: CountCardProps[ 'value' ] ) { - return useMemo( () => { - if ( typeof value === 'string' ) { - return value; - } - if ( typeof value === 'number' ) { - return value.toLocaleString(); - } - return '-'; - }, [ value ] ); +function TooltipContent( { value }: CountCardProps ) { + return ( +
    + + { formatNumber( value as number, false ) } + +
    + ); } -export default function CountCard( { heading, icon, value }: CountCardProps ) { - const displayValue = useDisplayValue( value ); +export default function CountCard( { + heading, + icon, + note, + value, + showValueTooltip, +}: CountCardProps ) { + const textRef = useRef( null ); + const [ isTooltipVisible, setTooltipVisible ] = useState( false ); + + // Tooltips are used to show the full number instead of the shortened number. + // Non-numeric values are not shown in the tooltip. + const shouldShowTooltip = showValueTooltip && typeof value === 'number'; return ( - -
    - -
    -
    { heading }
    - - } - > -
    - { displayValue } + + { icon &&
    { icon }
    } + { heading &&
    { heading }
    } +
    setTooltipVisible( true ) } + onMouseLeave={ () => setTooltipVisible( false ) } + > + + { typeof value === 'number' ? formatNumber( value, true ) : value } +
    - + { shouldShowTooltip && ( + + + { note &&
    { note }
    } +
    + ) } +
    ); } diff --git a/packages/components/src/highlight-cards/count-comparison-card.tsx b/packages/components/src/highlight-cards/count-comparison-card.tsx index d6a6e7ba3b98e6..64867f34349061 100644 --- a/packages/components/src/highlight-cards/count-comparison-card.tsx +++ b/packages/components/src/highlight-cards/count-comparison-card.tsx @@ -1,37 +1,20 @@ import { arrowDown, arrowUp, Icon } from '@wordpress/icons'; import clsx from 'clsx'; import { useRef, useState } from 'react'; -import { Card, ShortenedNumber, formattedNumber } from '../'; +import { Card } from '../'; import Popover from '../popover'; +import { formatNumber, formatPercentage, subtract, percentCalculator } from './lib/numbers'; type CountComparisonCardProps = { count: number | null; heading?: React.ReactNode; icon?: React.ReactNode; onClick?: ( event: MouseEvent ) => void; - previousCount?: number | null; + previousCount: number | null; showValueTooltip?: boolean | null; - note?: string; compact?: boolean; }; -function subtract( a: number | null, b: number | null | undefined ): number | null { - return a === null || b === null || b === undefined ? null : a - b; -} - -export function percentCalculator( part: number | null, whole: number | null | undefined ) { - if ( part === null || whole === null || whole === undefined ) { - return null; - } - // Handle NaN case. - if ( part === 0 && whole === 0 ) { - return 0; - } - const answer = ( part / whole ) * 100; - // Handle Infinities. - return Math.abs( answer ) === Infinity ? 100 : Math.round( answer ); -} - type TrendComparisonProps = { count: number | null; previousCount?: number | null; @@ -39,49 +22,63 @@ type TrendComparisonProps = { export function TrendComparison( { count, previousCount }: TrendComparisonProps ) { const difference = subtract( count, previousCount ); - const differenceMagnitude = Math.abs( difference as number ); const percentage = Number.isFinite( difference ) ? percentCalculator( Math.abs( difference as number ), previousCount ) : null; - if ( difference === null ) { + // Show nothing if inputs are invalid or if there is no change. + if ( difference === null || difference === 0 ) { return null; } - return ( + return Math.abs( difference ) === 0 ? null : ( 0, } ) } > - + { difference < 0 && } { difference > 0 && } - - { differenceMagnitude <= 9999 && formattedNumber( differenceMagnitude ) } - { differenceMagnitude > 9999 && } - { percentage !== null && ( - ({ percentage }%) + + { ' ' } + { formatPercentage( percentage ) } + ) } ); } +function TooltipContent( { count, previousCount }: CountComparisonCardProps ) { + const difference = subtract( count, previousCount ) as number; + return ( +
    +
    + { formatNumber( count, false ) } + { ' ' } + { difference !== 0 && difference !== null && ( + + ({ formatNumber( difference, false, true ) }) + + ) } +
    +
    + ); +} + export default function CountComparisonCard( { count, previousCount, icon, heading, showValueTooltip, - note = '', compact = false, }: CountComparisonCardProps ) { const textRef = useRef( null ); const [ isTooltipVisible, setTooltipVisible ] = useState( false ); - return ( { icon &&
    { icon }
    } @@ -93,12 +90,8 @@ export default function CountComparisonCard( { onMouseEnter={ () => setTooltipVisible( true ) } onMouseLeave={ () => setTooltipVisible( false ) } > - - + + { formatNumber( count ) } { ' ' } { showValueTooltip && ( @@ -108,14 +101,12 @@ export default function CountComparisonCard( { position="bottom right" context={ textRef.current } > -
    - - { icon && { icon } } - { heading && { heading } } - - { formattedNumber( count ) } -
    - { note &&
    { note }
    } + ) }
    diff --git a/packages/components/src/highlight-cards/highlight-cards-heading.tsx b/packages/components/src/highlight-cards/highlight-cards-heading.tsx new file mode 100644 index 00000000000000..06d8e19480dbdf --- /dev/null +++ b/packages/components/src/highlight-cards/highlight-cards-heading.tsx @@ -0,0 +1,18 @@ +import { useHasEnTranslation } from '@automattic/i18n-utils'; +import { useTranslate } from 'i18n-calypso'; + +export default function HighlightCardsHeading( { children }: { children: React.ReactNode } ) { + const translate = useTranslate(); + const hasEnTranslation = useHasEnTranslation(); + const hasTranslation = hasEnTranslation( 'Updates every 30 minutes' ); + return ( +
    +

    { children }

    + { hasTranslation && ( +
    + { translate( 'Updates every 30 minutes' ) } +
    + ) } +
    + ); +} diff --git a/packages/components/src/highlight-cards/lib/numbers.ts b/packages/components/src/highlight-cards/lib/numbers.ts new file mode 100644 index 00000000000000..45d87b0fc22dee --- /dev/null +++ b/packages/components/src/highlight-cards/lib/numbers.ts @@ -0,0 +1,46 @@ +import importedFormatNumber, { + DEFAULT_LOCALE, + STANDARD_FORMATTING_OPTIONS, + COMPACT_FORMATTING_OPTIONS, + PERCENTAGE_FORMATTING_OPTIONS, +} from '../../number-formatters/lib/format-number'; + +export function formatNumber( number: number | null, isShortened = true, showSign = false ) { + const option = isShortened + ? { ...COMPACT_FORMATTING_OPTIONS } + : { ...STANDARD_FORMATTING_OPTIONS }; + if ( showSign ) { + option.signDisplay = 'exceptZero'; + } + return importedFormatNumber( number, DEFAULT_LOCALE, option ); +} + +export function formatPercentage( + number: number | null, + usePreciseSmallPercentages: boolean = false +) { + // If the number is < 1%, then use 2 significant digits and maximumFractionDigits of 2. + // Otherwise, use the default percentage formatting options. + const option = + usePreciseSmallPercentages && number && number < 0.01 + ? { ...PERCENTAGE_FORMATTING_OPTIONS, maximumFractionDigits: 2, maximumSignificantDigits: 2 } + : PERCENTAGE_FORMATTING_OPTIONS; + return importedFormatNumber( number, DEFAULT_LOCALE, option ); +} + +export function subtract( a: number | null, b: number | null | undefined ): number | null { + return a === null || b === null || b === undefined ? null : a - b; +} + +export function percentCalculator( part: number | null, whole: number | null | undefined ) { + if ( part === null || whole === null || whole === undefined ) { + return null; + } + // Handle NaN case. + if ( part === 0 && whole === 0 ) { + return 0; + } + const answer = part / whole; + // Handle Infinities as 100%. + return ! Number.isFinite( answer ) ? 1 : answer; +} diff --git a/packages/components/src/highlight-cards/mobile-highlight-cards.tsx b/packages/components/src/highlight-cards/mobile-highlight-cards.tsx index 503156171f8270..d2503c6a19cbfc 100644 --- a/packages/components/src/highlight-cards/mobile-highlight-cards.tsx +++ b/packages/components/src/highlight-cards/mobile-highlight-cards.tsx @@ -3,6 +3,8 @@ import ShortenedNumber from '../number-formatters'; import { TrendComparison } from './count-comparison-card'; import './style.scss'; +// TODO: Figure out a way to remove this and make count-comparison-card handle the mobile layout. + type MobileHighlightCardProps = { heading: string; count: number | null; diff --git a/packages/components/src/highlight-cards/stories/annual-highlight-cards.stories.jsx b/packages/components/src/highlight-cards/stories/annual-highlight-cards.stories.jsx new file mode 100644 index 00000000000000..9d44bce50a5eef --- /dev/null +++ b/packages/components/src/highlight-cards/stories/annual-highlight-cards.stories.jsx @@ -0,0 +1,42 @@ +import AnnualCards from '../annual-highlight-cards'; + +export default { + title: 'packages/components/Highlight Cards/AnnualHighlightCards', + component: AnnualCards, + argTypes: { + year: { control: 'number' }, + 'counts.comments': { control: 'number' }, + 'counts.likes': { control: 'number' }, + 'counts.posts': { control: 'number' }, + 'counts.words': { control: 'number' }, + 'counts.followers': { control: 'number' }, + }, +}; + +const Template = ( { year, ...counts } ) => { + const countsObject = { + comments: counts[ 'counts.comments' ], + likes: counts[ 'counts.likes' ], + posts: counts[ 'counts.posts' ], + words: counts[ 'counts.words' ], + followers: counts[ 'counts.followers' ], + }; + + return ( +
    + +
    + ); +}; + +export const AnnualHighlightCards = Template.bind( {} ); +AnnualHighlightCards.args = { + year: 2022, + 'counts.comments': 72490, + 'counts.likes': 12298, + 'counts.posts': 79, + 'counts.words': 205035, + 'counts.followers': 1113323, +}; diff --git a/packages/components/src/highlight-cards/stories/base-card.stories.jsx b/packages/components/src/highlight-cards/stories/base-card.stories.jsx new file mode 100644 index 00000000000000..0e54dd60baa277 --- /dev/null +++ b/packages/components/src/highlight-cards/stories/base-card.stories.jsx @@ -0,0 +1,28 @@ +import BaseCard from '../base-card'; + +export default { + title: 'packages/components/Highlight Cards/BaseCard', + component: BaseCard, + argTypes: { + heading: { control: 'text' }, + body: { control: 'text' }, + }, +}; + +const Template = ( { heading, body } ) => { + return ( +
    + { heading }
    }> +
    { body }
    +
    +
    + ); +}; + +export const BaseCard_ = Template.bind( {} ); +BaseCard_.args = { + heading: 'Customizable Heading', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', +}; diff --git a/packages/components/src/highlight-cards/stories/card.stories.jsx b/packages/components/src/highlight-cards/stories/card.stories.jsx deleted file mode 100644 index 54c24cf265156f..00000000000000 --- a/packages/components/src/highlight-cards/stories/card.stories.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import { institution } from '@wordpress/icons'; -import BaseCard from '../base-card'; -import CountCard from '../count-card'; - -export default { title: 'packages/components/Highlight Card' }; - -export const BaseCard_ = () => ( - <> - With Heading and Body
    }> -
    - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut - labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in - voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat - cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -
    - - -
    - This card only has a body. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis - nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute - irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit - anim id est laborum. -
    -
    - With Only a Heading
    }> - -); - -export const CountCard_ = () => ( - <> - - - -); diff --git a/packages/components/src/highlight-cards/stories/cards.stories.jsx b/packages/components/src/highlight-cards/stories/cards.stories.jsx deleted file mode 100644 index 84c8a486678d31..00000000000000 --- a/packages/components/src/highlight-cards/stories/cards.stories.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import AnnualCards from '../annual-highlight-cards'; -import WeeklyCards from '../weekly-highlight-cards'; - -export default { title: 'packages/components/Highlight Cards' }; - -const handleClick = action( 'click' ); - -const WeeklyVariations = ( props ) => ( - -); - -export const WeeklyHighlights = () => ; - -export const WeeklyHighlightsWithPartialPreviousCounts = () => ( - -); -export const WeeklyHighlightsWithoutPreviousCounts = () => ( - -); - -export const WeeklyHighlightsWithoutCounts = () => ( - -); - -export const AnnualHighlights = () => ( - -); diff --git a/packages/components/src/highlight-cards/stories/count-card.stories.jsx b/packages/components/src/highlight-cards/stories/count-card.stories.jsx new file mode 100644 index 00000000000000..cb5db9f460963e --- /dev/null +++ b/packages/components/src/highlight-cards/stories/count-card.stories.jsx @@ -0,0 +1,36 @@ +import { Icon, institution } from '@wordpress/icons'; +import CountCard from '../count-card'; + +export default { + title: 'packages/components/Highlight Cards/CountCard', + component: CountCard, + argTypes: { + heading: { control: 'text' }, + valueType: { + control: { type: 'radio' }, + options: [ 'number', 'string' ], + }, + stringValue: { control: 'text' }, + numberValue: { control: 'number' }, + }, +}; + +const Template = ( args ) => { + const value = args.valueType === 'string' ? args.stringValue : args.numberValue; + + return ( +
    + } value={ value } /> +
    + ); +}; + +export const CountCard_ = Template.bind( {} ); +CountCard_.args = { + heading: 'Customizable Heading', + valueType: 'number', + stringValue: 'A really long string message that forces the box to expand', + numberValue: 12345, +}; diff --git a/packages/components/src/highlight-cards/stories/count-comparison-card.stories.jsx b/packages/components/src/highlight-cards/stories/count-comparison-card.stories.jsx new file mode 100644 index 00000000000000..708abe3aaf3f71 --- /dev/null +++ b/packages/components/src/highlight-cards/stories/count-comparison-card.stories.jsx @@ -0,0 +1,36 @@ +import { Icon, institution } from '@wordpress/icons'; +import CountComparisonCard from '../count-comparison-card'; + +export default { + title: 'packages/components/Highlight Cards/CountComparisonCard', + component: CountComparisonCard, + argTypes: { + heading: { control: 'text' }, + previousCount: { control: 'number' }, + count: { control: 'number' }, + showValueTooltip: { control: 'boolean' }, + }, +}; + +const Template = ( { count, previousCount, heading } ) => { + return ( +
    + } + count={ count } + previousCount={ previousCount } + /> +
    + ); +}; + +export const CountComparisonCard_ = Template.bind( {} ); +CountComparisonCard_.args = { + heading: 'Customizable Heading', + count: 234567, + previousCount: 123456, + showValueTooltip: false, +}; diff --git a/packages/components/src/highlight-cards/stories/weekly-highlight.stories.jsx b/packages/components/src/highlight-cards/stories/weekly-highlight.stories.jsx new file mode 100644 index 00000000000000..8dbf2785ef4808 --- /dev/null +++ b/packages/components/src/highlight-cards/stories/weekly-highlight.stories.jsx @@ -0,0 +1,71 @@ +import { action } from '@storybook/addon-actions'; +import WeeklyCards from '../weekly-highlight-cards'; + +export default { + title: 'packages/components/Highlight Cards/WeeklyHighlightCards', + component: WeeklyCards, + argTypes: { + 'counts.comments': { control: 'number' }, + 'counts.likes': { control: 'number' }, + 'counts.views': { control: 'number' }, + 'counts.visitors': { control: 'number' }, + 'previousCounts.comments': { control: 'number' }, + 'previousCounts.likes': { control: 'number' }, + 'previousCounts.views': { control: 'number' }, + 'previousCounts.visitors': { control: 'number' }, + 'story.isLoading': { control: 'boolean' }, + showValueTooltip: { control: 'boolean' }, + }, +}; + +const handleClick = action( 'click' ); + +const Template = ( { showValueTooltip, ...args } ) => { + const counts = args[ 'story.isLoading' ] + ? {} + : { + comments: args[ 'counts.comments' ], + likes: args[ 'counts.likes' ], + views: args[ 'counts.views' ], + visitors: args[ 'counts.visitors' ], + }; + + const previousCounts = args[ 'story.isLoading' ] + ? {} + : { + comments: args[ 'previousCounts.comments' ], + likes: args[ 'previousCounts.likes' ], + views: args[ 'previousCounts.views' ], + visitors: args[ 'previousCounts.visitors' ], + }; + + return ( +
    + +
    + ); +}; + +export const WeeklyHighlightCards = Template.bind( {} ); +WeeklyHighlightCards.args = { + 'counts.comments': 45, + 'counts.likes': 0, + 'counts.views': 4673, + 'counts.visitors': 1548, + 'previousCounts.comments': 45, + 'previousCounts.likes': 100, + 'previousCounts.views': 4073, + 'previousCounts.visitors': 1412, + showValueTooltip: true, + 'story.isLoading': false, +}; diff --git a/packages/components/src/highlight-cards/style.scss b/packages/components/src/highlight-cards/style.scss index b973d234480016..4a62347c78c7a5 100644 --- a/packages/components/src/highlight-cards/style.scss +++ b/packages/components/src/highlight-cards/style.scss @@ -133,10 +133,13 @@ $highlight-card-tooltip-font: Inter, $sans !default; .highlight-card { border-color: var(--studio-gray-5); border-radius: 5px; // stylelint-disable-line scales/radii - width: 100%; - min-width: 180px; // Minimum mobile width padding: 16px 24px; margin-bottom: 0; + flex-grow: 1; + flex-shrink: 0; + + // Ensure minimum of ~1:1 aspect ratio. + min-width: 120px; margin-right: 24px; &:last-child { @@ -145,10 +148,6 @@ $highlight-card-tooltip-font: Inter, $sans !default; } } -.highlight-card-icon { - margin-bottom: 24px; -} - .highlight-card-heading { font-weight: 500; line-height: 20px; @@ -159,8 +158,7 @@ $highlight-card-tooltip-font: Inter, $sans !default; .highlight-card-count { align-items: flex-end; display: flex; - flex-wrap: wrap; - font-size: 36px; // stylelint-disable-line declaration-property-unit-allowed-list + font-size: 2.25rem; font-weight: 400; line-height: 40px; @@ -223,7 +221,6 @@ $highlight-card-tooltip-font: Inter, $sans !default; &.highlight-card-popover { // overload tooltip's styles .popover__inner { padding: 16px 24px; - width: 244px; box-sizing: border-box; border-radius: 5px; // stylelint-disable-line scales/radii } @@ -385,6 +382,7 @@ $highlight-card-tooltip-font: Inter, $sans !default; .highlight-card-tooltip-label { display: flex; align-items: center; + margin-right: 1.5em; } .highlight-card-tooltip-icon { fill: var(--studio-white); @@ -393,6 +391,10 @@ $highlight-card-tooltip-font: Inter, $sans !default; } } +.highlight-card-tooltip-count-difference { + color: var(--studio-gray-10); +} + .highlight-card-tooltip-note { font-size: $font-body-extra-small; padding-top: 8px; @@ -431,6 +433,11 @@ $highlight-card-tooltip-font: Inter, $sans !default; padding-right: $font-body-small; } + .highlight-cards-heading__update-frequency { + padding-left: $font-body-small; + padding-right: $font-body-small; + } + // Show count and difference on newlines. .highlight-card-count { align-items: flex-start; @@ -490,3 +497,21 @@ $highlight-card-tooltip-font: Inter, $sans !default; padding-left: 8px; } } + +.highlight-cards-heading__wrapper { + margin-bottom: $header-margin-bottom; + + .highlight-cards-heading { + margin-bottom: 0; + } + + .highlight-cards-heading__update-frequency { + line-height: 16px; + + span { + color: var(--color-text-subtle); + display: inline-block; + font-size: $font-body-small; + } + } +} diff --git a/packages/components/src/highlight-cards/weekly-highlight-cards.tsx b/packages/components/src/highlight-cards/weekly-highlight-cards.tsx index efde912f8334e3..7ea9708335e141 100644 --- a/packages/components/src/highlight-cards/weekly-highlight-cards.tsx +++ b/packages/components/src/highlight-cards/weekly-highlight-cards.tsx @@ -15,6 +15,7 @@ import { eye } from '../icons'; import Popover from '../popover'; import { comparingInfoBarsChart, comparingInfoRangeChart } from './charts'; import CountComparisonCard from './count-comparison-card'; +import HighlightCardsHeading from './highlight-cards-heading'; import MobileHighlightCardListing from './mobile-highlight-cards'; import './style.scss'; @@ -253,7 +254,7 @@ export default function WeeklyHighlightCards( { return (
    -

    + { currentPeriod === PAST_THIRTY_DAYS ? translate( '30-day highlights' ) @@ -321,7 +322,7 @@ export default function WeeklyHighlightCards( { showTooltip={ showSettingsTooltip } /> ) } -

    + - { formatNumber( value ) } - - ); + return { formatNumber( value ) }; } diff --git a/packages/components/src/number-formatters/lib/format-number.ts b/packages/components/src/number-formatters/lib/format-number.ts index 1d4de89e17c753..3ff7bb72ae0f90 100644 --- a/packages/components/src/number-formatters/lib/format-number.ts +++ b/packages/components/src/number-formatters/lib/format-number.ts @@ -4,11 +4,25 @@ const warnOnce = memoize( console.warn ); // eslint-disable-line no-console export const DEFAULT_LOCALE = ( typeof window === 'undefined' ? null : window.navigator?.language ) ?? 'en-US'; -export const DEFAULT_OPTIONS = { - compactDisplay: 'short', - maximumFractionDigits: 1, + +// Preset Options +export const COMPACT_FORMATTING_OPTIONS = { notation: 'compact', + maximumSignificantDigits: 3, + maximumFractionDigits: 1, + compactDisplay: 'short', +} as Intl.NumberFormatOptions; +export const STANDARD_FORMATTING_OPTIONS = { + notation: 'standard', } as Intl.NumberFormatOptions; +export const PERCENTAGE_FORMATTING_OPTIONS = { + style: 'percent', +} as Intl.NumberFormatOptions; + +// Default Options +export const DEFAULT_OPTIONS = { ...COMPACT_FORMATTING_OPTIONS }; +// For backward compatibility; original implementation did not specify max sigfigs. +delete DEFAULT_OPTIONS.maximumSignificantDigits; export default function formatNumber( number: number | null, @@ -24,7 +38,7 @@ export default function formatNumber( // This approach ensures a smooth user experience by avoiding disruption for unaffected users. // Refer to https://github.com/Automattic/wp-calypso/issues/77635 for more details. try { - new Intl.NumberFormat( locale, options ).format( number as number ); + return new Intl.NumberFormat( locale, options ).format( number ); } catch ( error: unknown ) { warnOnce( `formatted-number numberFormat error: Intl.NumberFormat().format( ${ typeof number } )`, @@ -43,7 +57,7 @@ export default function formatNumber( const optionNamesToRemove = [ 'signDisplay', 'compactDisplay' ]; // Create new format options object with problematic parameters removed. - const reducedFormatOptions: Record< string, string > = {}; + const reducedFormatOptions: Record< string, boolean | number | string > = {}; for ( const [ key, value ] of Object.entries( options ) ) { if ( optionsToRemove[ key ] && value === optionsToRemove[ key ] ) { continue; diff --git a/packages/composite-checkout/package.json b/packages/composite-checkout/package.json index cdfee00c1b4002..4da4a0626d4ea7 100644 --- a/packages/composite-checkout/package.json +++ b/packages/composite-checkout/package.json @@ -46,7 +46,7 @@ "devDependencies": { "@automattic/calypso-storybook": "workspace:^", "@automattic/calypso-typescript-config": "workspace:^", - "@automattic/color-studio": "2.6.0", + "@automattic/color-studio": "^3.0.1", "@storybook/cli": "^7.6.19", "@storybook/react": "^7.6.19", "@testing-library/dom": "^10.1.0", diff --git a/packages/composite-checkout/src/lib/swatches.ts b/packages/composite-checkout/src/lib/swatches.ts index 691333e7151fc7..7fca61a9a26a0f 100644 --- a/packages/composite-checkout/src/lib/swatches.ts +++ b/packages/composite-checkout/src/lib/swatches.ts @@ -1,15 +1,4 @@ export type Swatches = { - wordpressBlue5: string; - wordpressBlue40: string; - wordpressBlue50: string; - wordpressBlue60: string; - wordpressBlue80: string; - blue5: string; - blue30: string; - blue40: string; - blue50: string; - blue60: string; - blue80: string; gray0: string; gray5: string; gray10: string; @@ -28,17 +17,6 @@ export type Swatches = { }; export const swatches: Swatches = { - wordpressBlue5: '#BEDAE6', - wordpressBlue40: '#187AA2', - wordpressBlue50: '#006088', - wordpressBlue60: '#004E6E', - wordpressBlue80: '#002C40', - blue5: '#BBE0FA', - blue30: '#399CE3', - blue40: '#1689DB', - blue50: '#0675C4', - blue60: '#055D9C', - blue80: '#02395C', gray0: '#F6F7F7', gray5: '#DCDCDE', gray10: '#C3C4C7', diff --git a/packages/composite-checkout/src/lib/theme.ts b/packages/composite-checkout/src/lib/theme.ts index 56d6f54abae27f..0a0d5b9913261e 100644 --- a/packages/composite-checkout/src/lib/theme.ts +++ b/packages/composite-checkout/src/lib/theme.ts @@ -60,12 +60,12 @@ const theme: Theme = { colors: { background: swatches.gray0, surface: swatches.white, - primary: colorStudio.colors[ 'Blue 50' ], + primary: colorStudio.colors[ 'WordPress Blue 50' ], primaryBorder: swatches.pink80, - primaryOver: colorStudio.colors[ 'Blue 60' ], - highlight: swatches.blue50, - highlightBorder: swatches.blue80, - highlightOver: swatches.blue60, + primaryOver: colorStudio.colors[ 'WordPress Blue 60' ], + highlight: colorStudio.colors[ 'WordPress Blue 50' ], + highlightBorder: colorStudio.colors[ 'WordPress Blue 80' ], + highlightOver: colorStudio.colors[ 'WordPress Blue 60' ], success: colorStudio.colors[ 'Green 30' ], discount: colorStudio.colors[ 'Green 30' ], disabledPaymentButtons: colorStudio.colors[ 'Gray 5' ], @@ -81,7 +81,7 @@ const theme: Theme = { textColorDisabled: colorStudio.colors[ 'Gray 10' ], error: swatches.red50, warningBackground: swatches.red0, - outline: swatches.blue30, + outline: colorStudio.colors[ 'WordPress Blue 30' ], applePayButtonColor: swatches.black, applePayButtonRollOverColor: swatches.gray80, noticeBackground: swatches.gray80, diff --git a/packages/data-stores/src/onboard/actions.ts b/packages/data-stores/src/onboard/actions.ts index 3f77e7e2faa7da..1076baeec78e2a 100644 --- a/packages/data-stores/src/onboard/actions.ts +++ b/packages/data-stores/src/onboard/actions.ts @@ -412,6 +412,13 @@ export const setDomainCartItem = ( domainCartItem: MinimalRequestCartProduct | u domainCartItem, } ); +export const setDomainCartItems = ( + domainCartItems: MinimalRequestCartProduct[] | undefined +) => ( { + type: 'SET_DOMAIN_CART_ITEMS' as const, + domainCartItems, +} ); + export const setDomainsTransferData = ( bulkDomainsData: DomainTransferData | undefined ) => ( { type: 'SET_DOMAINS_TRANSFER_DATA' as const, bulkDomainsData, @@ -501,6 +508,7 @@ export type OnboardAction = ReturnType< | typeof resetSelectedDesign | typeof setDomainForm | typeof setDomainCartItem + | typeof setDomainCartItems | typeof setSiteDescription | typeof setSiteLogo | typeof setSiteAccentColor diff --git a/packages/data-stores/src/onboard/reducer.ts b/packages/data-stores/src/onboard/reducer.ts index f7723270f38020..ba69990aeb87b7 100644 --- a/packages/data-stores/src/onboard/reducer.ts +++ b/packages/data-stores/src/onboard/reducer.ts @@ -455,6 +455,20 @@ const domainCartItem: Reducer< MinimalRequestCartProduct | undefined, OnboardAct return state; }; +const domainCartItems: Reducer< MinimalRequestCartProduct[] | undefined, OnboardAction > = ( + state = undefined, + action +) => { + if ( action.type === 'SET_DOMAIN_CART_ITEMS' ) { + return action.domainCartItems; + } + if ( action.type === 'RESET_ONBOARD_STORE' ) { + return undefined; + } + + return state; +}; + const isMigrateFromWp: Reducer< boolean, OnboardAction > = ( state = false, action ) => { if ( action.type === 'SET_IS_MIGRATE_FROM_WP' ) { return action.isMigrateFromWp; @@ -617,6 +631,7 @@ const reducer = combineReducers( { planCartItem, productCartItems, isMigrateFromWp, + domainCartItems, pluginsToVerify, profilerData, paidSubscribers, diff --git a/packages/data-stores/src/onboard/selectors.ts b/packages/data-stores/src/onboard/selectors.ts index 77e93abe2042e9..897e4d6cf9c416 100644 --- a/packages/data-stores/src/onboard/selectors.ts +++ b/packages/data-stores/src/onboard/selectors.ts @@ -72,6 +72,7 @@ export const hasSelectedDesign = ( state: State ) => !! state.selectedDesign; export const getDomainForm = ( state: State ) => state.domainForm; export const getDomainCartItem = ( state: State ) => state.domainCartItem; +export const getDomainCartItems = ( state: State ) => state.domainCartItems; export const getHideFreePlan = ( state: State ) => state.hideFreePlan; export const getHidePlansFeatureComparison = ( state: State ) => state.hidePlansFeatureComparison; export const getIsMigrateFromWp = ( state: State ) => state.isMigrateFromWp; diff --git a/packages/data-stores/src/purchases/lib/assembler.ts b/packages/data-stores/src/purchases/lib/assembler.ts index b392c65a29d5f9..49157f9ed00ad6 100644 --- a/packages/data-stores/src/purchases/lib/assembler.ts +++ b/packages/data-stores/src/purchases/lib/assembler.ts @@ -126,6 +126,7 @@ export function createPurchaseObject( purchase: RawPurchase | RawPurchaseCreditC object.payment.creditCard = { id: Number( purchase.payment_card_id ), type: purchase.payment_card_type, + displayBrand: purchase.payment_card_display_brand, processor: purchase.payment_card_processor, number: purchase.payment_details, expiryDate: purchase.payment_expiry, diff --git a/packages/data-stores/src/purchases/types.ts b/packages/data-stores/src/purchases/types.ts index 956a511210f577..b6e852fe7c9031 100644 --- a/packages/data-stores/src/purchases/types.ts +++ b/packages/data-stores/src/purchases/types.ts @@ -224,6 +224,7 @@ export interface RawPurchase { | 'emergent-paywall' | 'brazil-tef' | string; + payment_card_display_brand: string | null; payment_country_name: string; payment_country_code: string | null; stored_details_id: string | null; @@ -269,6 +270,7 @@ export interface RawPurchase { export type RawPurchaseCreditCard = RawPurchase & { payment_type: 'credit_card'; payment_card_type: string; + payment_card_display_brand: string | null; payment_card_processor: string; payment_details: string | number; payment_expiry: string; @@ -337,12 +339,14 @@ export type PurchasePaymentWithCreditCard = PurchasePayment & { countryName: string | undefined; storedDetailsId: string | number; type: string; + displayBrand: string | null; creditCard: PurchasePaymentCreditCard; }; export interface PurchasePaymentCreditCard { id: number; type: string; + displayBrand: string | null; processor: string; number: string; expiryDate: string; diff --git a/packages/data-stores/src/subscriber/actions.ts b/packages/data-stores/src/subscriber/actions.ts index b38dfcc28ced37..d5c9889e314a54 100644 --- a/packages/data-stores/src/subscriber/actions.ts +++ b/packages/data-stores/src/subscriber/actions.ts @@ -39,7 +39,12 @@ export function createActions() { job, } ); - function* importCsvSubscribers( siteId: number, file?: File, emails: string[] = [] ) { + function* importCsvSubscribers( + siteId: number, + file?: File, + emails: string[] = [], + parseOnly: boolean = false + ) { yield importCsvSubscribersStart( siteId, file, emails ); try { @@ -52,7 +57,7 @@ export function createActions() { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore formData: file && [ [ 'import', file, file.name ] ], - body: { emails }, + body: { emails, parse_only: parseOnly }, } ); yield importCsvSubscribersStartSuccess( siteId, data.upload_id ); diff --git a/packages/design-picker/src/components/assets/images/assembler-illustration-v2.png b/packages/design-picker/src/components/assets/images/assembler-illustration-v2.png new file mode 100644 index 00000000000000..07a45387827fad Binary files /dev/null and b/packages/design-picker/src/components/assets/images/assembler-illustration-v2.png differ diff --git a/packages/design-picker/src/components/pattern-assembler-cta/index.tsx b/packages/design-picker/src/components/pattern-assembler-cta/index.tsx index 7705d4443fd38b..6c27a900e04340 100644 --- a/packages/design-picker/src/components/pattern-assembler-cta/index.tsx +++ b/packages/design-picker/src/components/pattern-assembler-cta/index.tsx @@ -2,7 +2,7 @@ import { Button } from '@automattic/components'; import { useViewportMatch } from '@wordpress/compose'; import { useTranslate } from 'i18n-calypso'; import { ReactNode } from 'react'; -import assemblerIllustrationImage from '../assets/images/assembler-illustration.png'; +import assemblerIllustrationV2Image from '../assets/images/assembler-illustration-v2.png'; import './style.scss'; type PatternAssemblerCtaData = { @@ -49,7 +49,7 @@ const PatternAssemblerCta = ( { onButtonClick }: PatternAssemblerCtaProps ) => {
    Pattern Assembler
    diff --git a/packages/design-picker/src/components/pattern-assembler-cta/style.scss b/packages/design-picker/src/components/pattern-assembler-cta/style.scss index 2a79ca5aeb1bc4..c4b3fb695041a1 100644 --- a/packages/design-picker/src/components/pattern-assembler-cta/style.scss +++ b/packages/design-picker/src/components/pattern-assembler-cta/style.scss @@ -132,7 +132,6 @@ } .pattern-assembler-cta__image-wrapper { - padding-left: 35px; text-align: right; } diff --git a/packages/design-picker/src/components/style.scss b/packages/design-picker/src/components/style.scss index b2d43f32ce0b70..e8c3d3c3368e29 100644 --- a/packages/design-picker/src/components/style.scss +++ b/packages/design-picker/src/components/style.scss @@ -310,20 +310,18 @@ } .theme-card { - &:hover, - &:focus-within { - .theme-card__image-container { - border-color: #a7aaad; + .theme-card--is-active { + &:hover, + &:focus-within { + .theme-card__image-container { + border-color: #a7aaad; + } } } - - .theme-card__image-container { - border-color: rgba(0, 0, 0, 0.12); - } } .design-button-container .design-picker__design-option .design-picker__image-frame:hover::after, - .theme-card .theme-card__image:hover::after { + .theme-card--is-actionable .theme-card__image:hover::after { background-color: rgba(255, 255, 255, 0.72); } } diff --git a/packages/design-picker/src/components/theme-card/index.tsx b/packages/design-picker/src/components/theme-card/index.tsx index 619b5e7c903516..ad829efea43559 100644 --- a/packages/design-picker/src/components/theme-card/index.tsx +++ b/packages/design-picker/src/components/theme-card/index.tsx @@ -29,6 +29,28 @@ interface ThemeCardProps { onStyleVariationMoreClick?: () => void; } +const ActiveBadge = () => { + return ( +
    +
    + + + + + + + + + + { translate( 'Active', { + context: 'singular noun, the currently active theme', + } ) } + +
    +
    + ); +}; + const ThemeCard = forwardRef( ( { @@ -123,14 +145,7 @@ const ThemeCard = forwardRef(

    { name }

    - { isActive && ( - - { translate( 'Active', { - context: 'singular noun, the currently active theme', - } ) } - - ) } - { ! isActive && styleVariations.length > 0 && ( + { ! optionsMenu && styleVariations.length > 0 && (
    { badge } } { optionsMenu &&
    { optionsMenu }
    } + { isActive && }
    diff --git a/packages/design-picker/src/components/theme-card/style.scss b/packages/design-picker/src/components/theme-card/style.scss index 2d6277a9260a8a..a6495e24ec2139 100644 --- a/packages/design-picker/src/components/theme-card/style.scss +++ b/packages/design-picker/src/components/theme-card/style.scss @@ -14,28 +14,8 @@ $theme-card-info-margin-top: 16px; &--is-active { .theme-card__image-container { - border: 0; - } - - .theme-card__info { - align-items: center; - background: var(--color-primary); - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; - box-sizing: initial; - flex-direction: row; - gap: 16px; - margin-top: 0; - padding: 8px 24px; - - .theme-card__info-options, - .theme-card__info-options .theme__more-button { - position: relative; - } - } - - .theme-card__info-title { - color: var(--color-text-inverted); + box-shadow: 0 0 0 2px var(--color-primary); + border-radius: 4px; } } @@ -44,6 +24,23 @@ $theme-card-info-margin-top: 16px; cursor: pointer; } } + + .theme-card__info-badge-container { + display: flex; + flex-basis: 100%; + } + + .theme-card__info-badge-active { + display: flex; + background-color: var(--color-primary); + color: var(--studio-white); + font-weight: 400; + + svg { + margin-right: 3px; + margin-top: 1px; + } + } } .theme-card__content { @@ -60,6 +57,24 @@ $theme-card-info-margin-top: 16px; position: relative; } +.theme-card--is-actionable { + .theme-card__image { + &:hover, + &:focus { + opacity: 0.9; + + .theme-card__image-label { + opacity: 1; + animation: theme-card__image-label 150ms ease-in-out; + } + } + + .accessible-focus &:focus &-label { + box-shadow: 0 0 0 2px var(--color-primary-light); + } + } +} + .theme-card__image { cursor: default; height: 100%; @@ -70,20 +85,6 @@ $theme-card-info-margin-top: 16px; transition: all 200ms ease-in-out; width: 100%; - &:hover, - &:focus { - opacity: 0.9; - - .theme-card__image-label { - opacity: 1; - animation: theme-card__image-label 150ms ease-in-out; - } - } - - .accessible-focus &:focus &-label { - box-shadow: 0 0 0 2px var(--color-primary-light); - } - &-label { background: var(--color-surface); border: 1px solid var(--color-neutral-0); @@ -293,7 +294,6 @@ $theme-card-info-margin-top: 16px; .theme-card__info-options, .theme-card__info-options .theme__more-button { border: 0; - bottom: 0; display: flex; flex: 0 0 auto; height: 20px; diff --git a/packages/design-picker/src/components/unified-design-picker.tsx b/packages/design-picker/src/components/unified-design-picker.tsx index 466c57cb0cdc00..610f86b57e5ef4 100644 --- a/packages/design-picker/src/components/unified-design-picker.tsx +++ b/packages/design-picker/src/components/unified-design-picker.tsx @@ -178,6 +178,7 @@ interface DesignCardProps { onPreview: ( design: Design, variation?: StyleVariation ) => void; getBadge: ( themeId: string, isLockedStyleVariation: boolean ) => React.ReactNode; oldHighResImageLoading?: boolean; // Temporary for A/B test. + isActive: boolean; } const DesignCard: React.FC< DesignCardProps > = ( { @@ -190,6 +191,7 @@ const DesignCard: React.FC< DesignCardProps > = ( { onPreview, getBadge, oldHighResImageLoading, + isActive, } ) => { const [ selectedStyleVariation, setSelectedStyleVariation ] = useState< StyleVariation >(); @@ -203,6 +205,11 @@ const DesignCard: React.FC< DesignCardProps > = ( { shouldLimitGlobalStyles, } ); + const conditionalProps = + ! isLocked && isActive + ? {} + : { onImageClick: () => onPreview( design, selectedStyleVariation ) }; + return ( = ( { badge={ getBadge( design.slug, isLocked ) } styleVariations={ style_variations } selectedStyleVariation={ selectedStyleVariation } - onImageClick={ () => onPreview( design, selectedStyleVariation ) } onStyleVariationClick={ ( variation ) => { onChangeVariation( design, variation ); setSelectedStyleVariation( variation ); } } onStyleVariationMoreClick={ () => onPreview( design ) } + isActive={ isActive && ! isLocked } + { ...conditionalProps } /> ); }; @@ -244,6 +252,8 @@ interface DesignPickerProps { getBadge: ( themeId: string, isLockedStyleVariation: boolean ) => React.ReactNode; oldHighResImageLoading?: boolean; // Temporary for A/B test isSiteAssemblerEnabled?: boolean; // Temporary for A/B test + siteActiveTheme?: string | null; + showActiveThemeBadge?: boolean; } const DesignPicker: React.FC< DesignPickerProps > = ( { @@ -259,6 +269,8 @@ const DesignPicker: React.FC< DesignPickerProps > = ( { getBadge, oldHighResImageLoading, isSiteAssemblerEnabled, + siteActiveTheme = null, + showActiveThemeBadge = false, } ) => { const hasCategories = !! Object.keys( categorization?.categories || {} ).length; const filteredDesigns = useMemo( () => { @@ -269,6 +281,8 @@ const DesignPicker: React.FC< DesignPickerProps > = ( { return designs; }, [ designs, categorization?.selection ] ); + // Pick design + const assemblerCtaData = usePatternAssemblerCtaData(); return ( @@ -312,6 +326,7 @@ const DesignPicker: React.FC< DesignPickerProps > = ( { onPreview={ onPreview } getBadge={ getBadge } oldHighResImageLoading={ oldHighResImageLoading } + isActive={ showActiveThemeBadge && design.recipe?.stylesheet === siteActiveTheme } /> ); } ) } @@ -338,6 +353,8 @@ export interface UnifiedDesignPickerProps { getBadge: ( themeId: string, isLockedStyleVariation: boolean ) => React.ReactNode; oldHighResImageLoading?: boolean; // Temporary for A/B test isSiteAssemblerEnabled?: boolean; // Temporary for A/B test + siteActiveTheme?: string | null; + showActiveThemeBadge?: boolean; } const UnifiedDesignPicker: React.FC< UnifiedDesignPickerProps > = ( { @@ -355,6 +372,8 @@ const UnifiedDesignPicker: React.FC< UnifiedDesignPickerProps > = ( { getBadge, oldHighResImageLoading, isSiteAssemblerEnabled, + siteActiveTheme = null, + showActiveThemeBadge = false, } ) => { const hasCategories = !! Object.keys( categorization?.categories || {} ).length; @@ -389,6 +408,8 @@ const UnifiedDesignPicker: React.FC< UnifiedDesignPickerProps > = ( { getBadge={ getBadge } oldHighResImageLoading={ oldHighResImageLoading } isSiteAssemblerEnabled={ isSiteAssemblerEnabled } + siteActiveTheme={ siteActiveTheme } + showActiveThemeBadge={ showActiveThemeBadge } /> { bottomAnchorContent }
  • diff --git a/packages/design-picker/src/index.ts b/packages/design-picker/src/index.ts index 4265681ecdf791..0b65e462c6e192 100644 --- a/packages/design-picker/src/index.ts +++ b/packages/design-picker/src/index.ts @@ -1,6 +1,7 @@ export { default } from './components'; export { default as themesIllustrationImage } from './components/assets/images/themes-illustration.png'; export { default as assemblerIllustrationImage } from './components/assets/images/assembler-illustration.png'; +export { default as assemblerIllustrationV2Image } from './components/assets/images/assembler-illustration-v2.png'; export { default as hiBigSky } from './components/assets/images/hi-big-sky.png'; export { default as bigSkyModalHeader } from './components/assets/images/big-sky-interstitial-800.png'; export { default as FeaturedPicksButtons } from './components/featured-picks-buttons'; diff --git a/packages/help-center/src/components/help-center-container.tsx b/packages/help-center/src/components/help-center-container.tsx index 27c782aafa5210..c9fce5a6968f1e 100644 --- a/packages/help-center/src/components/help-center-container.tsx +++ b/packages/help-center/src/components/help-center-container.tsx @@ -59,12 +59,6 @@ const HelpCenterContainer: React.FC< Container > = ( { recordTracksEvent( `calypso_inlinehelp_close` ); }, [ handleClose ] ); - const animationProps = { - style: { - ...openingCoordinates, - }, - }; - const focusReturnRef = useFocusReturn(); const cardMergeRefs = useMergeRefs( [ nodeRef, focusReturnRef ] ); @@ -99,7 +93,7 @@ const HelpCenterContainer: React.FC< Container > = ( { handle=".help-center__container-header" bounds="body" > - + setIsMinimized( true ) } diff --git a/packages/help-center/src/components/help-center-header.scss b/packages/help-center/src/components/help-center-header.scss index 3c6a417084d927..24dea0f3d70e16 100644 --- a/packages/help-center/src/components/help-center-header.scss +++ b/packages/help-center/src/components/help-center-header.scss @@ -59,7 +59,7 @@ display: inline-block; margin-left: 8px; padding: 2px 8px; - background: var(--studio-pink-50); + background: var(--color-masterbar-unread-dot-background); border-radius: 50%; font-size: $font-body-extra-small; color: #fff; diff --git a/packages/help-center/src/components/help-center-more-resources.scss b/packages/help-center/src/components/help-center-more-resources.scss index 412df9c6e3f346..4c66331926a464 100644 --- a/packages/help-center/src/components/help-center-more-resources.scss +++ b/packages/help-center/src/components/help-center-more-resources.scss @@ -11,7 +11,7 @@ button { &.help-center-more-resources__institution { > svg:first-child { - fill: var(--studio-pink-50); + fill: var(--studio-green); } } diff --git a/packages/help-center/src/hooks/use-context-based-search-mapping.tsx b/packages/help-center/src/hooks/use-context-based-search-mapping.tsx index bcc1fc3bc1bea1..e317e5c5dfc8b1 100644 --- a/packages/help-center/src/hooks/use-context-based-search-mapping.tsx +++ b/packages/help-center/src/hooks/use-context-based-search-mapping.tsx @@ -1,5 +1,5 @@ import { useSelect } from '@wordpress/data'; -import urlMapping from '../route-to-query-mapping.json'; +import { useQueryForRoute } from '../route-to-query-mapping'; interface CoreBlockEditor { getSelectedBlock: () => object; @@ -25,15 +25,7 @@ export function useContextBasedSearchMapping( currentRoute: string | undefined ) return ''; }, [] ); - // Fuzzier matches - const urlMatchKey = Object.keys( urlMapping ).find( ( key ) => currentRoute?.startsWith( key ) ); - const urlSearchQuery = urlMatchKey ? urlMapping[ urlMatchKey as keyof typeof urlMapping ] : ''; - - // Find exact URL matches - const exactMatch = urlMapping[ currentRoute as keyof typeof urlMapping ]; - if ( exactMatch ) { - return { contextSearch: exactMatch }; - } + const urlSearchQuery = useQueryForRoute( currentRoute ?? '' ); return { contextSearch: blockSearchQuery || urlSearchQuery || '', diff --git a/packages/help-center/src/route-to-query-mapping.json b/packages/help-center/src/route-to-query-mapping.json deleted file mode 100644 index e564837045be8e..00000000000000 --- a/packages/help-center/src/route-to-query-mapping.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "/add-ons/": "add-ons", - "/comments/": "comments", - "/plugins/manage": "manage plugins", - "/plugins": "plugins", - "/plans/": "upgrade plan", - "/email/": "manage emails", - "/woocommerce": "woocommerce", - "/wp-admin/admin.php?page=wc": "woocommerce", - "/subscribers": "subscribers", - "/me/privacy": "privacy", - "/me/notifications": "notification settings", - "/me/site-blocks": "blocked sites", - "/me/get-apps": "wordpress apps", - "/settings/writing/": "writing settings", - "/settings/reading/": "reading settings", - "/settings/performance/": "performance settings", - "/settings/taxonomies/category/": "site categories", - "/settings/taxonomies/post_tag/": "post tag", - "/settings/podcasting/": "podcasting", - "/hosting-config/": "hosting configuration", - "/wp-admin/options-media.php": "media settings", - "/wp-admin/edit.php?post_type=jetpack-testimonial": "testimonials", - "/wp-admin/edit.php?post_type=feedback": "feedback form", - "/wp-admin/post-new.php?post_type=jetpack-testimonial": "new testimonial", - "/wp-admin/admin.php?page=akismet-key-config": "site spam", - "/wp-admin/admin.php?page=jetpack-search": "jetpack search", - "/wp-admin/admin.php?page=polls": "crowdsignal", - "/wp-admin/admin.php?page=ratings": "ratings", - "/wp-admin/options-general.php?page=debug-bar-extender": "debug bar extender", - "/wp-admin/index.php?page=my-blogs": "my sites", - "/read/conversations": "conversations", - "/read/notifications": "notifications", - "/read/subscriptions": "manage subscriptions", - "/read/list": "reader list", - "/read/search": "search", - "/read": "reader", - "/discover": "discover blogs", - "/tags": "tags", - "/sites": "manage sites", - "/marketing/sharing-buttons/": "social share", - "/marketing/business-tools/": "business tools", - "/advertising/": "advertising" -} diff --git a/packages/help-center/src/route-to-query-mapping.ts b/packages/help-center/src/route-to-query-mapping.ts new file mode 100644 index 00000000000000..1bc33893436e6b --- /dev/null +++ b/packages/help-center/src/route-to-query-mapping.ts @@ -0,0 +1,58 @@ +import { __ } from '@wordpress/i18n'; + +export const useQueryForRoute = ( currentRoute: string ) => { + const urlMapping = { + '/add-ons/': __( 'add-ons' ), + '/advertising/': __( 'advertising' ), + '/comments/': __( 'comments' ), + '/discover': __( 'discover blogs' ), + '/email/': __( 'manage emails' ), + '/hosting-config/': __( 'hosting configuration' ), + '/marketing/business-tools/': __( 'business tools' ), + '/marketing/sharing-buttons/': __( 'social share' ), + '/me/get-apps': __( 'wordpress apps' ), + '/me/notifications': __( 'notification settings' ), + '/me/privacy': __( 'privacy' ), + '/me/site-blocks': __( 'blocked sites' ), + '/plans/': __( 'upgrade plan' ), + '/plugins': __( 'plugins' ), + '/plugins/manage': __( 'manage plugins' ), + '/read': __( 'reader' ), + '/read/conversations': __( 'conversations' ), + '/read/list': __( 'reader list' ), + '/read/notifications': __( 'notifications' ), + '/read/search': __( 'search' ), + '/read/subscriptions': __( 'manage subscriptions' ), + '/settings/performance/': __( 'performance settings' ), + '/settings/podcasting/': __( 'podcasting' ), + '/settings/reading/': __( 'reading settings' ), + '/settings/taxonomies/category/': __( 'site categories' ), + '/settings/taxonomies/post_tag/': __( 'post tag' ), + '/settings/writing/': __( 'writing settings' ), + '/sites': __( 'manage sites' ), + '/subscribers': __( 'subscribers' ), + '/tags': __( 'tags' ), + '/woocommerce': __( 'woocommerce' ), + '/wp-admin/admin.php?page=akismet-key-config': __( 'site spam' ), + '/wp-admin/admin.php?page=jetpack-search': __( 'jetpack search' ), + '/wp-admin/admin.php?page=polls': __( 'crowdsignal' ), + '/wp-admin/admin.php?page=ratings': __( 'ratings' ), + '/wp-admin/admin.php?page=wc': __( 'woocommerce' ), + '/wp-admin/edit.php?post_type=feedback': __( 'feedback form' ), + '/wp-admin/edit.php?post_type=jetpack-testimonial': __( 'testimonials' ), + '/wp-admin/index.php?page=my-blogs': __( 'my sites' ), + '/wp-admin/options-general.php?page=debug-bar-extender': __( 'debug bar extender' ), + '/wp-admin/options-media.php': __( 'media settings' ), + '/wp-admin/post-new.php?post_type=jetpack-testimonial': __( 'new testimonial' ), + }; + + // Find exact URL matches + const exactMatch = urlMapping[ currentRoute as keyof typeof urlMapping ]; + if ( exactMatch ) { + return exactMatch; + } + + // Fuzzier matches + const urlMatchKey = Object.keys( urlMapping ).find( ( key ) => currentRoute?.startsWith( key ) ); + return urlMatchKey ? urlMapping[ urlMatchKey as keyof typeof urlMapping ] : ''; +}; diff --git a/packages/odie-client/src/components/send-message-input/style.scss b/packages/odie-client/src/components/send-message-input/style.scss index 2f725338917916..afccba74346488 100644 --- a/packages/odie-client/src/components/send-message-input/style.scss +++ b/packages/odie-client/src/components/send-message-input/style.scss @@ -26,6 +26,10 @@ top: 20px; } +.odie-send-message-input-container textarea { + font-family: inherit; +} + .odie-send-message-input-container .odie-send-message-inner-button { background: none; border: none; diff --git a/packages/onboarding/src/cart/index.tsx b/packages/onboarding/src/cart/index.tsx index 7fe611ee712e1e..b0cf608ce03030 100644 --- a/packages/onboarding/src/cart/index.tsx +++ b/packages/onboarding/src/cart/index.tsx @@ -144,8 +144,8 @@ export const createSiteWithCart = async ( siteAccentColor: string, useThemeHeadstart: boolean, username: string, + domainCartItems: MinimalRequestCartProduct[], domainItem?: DomainSuggestion, - domainCartItem?: MinimalRequestCartProduct, sourceSlug?: string ) => { const siteUrl = domainItem?.domain_name; @@ -213,14 +213,18 @@ export const createSiteWithCart = async ( await setupSiteAfterCreation( { siteId, flowName } ); } - await processItemCart( - siteSlug, - isFreeThemePreselected, - themeSlugWithRepo, - flowName, - userIsLoggedIn, - domainCartItem - ); + if ( domainCartItems.length ) { + for ( const domainCartItem of domainCartItems ) { + await processItemCart( + siteSlug, + isFreeThemePreselected, + themeSlugWithRepo, + flowName, + userIsLoggedIn, + domainCartItem + ); + } + } return providedDependencies; }; diff --git a/packages/onboarding/src/step-container/index.tsx b/packages/onboarding/src/step-container/index.tsx index 7f648529c59ba3..37ae351ab5cfc1 100644 --- a/packages/onboarding/src/step-container/index.tsx +++ b/packages/onboarding/src/step-container/index.tsx @@ -47,6 +47,7 @@ interface Props { showFooterWooCommercePowered?: boolean; showSenseiPowered?: boolean; showVideoPressPowered?: boolean; + backUrl?: string; } const StepContainer: React.FC< Props > = ( { @@ -75,6 +76,7 @@ const StepContainer: React.FC< Props > = ( { isExternalBackUrl, isLargeSkipLayout, customizedActionButtons, + backUrl, goBack, goNext, flowName, @@ -109,13 +111,14 @@ const StepContainer: React.FC< Props > = ( { function BackButton() { // Hide back button if goBack is falsy, it won't do anything in that case. - if ( shouldHideNavButtons || ! goBack ) { + if ( shouldHideNavButtons || ( ! goBack && ! backUrl ) ) { return null; } return ( void; } @@ -27,6 +28,7 @@ const StepNavigationLink: React.FC< Props > = ( { cssClass, rel, recordClick, + backUrl, } ) => { const translate = useTranslate(); @@ -59,6 +61,7 @@ const StepNavigationLink: React.FC< Props > = ( { borderless={ borderless } className={ buttonClasses } onClick={ onClick } + href={ backUrl } rel={ rel } > { backGridicon } diff --git a/packages/page-pattern-modal/package.json b/packages/page-pattern-modal/package.json index fe217d95648013..347ad6376d2336 100644 --- a/packages/page-pattern-modal/package.json +++ b/packages/page-pattern-modal/package.json @@ -27,7 +27,7 @@ ], "types": "dist/types", "dependencies": { - "@automattic/color-studio": "2.6.0", + "@automattic/color-studio": "^3.0.1", "@automattic/typography": "1.0.0", "@wordpress/base-styles": "5.2.0", "@wordpress/block-editor": "^13.2.0", diff --git a/packages/plans-grid-next/src/components/shared/header-price/index.tsx b/packages/plans-grid-next/src/components/shared/header-price/index.tsx index 45006ecfab3108..d33f344b935045 100644 --- a/packages/plans-grid-next/src/components/shared/header-price/index.tsx +++ b/packages/plans-grid-next/src/components/shared/header-price/index.tsx @@ -39,7 +39,11 @@ const HeaderPrice = ( { planSlug, visibleGridPlans }: HeaderPriceProps ) => { ); const { prices } = usePlanPricingInfoFromGridPlans( { gridPlans: visibleGridPlans } ); - const isLargeCurrency = useIsLargeCurrency( { prices, currencyCode: currencyCode || 'USD' } ); + const isLargeCurrency = useIsLargeCurrency( { + prices, + currencyCode: currencyCode || 'USD', + ignoreWhitespace: true, + } ); if ( isWpcomEnterpriseGridPlan( planSlug ) || ! isPricedPlan ) { return null; diff --git a/packages/plans-grid-next/src/hooks/data-store/types.ts b/packages/plans-grid-next/src/hooks/data-store/types.ts index 387929966d5d52..e521bbe667e669 100644 --- a/packages/plans-grid-next/src/hooks/data-store/types.ts +++ b/packages/plans-grid-next/src/hooks/data-store/types.ts @@ -17,7 +17,6 @@ export interface UseGridPlansParams { selectedFeature?: string | null; selectedPlan?: PlanSlug; showLegacyStorageFeature?: boolean; - forceDefaultIntent?: boolean; siteId?: number | null; storageAddOns: ( AddOnMeta | null )[]; term?: ( typeof TERMS_LIST )[ number ]; // defaults to monthly diff --git a/packages/plans-grid-next/src/hooks/data-store/use-grid-plans-for-comparison-grid.ts b/packages/plans-grid-next/src/hooks/data-store/use-grid-plans-for-comparison-grid.ts index 528304910dd3d6..ae05b1aaa38a8d 100644 --- a/packages/plans-grid-next/src/hooks/data-store/use-grid-plans-for-comparison-grid.ts +++ b/packages/plans-grid-next/src/hooks/data-store/use-grid-plans-for-comparison-grid.ts @@ -27,7 +27,6 @@ const useGridPlansForComparisonGrid = ( { term, useCheckPlanAvailabilityForPurchase, useFreeTrialPlanSlugs, - forceDefaultIntent, }: UseGridPlansParams ): GridPlan[] | null => { const gridPlans = useGridPlans( { allFeaturesList, @@ -45,7 +44,6 @@ const useGridPlansForComparisonGrid = ( { term, useCheckPlanAvailabilityForPurchase, useFreeTrialPlanSlugs, - forceDefaultIntent, } ); const planFeaturesForComparisonGrid = useRestructuredPlanFeaturesForComparisonGrid( { diff --git a/packages/plans-grid-next/src/hooks/data-store/use-grid-plans.tsx b/packages/plans-grid-next/src/hooks/data-store/use-grid-plans.tsx index be3df279d29fc4..84b6ab18454d99 100644 --- a/packages/plans-grid-next/src/hooks/data-store/use-grid-plans.tsx +++ b/packages/plans-grid-next/src/hooks/data-store/use-grid-plans.tsx @@ -228,7 +228,6 @@ const useGridPlans: UseGridPlansType = ( { coupon, siteId, isDisplayingPlansNeededForFeature, - forceDefaultIntent, highlightLabelOverrides, } ) => { const freeTrialPlanSlugs = useFreeTrialPlanSlugs?.( { @@ -248,7 +247,7 @@ const useGridPlans: UseGridPlansType = ( { } ); const planSlugsForIntent = usePlansFromTypes( { planTypes: usePlanTypesWithIntent( { - intent: forceDefaultIntent ? 'plans-default-wpcom' : intent, + intent, selectedPlan, siteId, hiddenPlans, diff --git a/packages/plans-grid-next/src/hooks/use-is-large-currency.ts b/packages/plans-grid-next/src/hooks/use-is-large-currency.ts index 98524ec04086e5..e2428c1f63c460 100644 --- a/packages/plans-grid-next/src/hooks/use-is-large-currency.ts +++ b/packages/plans-grid-next/src/hooks/use-is-large-currency.ts @@ -9,9 +9,10 @@ interface Props { prices?: number[]; isAddOn?: boolean; currencyCode: string; + ignoreWhitespace?: boolean; } -function useDisplayPrices( currencyCode: string, prices?: number[] ) { +function useDisplayPrices( currencyCode: string, prices?: number[], ignoreWhitespace = false ) { /** * Prices are represented in smallest units for a currency, and not as prices that * are actually displayed. Ex. $20 is the integer 2000, and not 20. To determine if @@ -20,13 +21,15 @@ function useDisplayPrices( currencyCode: string, prices?: number[] ) { return useMemo( () => - prices?.map( ( price ) => - formatCurrency( price, currencyCode, { + prices?.map( ( price ) => { + const displayPrice = formatCurrency( price, currencyCode, { stripZeros: true, isSmallestUnit: true, - } ) - ), - [ currencyCode, prices ] + } ); + + return ignoreWhitespace ? displayPrice.replace( /\s/g, '' ) : displayPrice; + } ), + [ currencyCode, prices, ignoreWhitespace ] ); } @@ -64,7 +67,12 @@ function hasExceededCombinedPriceThreshold( displayPrices?: string[] ) { * 9 characters. For example, $4,000 undiscounted and $30 discounted would be 9 characters. * This is primarily used for lowering the font-size of "large" display prices. */ -export default function useIsLargeCurrency( { prices, isAddOn = false, currencyCode }: Props ) { +export default function useIsLargeCurrency( { + prices, + isAddOn = false, + currencyCode, + ignoreWhitespace = false, +}: Props ) { /** * Because this hook is primarily used for lowering font-sizes of "large" display prices, * this implementation is non-ideal. It assumes that each character in the display price, @@ -76,7 +84,7 @@ export default function useIsLargeCurrency( { prices, isAddOn = false, currencyC * * https://github.com/Automattic/wp-calypso/pull/81537#discussion_r1323182287 */ - const displayPrices = useDisplayPrices( currencyCode, prices ); + const displayPrices = useDisplayPrices( currencyCode, prices, ignoreWhitespace ); const exceedsPriceThreshold = hasExceededPriceThreshold( displayPrices, isAddOn ); const exceedsCombinedPriceThreshold = hasExceededCombinedPriceThreshold( displayPrices ); diff --git a/packages/urls/src/index.ts b/packages/urls/src/index.ts index 9f49f395349123..96440e9da05f32 100644 --- a/packages/urls/src/index.ts +++ b/packages/urls/src/index.ts @@ -25,7 +25,7 @@ export const DNS_RECORDS_EDITING_OR_DELETING = `${ root }/domains/custom-dns/#ed export const DNS_TXT_RECORD_CHAR_LIMIT = `${ root }/domains/custom-dns/#txt-record-character-limit`; export const ECOMMERCE = `${ root }/ecommerce/`; export const INCOMING_DOMAIN_TRANSFER_STATUSES = `${ root }/move-domain/incoming-domain-transfer/#checking-your-transfer-status-and-failed-transfers`; -export const INCOMING_DOMAIN_TRANSFER_STATUSES_IN_PROGRESS = `${ root }/incoming-domain-transfer/status-and-failed-transfers/#pending`; +export const INCOMING_DOMAIN_TRANSFER_STATUSES_IN_PROGRESS = `${ root }/incoming-domain-transfer/#step-4-check-the-transfer-status`; export const INCOMING_DOMAIN_TRANSFER = `${ root }/incoming-domain-transfer/`; export const INCOMING_DOMAIN_TRANSFER_PREPARE_UNLOCK = `${ root }/incoming-domain-transfer/#step-1-unlock-your-domain`; export const INCOMING_DOMAIN_TRANSFER_PREPARE_AUTH_CODE = `${ root }/incoming-domain-transfer/#step-2-obtain-your-domain-transfer-authorization-code`; diff --git a/packages/wpcom-checkout/src/checkout-labels.ts b/packages/wpcom-checkout/src/checkout-labels.ts index a3ae923758bf19..ae79b469bba665 100644 --- a/packages/wpcom-checkout/src/checkout-labels.ts +++ b/packages/wpcom-checkout/src/checkout-labels.ts @@ -96,13 +96,10 @@ export function getLabel( product: ResponseCartProduct ): string { return product.meta; } - if ( - isJetpackAISlug( product.product_slug ) && - ( product.quantity !== null || product.current_quantity !== null ) - ) { - // In theory, it'll fallback to 0, but just in case. - const quantity = product.quantity || product.current_quantity || 0; + // In theory, it'll fallback to 0, but just in case. + const quantity = product.quantity || product.current_quantity || 0; + if ( isJetpackAISlug( product.product_slug ) && quantity > 1 ) { return translate( '%(productName)s (%(quantity)d requests per month)', { args: { productName: product.product_name, diff --git a/packages/wpcom-checkout/src/payment-method-logos.tsx b/packages/wpcom-checkout/src/payment-method-logos.tsx index af7c7084d9c1b4..47a65d0ee1381e 100644 --- a/packages/wpcom-checkout/src/payment-method-logos.tsx +++ b/packages/wpcom-checkout/src/payment-method-logos.tsx @@ -4,14 +4,21 @@ import styled from '@emotion/styled'; /* eslint-disable @typescript-eslint/no-use-before-define */ export const PaymentMethodLogos = styled.span` + display: flex; + flex: 1; text-align: right; - transform: translateY( 3px ); + align-items: center; + justify-content: flex-end; + .rtl & { text-align: left; } svg { - display: block; + display: inline-block; + &.has-background { + padding-inline-end: 5px; + } } &.google-pay__logo svg { @@ -30,7 +37,6 @@ export function PaymentLogo( { brand, isSummary }: { brand: string; isSummary?: ); break; - case 'cb': case 'cartes_bancaires': cardFieldIcon = ( @@ -162,44 +168,65 @@ export function VisaLogo( { className }: { className?: string } ) { } export function CBLogo( { className }: { className?: string } ) { + // We need to provide a unique ID to any svg that uses an id prop + // especially if we expect multiple instances of the component to render on the page + const uniqueID = `${ Math.floor( 10000 + Math.random() * 90000 ) }`; + return ( diff --git a/packages/wpcom-template-parts/src/universal-header-navigation/style.scss b/packages/wpcom-template-parts/src/universal-header-navigation/style.scss index 5b34ea6e34a913..ba836e1f7eb753 100644 --- a/packages/wpcom-template-parts/src/universal-header-navigation/style.scss +++ b/packages/wpcom-template-parts/src/universal-header-navigation/style.scss @@ -889,43 +889,3 @@ $x-menu-heading-line-height-wide: $x-menu-heading-font-size-wide + 7px; /* 1 */ } /* End site profiler section overrides */ - -/* Start start with section overrides */ - -.is-style-white { - .x-root { - .masterbar-menu { - .masterbar { - border-bottom: none; - background: #fdfdfd; - height: 56px; - position: relative; - .x-nav { - height: 56px; - } - } - } - } - - .x-nav-item { - color: #000; - - .cta-btn-nav { - border-radius: 4px; - background: inherit !important; - border: 1px solid #000; - } - - .x-nav-link__chevron::after, - .x-nav-link-chevron::before { - content: $lp-chevron-content-down; - color: #000; - } - - .x-nav-link.x-nav-link.x-nav-link__primary { - color: #000 !important; - } - } -} - -/* End start with section overrides */ diff --git a/renovate.json5 b/renovate.json5 index b26ea154eb7ebe..dcd5e1d340edd2 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -77,12 +77,7 @@ enabled: false, }, ], - ignoreDeps: [ - 'electron-builder', - // We're intentionally locking to v0.4.1, see https://github.com/Automattic/wp-calypso/pull/87956 - // @TODO: Remove once updated to use the latest version - '@wordpress/dataviews', - ], + ignoreDeps: [ 'electron-builder' ], regexManagers: [ // Update the renovate-version in the action itself. // See also https://github.com/renovatebot/github-action/issues/756 diff --git a/test/e2e/specs/jetpack/jetpack__dashboard-smoke.ts b/test/e2e/specs/jetpack/jetpack__dashboard-smoke.ts index 2ad3a690db76fb..8323ade220b40c 100644 --- a/test/e2e/specs/jetpack/jetpack__dashboard-smoke.ts +++ b/test/e2e/specs/jetpack/jetpack__dashboard-smoke.ts @@ -47,6 +47,16 @@ skipDescribeIf( envVariables.TEST_ON_ATOMIC !== true )( } jetpackDashboardPage = new JetpackDashboardPage( page ); + + // Atomic tests sites might have local users, so the Jetpack SSO login will + // show up when visiting the Jetpack dashboard directly. We can bypass it if + // we simulate a redirect from Calypso to WP Admin with a hardcoded referer. + // @see https://github.com/Automattic/jetpack/blob/12b3b9a4771169398d4e1982573aaec820babc17/projects/plugins/wpcomsh/wpcomsh.php#L230-L254 + const siteUrl = testAccount.getSiteURL( { protocol: true } ); + await page.goto( `${ siteUrl }wp-admin/`, { + timeout: 15 * 1000, + referer: 'https://wordpress.com/', + } ); } ); it( 'Navigate to Jetpack dashboard', async function () { diff --git a/test/e2e/specs/martech/tos-screenshots__checkout.ts b/test/e2e/specs/martech/tos-screenshots__checkout.ts index f1d607fdbf8c56..e0cd3e5b26bfec 100644 --- a/test/e2e/specs/martech/tos-screenshots__checkout.ts +++ b/test/e2e/specs/martech/tos-screenshots__checkout.ts @@ -33,6 +33,10 @@ describe( DataHelper.createSuiteTitle( 'ToS acceptance tracking screenshots' ), await page.reload( { waitUntil: 'domcontentloaded', timeout: EXTENDED_TIMEOUT } ); } ); + it( 'See Home', async function () { + await page.waitForURL( /home/ ); + } ); + it( 'Add WordPress.com Business plan to cart', async function () { await Promise.all( [ page.waitForURL( /.*checkout.*/ ), diff --git a/test/e2e/specs/published-content/forms__submissions.ts b/test/e2e/specs/published-content/forms__submissions.ts index 26667ea3af8b7c..ee118eea01dc41 100644 --- a/test/e2e/specs/published-content/forms__submissions.ts +++ b/test/e2e/specs/published-content/forms__submissions.ts @@ -115,6 +115,18 @@ describe( DataHelper.createSuiteTitle( 'Feedback: Form Submission' ), function ( } else { await testAccount.authenticate( page ); } + + // Atomic tests sites might have local users, so the Jetpack SSO login will + // show up when visiting the Jetpack dashboard directly. We can bypass it if + // we simulate a redirect from Calypso to WP Admin with a hardcoded referer. + // @see https://github.com/Automattic/jetpack/blob/12b3b9a4771169398d4e1982573aaec820babc17/projects/plugins/wpcomsh/wpcomsh.php#L230-L254 + if ( envVariables.TEST_ON_ATOMIC ) { + const siteUrl = testAccount.getSiteURL( { protocol: true } ); + await page.goto( `${ siteUrl }wp-admin/`, { + timeout: 15 * 1000, + referer: 'https://wordpress.com/', + } ); + } } ); it( 'Navigate to the Jetpack Forms Inbox', async function () { diff --git a/yarn.lock b/yarn.lock index 6338854bbe8aa2..33b028d55b208a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,6 +63,13 @@ __metadata: languageName: node linkType: hard +"@ariakit/core@npm:0.4.9": + version: 0.4.9 + resolution: "@ariakit/core@npm:0.4.9" + checksum: 17a2b95804b3d3b1d7c6bc6aeef25dc300b3d7303e700c70878cfaea203105a5f1a0b5c67b7997d911cda0ba5895783ef2d3b5b83268a4e24d504eb67fa25fec + languageName: node + linkType: hard + "@ariakit/react-core@npm:0.3.14": version: 0.3.14 resolution: "@ariakit/react-core@npm:0.3.14" @@ -77,6 +84,20 @@ __metadata: languageName: node linkType: hard +"@ariakit/react-core@npm:0.4.10": + version: 0.4.10 + resolution: "@ariakit/react-core@npm:0.4.10" + dependencies: + "@ariakit/core": "npm:0.4.9" + "@floating-ui/dom": "npm:^1.0.0" + use-sync-external-store: "npm:^1.2.0" + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: eeb51c643ad36af8a293cc0fe3267214780ce9e97811dc44e69166f4592282efe0fd452bf12fee09a74f8deacce73a4c8d1859d0c11fbb4ca2b1a3dde2c7e708 + languageName: node + linkType: hard + "@ariakit/react@npm:^0.3.12": version: 0.3.14 resolution: "@ariakit/react@npm:0.3.14" @@ -89,6 +110,18 @@ __metadata: languageName: node linkType: hard +"@ariakit/react@npm:^0.4.10": + version: 0.4.10 + resolution: "@ariakit/react@npm:0.4.10" + dependencies: + "@ariakit/react-core": "npm:0.4.10" + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: ed70c8b25694cec6022cd9fff87ef1ced57f75803df2db2c362a38f3dc15fb345cdd60b6c63aa7d305cef489ff2a49ef964cee9d5d518f09319d169a071a8190 + languageName: node + linkType: hard + "@automattic/accessible-focus@workspace:^, @automattic/accessible-focus@workspace:packages/accessible-focus": version: 0.0.0-use.local resolution: "@automattic/accessible-focus@workspace:packages/accessible-focus" @@ -322,7 +355,7 @@ __metadata: dependencies: "@automattic/calypso-eslint-overrides": "workspace:^" "@automattic/calypso-typescript-config": "workspace:^" - "@automattic/color-studio": "npm:2.6.0" + "@automattic/color-studio": "npm:^3.0.1" postcss: "npm:^8.4.5" postcss-custom-properties: "npm:^11.0.0" sass: "npm:^1.37.5" @@ -545,10 +578,10 @@ __metadata: languageName: unknown linkType: soft -"@automattic/color-studio@npm:2.6.0": - version: 2.6.0 - resolution: "@automattic/color-studio@npm:2.6.0" - checksum: 1171df1d9b92b2734950239eb82250fe53f45d09485fd18b2aa62a40aaabbaa1c127898da97319f871e137254aae9ed7141e2aae30ec09f0151f515af8516510 +"@automattic/color-studio@npm:^3.0.1": + version: 3.0.1 + resolution: "@automattic/color-studio@npm:3.0.1" + checksum: 1fed9e35e5a4cf283055d323661bf94f8836fbe762c3a1ff7c514407c9cffcae9b05791fb2d4904050c097e939b745c8f2bc13cc84c9963d35c9d1aa57f1fbd8 languageName: node linkType: hard @@ -673,7 +706,7 @@ __metadata: dependencies: "@automattic/calypso-storybook": "workspace:^" "@automattic/calypso-typescript-config": "workspace:^" - "@automattic/color-studio": "npm:2.6.0" + "@automattic/color-studio": "npm:^3.0.1" "@emotion/react": "npm:^11.11.1" "@emotion/styled": "npm:^11.11.0" "@storybook/cli": "npm:^7.6.19" @@ -1592,7 +1625,7 @@ __metadata: resolution: "@automattic/page-pattern-modal@workspace:packages/page-pattern-modal" dependencies: "@automattic/calypso-typescript-config": "workspace:^" - "@automattic/color-studio": "npm:2.6.0" + "@automattic/color-studio": "npm:^3.0.1" "@automattic/typography": "npm:1.0.0" "@testing-library/react": "npm:^15.0.7" "@wordpress/base-styles": "npm:5.2.0" @@ -9676,47 +9709,26 @@ __metadata: languageName: node linkType: hard -"@wordpress/dataviews@npm:0.4.1": - version: 0.4.1 - resolution: "@wordpress/dataviews@npm:0.4.1" - dependencies: - "@babel/runtime": "npm:^7.16.0" - "@wordpress/a11y": "npm:^3.50.0" - "@wordpress/components": "npm:^25.16.0" - "@wordpress/compose": "npm:^6.27.0" - "@wordpress/element": "npm:^5.27.0" - "@wordpress/i18n": "npm:^4.50.0" - "@wordpress/icons": "npm:^9.41.0" - "@wordpress/keycodes": "npm:^3.50.0" - "@wordpress/primitives": "npm:^3.48.0" - "@wordpress/private-apis": "npm:^0.32.0" - classnames: "npm:^2.3.1" - remove-accents: "npm:^0.5.0" - peerDependencies: - react: ^18.0.0 - checksum: 00f5be7dc18de659bb52587380d5d88f0eda5fa99309bcc16bb02b9a4a7a51c82654bbf69aa0f7831e8f64df3e5c378d121874b795b7d3d60155d73a0f5adc1b - languageName: node - linkType: hard - -"@wordpress/dataviews@patch:@wordpress/dataviews@npm%3A0.4.1#~/.yarn/patches/@wordpress-dataviews-npm-0.4.1-2c01fa0792.patch": - version: 0.4.1 - resolution: "@wordpress/dataviews@patch:@wordpress/dataviews@npm%3A0.4.1#~/.yarn/patches/@wordpress-dataviews-npm-0.4.1-2c01fa0792.patch::version=0.4.1&hash=d91dff" +"@wordpress/dataviews@npm:4.2.0": + version: 4.2.0 + resolution: "@wordpress/dataviews@npm:4.2.0" dependencies: + "@ariakit/react": "npm:^0.4.10" "@babel/runtime": "npm:^7.16.0" - "@wordpress/a11y": "npm:^3.50.0" - "@wordpress/components": "npm:^25.16.0" - "@wordpress/compose": "npm:^6.27.0" - "@wordpress/element": "npm:^5.27.0" - "@wordpress/i18n": "npm:^4.50.0" - "@wordpress/icons": "npm:^9.41.0" - "@wordpress/keycodes": "npm:^3.50.0" - "@wordpress/primitives": "npm:^3.48.0" - "@wordpress/private-apis": "npm:^0.32.0" - classnames: "npm:^2.3.1" + "@wordpress/components": "npm:^28.6.0" + "@wordpress/compose": "npm:^7.6.0" + "@wordpress/data": "npm:^10.6.0" + "@wordpress/element": "npm:^6.6.0" + "@wordpress/i18n": "npm:^5.6.0" + "@wordpress/icons": "npm:^10.6.0" + "@wordpress/primitives": "npm:^4.6.0" + "@wordpress/private-apis": "npm:^1.6.0" + "@wordpress/warning": "npm:^3.6.0" + clsx: "npm:^2.1.1" remove-accents: "npm:^0.5.0" peerDependencies: react: ^18.0.0 - checksum: 757d69e92446b3d0a28b94c3cf2d3711a304c9949001392e2bb62f37409bee6561af5587df057a2296229f12b70210626d25db94280dfaa8b5f0789dd5131ddb + checksum: 01dbabaea48def0b810b5bc53012d89f3aa308fa44a6b5ac51a2537474f6f2ce513390ca9c4fdf9d0190f3fc9d3210d5ba0bb2fbb4862d9e3ac085d23dabe646 languageName: node linkType: hard @@ -9833,7 +9845,6 @@ __metadata: "@wordpress/core-commands": "npm:^1.2.0" "@wordpress/core-data": "npm:^7.2.0" "@wordpress/data": "npm:^10.2.0" - "@wordpress/dataviews": "npm:^2.2.0" "@wordpress/date": "npm:^5.2.0" "@wordpress/deprecated": "npm:^4.2.0" "@wordpress/dom": "npm:^4.2.0" @@ -12661,7 +12672,7 @@ __metadata: "@automattic/calypso-sentry": "workspace:^" "@automattic/calypso-stripe": "workspace:^" "@automattic/calypso-url": "workspace:^" - "@automattic/color-studio": "npm:2.6.0" + "@automattic/color-studio": "npm:^3.0.1" "@automattic/command-palette": "workspace:^" "@automattic/components": "workspace:^" "@automattic/composite-checkout": "workspace:^" @@ -12737,7 +12748,7 @@ __metadata: "@wordpress/components": "npm:^28.2.0" "@wordpress/compose": "npm:^7.2.0" "@wordpress/data": "npm:^10.2.0" - "@wordpress/dataviews": "patch:@wordpress/dataviews@npm%3A0.4.1#~/.yarn/patches/@wordpress-dataviews-npm-0.4.1-2c01fa0792.patch" + "@wordpress/dataviews": "npm:^4.2.0" "@wordpress/dom": "npm:^4.2.0" "@wordpress/edit-post": "npm:^8.2.0" "@wordpress/element": "npm:^6.2.0" @@ -13329,13 +13340,6 @@ __metadata: languageName: node linkType: hard -"classnames@npm:^2.3.1": - version: 2.3.2 - resolution: "classnames@npm:2.3.2" - checksum: cd50ead57b4f97436aaa9f9885c6926323efc7c2bea8e3d4eb10e4e972aa6a1cfca1c7a0e06f8a199ca7498d4339e30bb6002e589e61c9f21248cbf3e8b0b18d - languageName: node - linkType: hard - "clean-css@npm:4.2.x": version: 4.2.3 resolution: "clean-css@npm:4.2.3" @@ -19287,7 +19291,7 @@ __metadata: "@automattic/calypso-build": "workspace:^" "@automattic/calypso-config": "workspace:^" "@automattic/calypso-products": "workspace:^" - "@automattic/color-studio": "npm:2.6.0" + "@automattic/color-studio": "npm:^3.0.1" "@automattic/components": "workspace:^" "@automattic/format-currency": "workspace:^" "@automattic/typography": "workspace:^" @@ -34502,7 +34506,7 @@ __metadata: "@automattic/calypso-razorpay": "workspace:^" "@automattic/calypso-router": "workspace:^" "@automattic/calypso-storybook": "workspace:^" - "@automattic/color-studio": "npm:2.6.0" + "@automattic/color-studio": "npm:^3.0.1" "@automattic/command-palette": "workspace:^" "@automattic/components": "workspace:^" "@automattic/data-stores": "workspace:^"