@@ -407,6 +447,7 @@ const connectComponent = connect(
intervals,
siteId,
isSiteJetpackNotAtomic,
+ momentSiteZone: getMomentSiteZone( state, siteId ),
};
},
{ recordGoogleEvent: recordGoogleEventAction, toggleUpsellModal }
diff --git a/client/my-sites/stats/stats-strings.js b/client/my-sites/stats/stats-strings.js
index 475487f1e6b64..e45d1000a5c4a 100644
--- a/client/my-sites/stats/stats-strings.js
+++ b/client/my-sites/stats/stats-strings.js
@@ -14,7 +14,13 @@ export default function () {
{
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Posts & Pages module is empty',
}
@@ -32,7 +38,13 @@ export default function () {
{
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Referrers module is empty',
}
@@ -48,7 +60,9 @@ export default function () {
empty: translate( 'Your most {{link}}clicked external links{{/link}} will display here.', {
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Clicks module is empty',
} ),
@@ -65,7 +79,13 @@ export default function () {
{
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Countries module is empty',
}
@@ -83,7 +103,13 @@ export default function () {
{
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the UTM module is empty',
}
@@ -101,7 +127,13 @@ export default function () {
empty: translate( 'See {{link}}terms that visitors search{{/link}} to find your site, here. ', {
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Search Terms module is empty',
} ),
@@ -116,7 +148,9 @@ export default function () {
empty: translate( '{{link}}Traffic that authors have generated{{/link}} will show here.', {
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Authors module is empty',
} ),
@@ -131,7 +165,9 @@ export default function () {
empty: translate( 'Your most viewed {{link}}video stats{{/link}} will show up here.', {
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Videos module is empty',
} ),
@@ -146,7 +182,13 @@ export default function () {
empty: translate( 'Stats from any {{link}}downloaded files{{/link}} will display here.', {
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the file downloads module is empty',
} ),
@@ -162,7 +204,11 @@ export default function () {
comment: '{{link}} links to support documentation.',
components: {
link: (
-
+
),
},
context: 'Stats: Info box label when the Tags module is empty',
@@ -187,7 +233,9 @@ export default function () {
empty: translate( 'Stats from {{link}}your emails{{/link}} will display here.', {
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Email Open module is empty',
} ),
@@ -215,7 +263,13 @@ export default function () {
{
comment: '{{link}} links to support documentation.',
components: {
- link:
,
+ link: (
+
+ ),
},
context: 'Stats: Info box label when the Devices module is empty',
}
diff --git a/client/my-sites/stats/stats-tabs/index.jsx b/client/my-sites/stats/stats-tabs/index.jsx
index 5ceae46768103..556a62df7e20b 100644
--- a/client/my-sites/stats/stats-tabs/index.jsx
+++ b/client/my-sites/stats/stats-tabs/index.jsx
@@ -1,3 +1,5 @@
+import { TrendComparison } from '@automattic/components/src/highlight-cards/count-comparison-card';
+import formatNumber from '@automattic/components/src/number-formatters/lib/format-number';
import clsx from 'clsx';
import { localize } from 'i18n-calypso';
import { find } from 'lodash';
@@ -11,65 +13,88 @@ class StatsTabs extends Component {
static displayName = 'StatsTabs';
static propTypes = {
- activeKey: PropTypes.string,
+ children: PropTypes.node,
+ data: PropTypes.array,
+ previousData: PropTypes.array,
activeIndex: PropTypes.string,
- selectedTab: PropTypes.string,
- switchTab: PropTypes.func,
+ activeKey: PropTypes.string,
tabs: PropTypes.array,
+ switchTab: PropTypes.func,
+ selectedTab: PropTypes.string,
borderless: PropTypes.bool,
aggregate: PropTypes.bool,
};
+ formatData = ( data, aggregate = true ) => {
+ const { activeIndex, activeKey, tabs } = this.props;
+ let activeData = {};
+ if ( ! aggregate ) {
+ activeData = find( data, { [ activeKey ]: activeIndex } );
+ } else {
+ data?.map( ( day ) =>
+ tabs.map( ( tab ) => {
+ if ( isFinite( day[ tab.attr ] ) ) {
+ if ( ! ( tab.attr in activeData ) ) {
+ activeData[ tab.attr ] = 0;
+ }
+ activeData[ tab.attr ] = activeData[ tab.attr ] + day[ tab.attr ];
+ }
+ } )
+ );
+ }
+ return activeData;
+ };
+
render() {
const {
children,
data,
- activeIndex,
- activeKey,
+ previousData,
tabs,
switchTab,
selectedTab,
borderless,
aggregate,
+ tabCountsAlt,
+ tabCountsAltComp,
} = this.props;
let statsTabs;
if ( data && ! children ) {
- let activeData = {};
- if ( ! aggregate ) {
- activeData = find( data, { [ activeKey ]: activeIndex } );
- } else {
- // TODO: not major but we might want to cache the data.
- data.map( ( day ) =>
- tabs.map( ( tab ) => {
- if ( isFinite( day[ tab.attr ] ) ) {
- if ( ! ( tab.attr in activeData ) ) {
- activeData[ tab.attr ] = 0;
- }
- activeData[ tab.attr ] = activeData[ tab.attr ] + day[ tab.attr ];
- }
- } )
- );
- }
+ const trendData = this.formatData( data, aggregate );
+ const activeData = { ...tabCountsAlt, ...trendData };
+ const activePreviousData = { ...tabCountsAltComp, ...this.formatData( previousData ) };
statsTabs = tabs.map( ( tab ) => {
- const hasData =
- activeData && activeData[ tab.attr ] >= 0 && activeData[ tab.attr ] !== null;
+ const hasTrend = trendData?.[ tab.attr ] >= 0 && trendData[ tab.attr ] !== null;
+ const hasData = activeData?.[ tab.attr ] >= 0 && activeData[ tab.attr ] !== null;
+ const value = hasData ? activeData[ tab.attr ] : null;
+ const previousValue =
+ activePreviousData?.[ tab.attr ] !== null ? activePreviousData[ tab.attr ] : null;
const tabOptions = {
attr: tab.attr,
icon: tab.icon,
- className: tab.className,
+ className: clsx( tab.className, { 'is-highlighted': previousData } ),
label: tab.label,
loading: ! hasData,
selected: selectedTab === tab.attr,
- tabClick: switchTab,
- value: hasData ? activeData[ tab.attr ] : null,
+ tabClick: hasTrend ? switchTab : undefined,
+ value,
format: tab.format,
};
- return
;
+ return (
+
+ { previousData && (
+
+ { formatNumber( value ) }
+
+
+ ) }
+
+ );
} );
}
diff --git a/client/my-sites/stats/stats-tabs/style.scss b/client/my-sites/stats/stats-tabs/style.scss
index 4c476458a2769..27f003f914f45 100644
--- a/client/my-sites/stats/stats-tabs/style.scss
+++ b/client/my-sites/stats/stats-tabs/style.scss
@@ -4,7 +4,6 @@
$stats-tab-outer-padding: 10px;
.stats-tabs {
- @include clear-fix;
background: var(--color-surface);
border-top: 1px solid var(--color-border-subtle);
list-style: none;
@@ -98,7 +97,6 @@ $stats-tab-outer-padding: 10px;
@include breakpoint-deprecated( ">480px" ) {
@include mobile-link-element;
- @include clear-fix;
padding-bottom: $stats-tab-outer-padding;
-webkit-touch-callout: none;
}
diff --git a/client/my-sites/stats/stats-tabs/tab.jsx b/client/my-sites/stats/stats-tabs/tab.jsx
index ba68eb54a517e..e37e12a88edad 100644
--- a/client/my-sites/stats/stats-tabs/tab.jsx
+++ b/client/my-sites/stats/stats-tabs/tab.jsx
@@ -55,22 +55,16 @@ class StatsTabsTab extends Component {
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
-
- { hasClickAction ? (
-
- { tabIcon }
- { tabLabel }
- { tabValue }
- { children }
-
- ) : (
-
- { tabIcon }
- { tabLabel }
- { tabValue }
- { children }
-
- ) }
+
+
+ { tabIcon }
+ { tabLabel }
+ { tabValue }
+ { children }
+
);
}
diff --git a/client/my-sites/stats/style.scss b/client/my-sites/stats/style.scss
index 07d2f9baea74c..e0e4443459ac4 100644
--- a/client/my-sites/stats/style.scss
+++ b/client/my-sites/stats/style.scss
@@ -47,16 +47,20 @@ $stats-card-min-width: 390px;
}
}
-.stats__sticky-navigation.is-sticky .sticky-panel__content {
+.stats__sticky-navigation.is-sticky .sticky-panel__content, .sticky-panel.is-sticky .sticky-panel__content {
background: var(--studio-white);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1), 0 0 56px rgba(0, 0, 0, 0.075);
.stats-period-navigation {
margin: 9px 0;
}
+}
- .stats-date-picker__refresh-status {
- display: none;
+.stats__period-header {
+ padding: 0;
+
+ .is-sticky & {
+ padding: 0 32px;
}
}
@@ -226,6 +230,11 @@ $stats-card-min-width: 390px;
&.color-scheme.is-classic-dark {
@include apply-improved-classic-dark-colors();
}
+
+ .sticky-panel.is-sticky {
+ padding: 0;
+ }
+
}
.list-emails {
diff --git a/client/my-sites/stats/summary/index.jsx b/client/my-sites/stats/summary/index.jsx
index a5ee2d078f708..314984d6b239a 100644
--- a/client/my-sites/stats/summary/index.jsx
+++ b/client/my-sites/stats/summary/index.jsx
@@ -88,6 +88,15 @@ class StatsSummary extends Component {
date: endOf.format( 'YYYY-MM-DD' ),
max: 0,
};
+
+ // Update query with date range if it provided.
+ const dateRange = this.props.dateRange;
+ if ( dateRange ) {
+ query.start_date = dateRange.startDate.format( 'YYYY-MM-DD' );
+ query.date = dateRange.endDate.format( 'YYYY-MM-DD' );
+ query.summarize = 1;
+ }
+
const moduleQuery = merge( {}, statsQueryOptions, query );
const urlParams = new URLSearchParams( this.props.context.querystring );
const listItemClassName = 'stats__summary--narrow-mobile';
diff --git a/client/sites-dashboard/components/sites-site-name.ts b/client/sites-dashboard/components/sites-site-name.ts
index 885bcc32d7c4f..509950df7eb9a 100644
--- a/client/sites-dashboard/components/sites-site-name.ts
+++ b/client/sites-dashboard/components/sites-site-name.ts
@@ -8,16 +8,11 @@ export const SiteName = styled.a< { fontSize?: number } >`
font-weight: 500;
font-size: ${ ( props ) => `${ props.fontSize }px` };
letter-spacing: -0.4px;
+ color: var( --studio-gray-100 );
&:is( a ):hover {
text-decoration: underline;
}
-
- &,
- &:hover,
- &:visited {
- color: var( --studio-gray-100 );
- }
`;
SiteName.defaultProps = {
diff --git a/client/sites/components/dotcom-style.scss b/client/sites/components/dotcom-style.scss
index 51de8db2a7d2e..5562dca338398 100644
--- a/client/sites/components/dotcom-style.scss
+++ b/client/sites/components/dotcom-style.scss
@@ -18,10 +18,9 @@
.a4a-layout__body {
> * {
padding-inline: 48px;
- max-width: none;
- max-width: 1400px !important;
- margin-inline: auto !important;
-
+ // Override the max-width set in a8c-for-agencies/components/layout/style.scss
+ // as the title aligns with DataViews (full width).
+ max-width: revert;
@media (max-width: 402px) {
padding-inline: 24px;
}
@@ -107,16 +106,6 @@
}
}
-// Style the sortable table headers.
-.wpcom-site .dataviews-view-table .components-button.is-tertiary {
- &:active:not(:disabled),
- &:hover:not(:disabled) {
- box-shadow: none;
- background-color: inherit;
- color: var(--color-accent) !important;
- }
-}
-
.wpcom-site {
.layout__content {
min-height: 100vh;
@@ -392,41 +381,14 @@
display: flex;
flex-direction: column;
height: 100%;
-
- .dataviews-wrapper {
- .dataviews-pagination {
- border: 0;
- margin: 0;
- padding-left: 0;
- padding-right: 0;
- position: relative;
- }
-
- .spinner-wrapper {
- position: absolute;
- left: 50%;
- top: 70px;
- z-index: 2;
- }
- }
}
}
.wpcom-site .main.a4a-layout.sites-dashboard.sites-dashboard__layout.preview-hidden {
- .dataviews-wrapper {
- margin: 0 auto;
- max-width: 1400px;
- box-sizing: border-box;
-
- .dataviews-pagination {
- width: auto;
- }
- }
-
div.a4a-layout__viewport {
- margin: 0 auto;
- max-width: 1400px;
- box-sizing: border-box;
+ // margin: 0 auto;
+ // max-width: 1400px;
+ // box-sizing: border-box;
}
}
diff --git a/client/sites/components/panel/style.scss b/client/sites/components/panel/style.scss
index 1544d8ef97824..92944382239c9 100644
--- a/client/sites/components/panel/style.scss
+++ b/client/sites/components/panel/style.scss
@@ -12,6 +12,10 @@
.header-cake__back {
padding: 0;
}
+
+ .upsell-nudge {
+ width: 100%;
+ }
}
.panel-section {
diff --git a/client/sites/components/site-preview-pane/dotcom-preview-pane.tsx b/client/sites/components/site-preview-pane/dotcom-preview-pane.tsx
index 715188eeecd9d..c71f41f192569 100644
--- a/client/sites/components/site-preview-pane/dotcom-preview-pane.tsx
+++ b/client/sites/components/site-preview-pane/dotcom-preview-pane.tsx
@@ -5,7 +5,7 @@ import { useI18n } from '@wordpress/react-i18n';
import React, { useMemo, useEffect } from 'react';
import ItemPreviewPane from 'calypso/a8c-for-agencies/components/items-dashboard/item-preview-pane';
import HostingFeaturesIcon from 'calypso/hosting/hosting-features/components/hosting-features-icon';
-import { areHostingFeaturesSupported } from 'calypso/sites/features';
+import { areHostingFeaturesSupported } from 'calypso/sites/hosting-features/features';
import { useStagingSite } from 'calypso/sites/tools/staging-site/hooks/use-staging-site';
import { getMigrationStatus } from 'calypso/sites-dashboard/utils';
import { useSelector } from 'calypso/state';
diff --git a/client/sites/components/sites-dashboard.tsx b/client/sites/components/sites-dashboard.tsx
index 22fb809bac9d3..83e68be884429 100644
--- a/client/sites/components/sites-dashboard.tsx
+++ b/client/sites/components/sites-dashboard.tsx
@@ -23,6 +23,7 @@ import LayoutTop from 'calypso/a8c-for-agencies/components/layout/top';
import { GuidedTourContextProvider } from 'calypso/a8c-for-agencies/data/guided-tours/guided-tour-context';
import DocumentHead from 'calypso/components/data/document-head';
import { useSiteExcerptsQuery } from 'calypso/data/sites/use-site-excerpts-query';
+import { recordTracksEvent } from 'calypso/lib/analytics/tracks';
import { isP2Theme } from 'calypso/lib/site/utils';
import {
SitesDashboardQueryParams,
@@ -76,9 +77,14 @@ const DEFAULT_SITE_TYPE = 'non-p2';
// Limit fields on breakpoints smaller than 960px wide.
const desktopFields = [ 'site', 'plan', 'status', 'last-publish', 'stats' ];
const mobileFields = [ 'site' ];
+const listViewFields = [ 'site' ];
-const getFieldsByBreakpoint = ( isDesktop: boolean ) =>
- isDesktop ? desktopFields : mobileFields;
+const getFieldsByBreakpoint = ( selectedSite: boolean, isDesktop: boolean ) => {
+ if ( selectedSite ) {
+ return listViewFields;
+ }
+ return isDesktop ? desktopFields : mobileFields;
+};
export function showSitesPage( route: string ) {
const currentParams = new URL( window.location.href ).searchParams;
@@ -171,7 +177,7 @@ const SitesDashboard = ( {
page,
perPage,
search: search ?? '',
- fields: getFieldsByBreakpoint( isDesktop ),
+ fields: getFieldsByBreakpoint( !! selectedSite, isDesktop ),
...( status
? {
filters: [
@@ -211,7 +217,7 @@ const SitesDashboard = ( {
const [ dataViewsState, setDataViewsState ] = useState< View >( defaultDataViewsState );
useEffect( () => {
- const fields = getFieldsByBreakpoint( isDesktop );
+ const fields = getFieldsByBreakpoint( !! selectedSite, 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.
@@ -238,7 +244,7 @@ const SitesDashboard = ( {
},
} );
}
- }, [ isDesktop, isWide, dataViewsState ] );
+ }, [ isDesktop, isWide, dataViewsState, selectedSite ] );
// Ensure site sort preference is applied when it loads in. This isn't always available on
// initial mount.
@@ -336,7 +342,14 @@ const SitesDashboard = ( {
}
};
- const openSitePreviewPane = ( site: SiteExcerptData ) => {
+ const openSitePreviewPane = (
+ site: SiteExcerptData,
+ source: 'site_field' | 'action' | 'list_row_click' | 'environment_switcher'
+ ) => {
+ recordTracksEvent( 'calypso_sites_dashboard_open_site_preview_pane', {
+ site_id: site.ID,
+ source,
+ } );
showSitesPage(
`/${ FEATURE_TO_ROUTE_MAP[ initialSiteFeature ].replace( ':site', site.slug ) }`
);
@@ -345,7 +358,7 @@ const SitesDashboard = ( {
const changeSitePreviewPane = ( siteId: number ) => {
const targetSite = allSites.find( ( site ) => site.ID === siteId );
if ( targetSite ) {
- openSitePreviewPane( targetSite );
+ openSitePreviewPane( targetSite, 'environment_switcher' );
}
};
diff --git a/client/sites/components/sites-dataviews/actions.tsx b/client/sites/components/sites-dataviews/actions.tsx
index 2f6e0d6ee1222..d6b5c859ffefe 100644
--- a/client/sites/components/sites-dataviews/actions.tsx
+++ b/client/sites/components/sites-dataviews/actions.tsx
@@ -1,8 +1,18 @@
import { FEATURE_SFTP, WPCOM_FEATURES_COPY_SITE } from '@automattic/calypso-products';
import page from '@automattic/calypso-router';
+import {
+ SiteExcerptData,
+ SITE_EXCERPT_REQUEST_FIELDS,
+ SITE_EXCERPT_REQUEST_OPTIONS,
+} from '@automattic/sites';
+import { useQueryClient } from '@tanstack/react-query';
+import { drawerLeft, wordpress, external } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { addQueryArgs } from '@wordpress/url';
import { useMemo } from 'react';
+import { USE_SITE_EXCERPTS_QUERY_KEY } from 'calypso/data/sites/use-site-excerpts-query';
+import { navigate } from 'calypso/lib/navigate';
+import useRestoreSiteMutation from 'calypso/sites/hooks/use-restore-site-mutation';
import {
getAdminInterface,
getPluginsUrl,
@@ -13,19 +23,149 @@ import {
isNotAtomicJetpack,
isP2Site,
isSimpleSite,
+ isDisconnectedJetpackAndNotAtomic,
} from 'calypso/sites-dashboard/utils';
-import { useDispatch as useReduxDispatch } from 'calypso/state';
+import { useDispatch as useReduxDispatch, useSelector } from 'calypso/state';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
+import { errorNotice, successNotice } from 'calypso/state/notices/actions';
import { launchSiteOrRedirectToLaunchSignupFlow } from 'calypso/state/sites/launch/actions';
-import type { SiteExcerptData } from '@automattic/sites';
import type { Action } from '@wordpress/dataviews';
-export function useActions(): Action< SiteExcerptData >[] {
+export function useActions( {
+ openSitePreviewPane,
+ selectedItem,
+}: {
+ openSitePreviewPane?: (
+ site: SiteExcerptData,
+ source: 'site_field' | 'action' | 'list_row_click' | 'environment_switcher'
+ ) => void;
+ selectedItem?: SiteExcerptData | null;
+} ): Action< SiteExcerptData >[] {
const { __ } = useI18n();
const dispatch = useReduxDispatch();
+ const queryClient = useQueryClient();
+ const reduxDispatch = useReduxDispatch();
+ const { mutate: restoreSite } = useRestoreSiteMutation( {
+ onSuccess() {
+ queryClient.invalidateQueries( {
+ queryKey: [
+ USE_SITE_EXCERPTS_QUERY_KEY,
+ SITE_EXCERPT_REQUEST_FIELDS,
+ SITE_EXCERPT_REQUEST_OPTIONS,
+ [],
+ 'all',
+ ],
+ } );
+ queryClient.invalidateQueries( {
+ queryKey: [
+ USE_SITE_EXCERPTS_QUERY_KEY,
+ SITE_EXCERPT_REQUEST_FIELDS,
+ SITE_EXCERPT_REQUEST_OPTIONS,
+ [],
+ 'deleted',
+ ],
+ } );
+ reduxDispatch(
+ successNotice( __( 'The site has been restored.' ), {
+ duration: 3000,
+ } )
+ );
+ },
+ onError: ( error ) => {
+ if ( error.status === 403 ) {
+ reduxDispatch(
+ errorNotice( __( 'Only an administrator can restore a deleted site.' ), {
+ duration: 5000,
+ } )
+ );
+ } else {
+ reduxDispatch(
+ errorNotice( __( 'We were unable to restore the site.' ), { duration: 5000 } )
+ );
+ }
+ },
+ } );
+
+ const capabilities = useSelector<
+ {
+ currentUser: {
+ capabilities: Record< string, Record< string, boolean > >;
+ };
+ },
+ Record< string, Record< string, boolean > >
+ >( ( state ) => state.currentUser.capabilities );
+
return useMemo(
() => [
+ {
+ id: 'site-overview',
+ isPrimary: true,
+ label: __( 'Overview' ),
+ icon: drawerLeft,
+ callback: ( sites ) => {
+ const site = sites[ 0 ];
+ const adminUrl = site.options?.admin_url ?? '';
+ const isAdmin = capabilities[ site.ID ]?.manage_options;
+ if (
+ isAdmin &&
+ ! isP2Site( site ) &&
+ ! isNotAtomicJetpack( site ) &&
+ ! isDisconnectedJetpackAndNotAtomic( site )
+ ) {
+ openSitePreviewPane && openSitePreviewPane( site, 'action' );
+ } else {
+ navigate( adminUrl );
+ }
+ },
+ isEligible: ( site ) => {
+ if ( site.ID === selectedItem?.ID ) {
+ return false;
+ }
+ if ( site.is_deleted ) {
+ return false;
+ }
+ return true;
+ },
+ },
+ {
+ id: 'open-site',
+ isPrimary: true,
+ label: __( 'Open site' ),
+ icon: external,
+ callback: ( sites ) => {
+ const site = sites[ 0 ];
+ const siteUrl = window.open( site.URL, '_blank' );
+ if ( siteUrl ) {
+ siteUrl.opener = null;
+ siteUrl.focus();
+ }
+ },
+ isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+ return true;
+ },
+ },
+ {
+ id: 'admin',
+ isPrimary: true,
+ label: __( 'WP Admin' ),
+ icon: wordpress,
+ callback: ( sites ) => {
+ const site = sites[ 0 ];
+ window.location.href = site.options?.admin_url ?? '';
+ dispatch( recordTracksEvent( 'calypso_sites_dashboard_site_action_wpadmin_click' ) );
+ },
+ isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+ return true;
+ },
+ },
+
{
id: 'launch-site',
label: __( 'Launch site' ),
@@ -34,6 +174,10 @@ export function useActions(): Action< SiteExcerptData >[] {
dispatch( recordTracksEvent( 'calypso_sites_dashboard_site_action_launch_click' ) );
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
const isLaunched = site.launch_status !== 'unlaunched';
const isA4ADevSite = site.is_a4a_dev_site;
const isWpcomStagingSite = site.is_wpcom_staging_site;
@@ -52,6 +196,10 @@ export function useActions(): Action< SiteExcerptData >[] {
);
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
const isLaunched = site.launch_status !== 'unlaunched';
const isA4ADevSite = site.is_a4a_dev_site;
const isWpcomStagingSite = site.is_wpcom_staging_site;
@@ -67,12 +215,22 @@ export function useActions(): Action< SiteExcerptData >[] {
page( getSettingsUrl( sites[ 0 ].slug ) );
dispatch( recordTracksEvent( 'calypso_sites_dashboard_site_action_settings_click' ) );
},
+ isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+ return true;
+ },
},
{
id: 'general-settings',
label: __( 'General settings' ),
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
const adminInterface = getAdminInterface( site );
const isWpAdminInterface = adminInterface === 'wp-admin';
return isWpAdminInterface;
@@ -100,6 +258,10 @@ export function useActions(): Action< SiteExcerptData >[] {
dispatch( recordTracksEvent( 'calypso_sites_dashboard_site_action_hosting_click' ) );
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
const isSiteJetpackNotAtomic = isNotAtomicJetpack( site );
return ! isSiteJetpackNotAtomic && ! isP2Site( site );
},
@@ -115,6 +277,10 @@ export function useActions(): Action< SiteExcerptData >[] {
);
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
return !! site.is_wpcom_atomic;
},
},
@@ -135,6 +301,10 @@ export function useActions(): Action< SiteExcerptData >[] {
dispatch( recordTracksEvent( 'calypso_sites_dashboard_site_action_plugins_click' ) );
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
return ! isP2Site( site );
},
},
@@ -152,6 +322,10 @@ export function useActions(): Action< SiteExcerptData >[] {
dispatch( recordTracksEvent( 'calypso_sites_dashboard_site_action_copy_site_click' ) );
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
const isWpcomStagingSite = site.is_wpcom_staging_site;
const shouldShowSiteCopyItem =
!! site.plan?.features.active.includes( WPCOM_FEATURES_COPY_SITE );
@@ -177,6 +351,10 @@ export function useActions(): Action< SiteExcerptData >[] {
);
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
const adminInterface = getAdminInterface( site );
const isWpAdminInterface = adminInterface === 'wp-admin';
const isClassicSimple = isWpAdminInterface && isSimpleSite( site );
@@ -195,6 +373,10 @@ export function useActions(): Action< SiteExcerptData >[] {
);
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
const isLaunched = site.launch_status !== 'unlaunched';
return isLaunched;
},
@@ -211,6 +393,10 @@ export function useActions(): Action< SiteExcerptData >[] {
);
},
isEligible: ( site ) => {
+ if ( site.is_deleted ) {
+ return false;
+ }
+
const hasCustomDomain = isCustomDomain( site.slug );
const isSiteJetpackNotAtomic = isNotAtomicJetpack( site );
return hasCustomDomain && ! isSiteJetpackNotAtomic;
@@ -218,15 +404,15 @@ export function useActions(): Action< SiteExcerptData >[] {
},
{
- id: 'admin',
- label: __( 'WP Admin' ),
+ id: 'restore',
+ label: __( 'Restore' ),
callback: ( sites ) => {
const site = sites[ 0 ];
- window.location.href = site.options?.admin_url ?? '';
- dispatch( recordTracksEvent( 'calypso_sites_dashboard_site_action_wpadmin_click' ) );
+ restoreSite( site.ID );
},
+ isEligible: ( site ) => !! site?.is_deleted,
},
],
- [ __, dispatch ]
+ [ __, capabilities, dispatch, openSitePreviewPane, restoreSite, selectedItem?.ID ]
);
}
diff --git a/client/sites/components/sites-dataviews/dataview-style.scss b/client/sites/components/sites-dataviews/dataview-style.scss
index f0b14b9e81b29..d2aa43962c29b 100644
--- a/client/sites/components/sites-dataviews/dataview-style.scss
+++ b/client/sites/components/sites-dataviews/dataview-style.scss
@@ -37,10 +37,6 @@
background: var(--studio-white);
}
- tbody tr.dataviews-view-table__row {
- cursor: pointer;
- }
-
tr.dataviews-view-table__row {
background: var(--studio-white);
@@ -132,98 +128,6 @@
color: inherit;
}
}
-
- .sites-dataviews__actions {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: end;
- flex-wrap: nowrap;
-
- @media (min-width: 1080px) {
- .site-actions__actions-large-screen {
- float: none;
- margin-inline-end: 20px;
- }
- }
-
- > *:not(:last-child) {
- margin-inline-end: 10px;
- }
-
- .components-dropdown-menu__toggle,
- .site-preview__open {
- .gridicon {
- width: 18px;
- height: 18px;
- }
- }
-
- &.sites-dataviews__actions-error {
- svg {
- color: var(--color-accent-5);
- }
- }
- }
-
- .dataviews-pagination {
- flex-shrink: 0;
- background: #fff;
- border-top: 1px solid #f1f1f1;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- bottom: 0;
- color: var(--Gray-Gray-40, #787c82);
- @include a4a-font-body-sm;
- 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);
- }
-
- .components-input-control__container {
- padding: 0 5px;
- }
-
- @include breakpoint-deprecated( ">1400px" ) {
- bottom: 0;
- }
- }
-
- // DataView overrides:
- // Using the List view for the fly-out panel requires hiding certain elements
- // to properly fit the site list when the panel is open.
- @media (min-width: $break-large) {
- ul.dataviews-view-list {
- // This override could potentially be removed if we’re able to pass the Actions column via data.actions
- .components-h-stack,
- .dataviews-view-list__item {
- width: 100%;
- }
-
- // This styling is a bit of a hack: To make the site name clickable area larger, will need
- // to hide the other fields when in preview mode.
- .dataviews-view-list__field {
- // Hide the field except first (Site name) and last (actions menu)
- &:not(:first-child) {
- display: none;
- }
-
- // Force the site name to take up the full width
- &:first-child {
- flex-grow: 1;
-
- .sites-dataviews__site {
- width: 100%;
- justify-content: flex-start;
- }
- }
- }
- }
- }
}
// Selected and hover states on the site list.
diff --git a/client/sites/components/sites-dataviews/dataviews-fields/site-field.tsx b/client/sites/components/sites-dataviews/dataviews-fields/site-field.tsx
index 1d18f50de5d49..c18eca9ed19e3 100644
--- a/client/sites/components/sites-dataviews/dataviews-fields/site-field.tsx
+++ b/client/sites/components/sites-dataviews/dataviews-fields/site-field.tsx
@@ -1,7 +1,6 @@
import { ListTile, Button } from '@automattic/components';
import { css } from '@emotion/css';
import styled from '@emotion/styled';
-import { Icon, external } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import clsx from 'clsx';
import { translate } from 'i18n-calypso';
@@ -31,7 +30,10 @@ import type { SiteExcerptData } from '@automattic/sites';
type Props = {
site: SiteExcerptData;
- openSitePreviewPane?: ( site: SiteExcerptData ) => void;
+ openSitePreviewPane?: (
+ site: SiteExcerptData,
+ source: 'site_field' | 'action' | 'list_row_click' | 'environment_switcher'
+ ) => void;
};
const SiteListTile = styled( ListTile )`
@@ -43,6 +45,13 @@ const SiteListTile = styled( ListTile )`
gap: 12px;
max-width: 500px;
width: 100%;
+ /*
+ * Ensures the row fits within the device width on mobile in most cases,
+ * as it's not apparent to users that they can scroll horizontally.
+ */
+ @media ( max-width: 480px ) {
+ width: 250px;
+ }
}
`;
@@ -64,7 +73,7 @@ const SiteField = ( { site, openSitePreviewPane }: Props ) => {
}
const title = __( 'View Site Details' );
- const { adminLabel, adminUrl } = useSiteAdminInterfaceData( site.ID );
+ const { adminUrl } = useSiteAdminInterfaceData( site.ID );
const isP2Site = site.options?.theme_slug && isP2Theme( site.options?.theme_slug );
const isWpcomStagingSite = isStagingSite( site );
@@ -79,7 +88,7 @@ const SiteField = ( { site, openSitePreviewPane }: Props ) => {
! isNotAtomicJetpack( site ) &&
! isDisconnectedJetpackAndNotAtomic( site )
) {
- openSitePreviewPane && openSitePreviewPane( site );
+ openSitePreviewPane && openSitePreviewPane( site, 'site_field' );
} else {
navigate( adminUrl );
}
@@ -90,7 +99,7 @@ const SiteField = ( { site, openSitePreviewPane }: Props ) => {
const siteTitle = isMigrationPending ? translate( 'Incoming Migration' ) : site.title;
return (
-
+
+
);
};
diff --git a/client/sites/components/sites-dataviews/index.tsx b/client/sites/components/sites-dataviews/index.tsx
index 51b042e6eb565..6bebaa232ce6f 100644
--- a/client/sites/components/sites-dataviews/index.tsx
+++ b/client/sites/components/sites-dataviews/index.tsx
@@ -1,7 +1,8 @@
+import { SiteExcerptData } from '@automattic/sites';
import { usePrevious } from '@wordpress/compose';
import { DataViews, Field } from '@wordpress/dataviews';
import { useI18n } from '@wordpress/react-i18n';
-import { useCallback, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
+import { useCallback, useMemo, useRef, useLayoutEffect } from 'react';
import JetpackLogo from 'calypso/components/jetpack-logo';
import TimeSince from 'calypso/components/time-since';
import { SitePlan } from 'calypso/sites-dashboard/components/sites-site-plan';
@@ -11,7 +12,6 @@ import { useActions } from './actions';
import SiteField from './dataviews-fields/site-field';
import { SiteStats } from './sites-site-stats';
import { SiteStatus } from './sites-site-status';
-import type { SiteExcerptData } from '@automattic/sites';
import type { View } from '@wordpress/dataviews';
import './style.scss';
@@ -24,7 +24,10 @@ type Props = {
dataViewsState: View;
setDataViewsState: ( callback: ( prevState: View ) => View ) => void;
selectedItem: SiteExcerptData | null | undefined;
- openSitePreviewPane: ( site: SiteExcerptData ) => void;
+ openSitePreviewPane: (
+ site: SiteExcerptData,
+ source: 'site_field' | 'action' | 'list_row_click' | 'environment_switcher'
+ ) => void;
};
export function useSiteStatusGroups() {
@@ -88,46 +91,28 @@ const DotcomSitesDataViews = ( {
// To prevent that, we want to use DataViews in "controlled" mode, so that we can pass an initial selection during initial mount.
//
// To do that, we need to pass a required `onSelectionChange` callback to signal that it is being used in controlled mode.
- // However, when don't need to do anything in the callback, because we already maintain a selectedItem state.
// The current selection is a derived value which is [selectedItem.ID] (see getSelection()).
- const onSelectionChange = () => {};
+ const onSelectionChange = useCallback(
+ ( selectedSiteIds: string[] ) => {
+ // In table view, when a row is clicked, the item is selected for a bulk action, so the panel should not open.
+ if ( dataViewsState.type !== 'list' ) {
+ return;
+ }
+ if ( selectedSiteIds.length === 0 ) {
+ return;
+ }
+ const site = sites.find( ( s ) => s.ID === Number( selectedSiteIds[ 0 ] ) );
+ if ( site ) {
+ openSitePreviewPane( site, 'list_row_click' );
+ }
+ },
+ [ dataViewsState.type, openSitePreviewPane, sites ]
+ );
const getSelection = useCallback(
() => ( selectedItem ? [ selectedItem.ID.toString() ] : undefined ),
[ selectedItem ]
);
- useEffect( () => {
- // If the user clicks on a row, open the site preview pane by triggering the site button click.
- const handleRowClick = ( event: Event ) => {
- const target = event.target as HTMLElement;
- const row = target.closest(
- '.dataviews-view-table__row, li:has(.dataviews-view-list__item)'
- );
- if ( row ) {
- const isButtonOrLink = target.closest( 'button, a' );
- if ( ! isButtonOrLink ) {
- const button = row.querySelector(
- '.sites-dataviews__preview-trigger'
- ) as HTMLButtonElement;
- if ( button ) {
- button.click();
- }
- }
- }
- };
-
- const rowsContainer = document.querySelector( '.dataviews-view-table, .dataviews-view-list' );
- if ( rowsContainer ) {
- rowsContainer.addEventListener( 'click', handleRowClick as EventListener );
- }
-
- return () => {
- if ( rowsContainer ) {
- rowsContainer.removeEventListener( 'click', handleRowClick as EventListener );
- }
- };
- }, [] );
-
const siteStatusGroups = useSiteStatusGroups();
// Generate DataViews table field-columns
@@ -199,7 +184,7 @@ const DotcomSitesDataViews = ( {
[ __, openSitePreviewPane, userId, siteStatusGroups ]
);
- const actions = useActions();
+ const actions = useActions( { openSitePreviewPane, selectedItem } );
return (
@@ -214,8 +199,6 @@ const DotcomSitesDataViews = ( {
selection={ getSelection() }
paginationInfo={ paginationInfo }
getItemId={ ( item ) => {
- // @ts-expect-error -- From ItemsDataViews, this item.id assignation is to fix an issue with the DataViews component and item selection. It should be removed once the issue is fixed.
- item.id = item.ID.toString();
return item.ID.toString();
} }
isLoading={ isLoading }
diff --git a/client/sites/components/sites-dataviews/sites-site-status.tsx b/client/sites/components/sites-dataviews/sites-site-status.tsx
index 6a2763d002588..c63a9c6673feb 100644
--- a/client/sites/components/sites-dataviews/sites-site-status.tsx
+++ b/client/sites/components/sites-dataviews/sites-site-status.tsx
@@ -1,23 +1,12 @@
-import { Button } from '@automattic/components';
-import {
- SITE_EXCERPT_REQUEST_FIELDS,
- SITE_EXCERPT_REQUEST_OPTIONS,
- useSiteLaunchStatusLabel,
-} from '@automattic/sites';
+import { useSiteLaunchStatusLabel } from '@automattic/sites';
import styled from '@emotion/styled';
-import { useQueryClient } from '@tanstack/react-query';
-import { Spinner } from '@wordpress/components';
import { useI18n } from '@wordpress/react-i18n';
-import { useDispatch as useReduxDispatch } from 'react-redux';
-import { USE_SITE_EXCERPTS_QUERY_KEY } from 'calypso/data/sites/use-site-excerpts-query';
import { SiteLaunchNag } from 'calypso/sites-dashboard/components/sites-site-launch-nag';
import TransferNoticeWrapper from 'calypso/sites-dashboard/components/sites-transfer-notice-wrapper';
import { WithAtomicTransfer } from 'calypso/sites-dashboard/components/with-atomic-transfer';
import { getMigrationStatus, MEDIA_QUERIES } from 'calypso/sites-dashboard/utils';
import { useSelector } from 'calypso/state';
-import { errorNotice, successNotice } from 'calypso/state/notices/actions';
import isDIFMLiteInProgress from 'calypso/state/selectors/is-difm-lite-in-progress';
-import useRestoreSiteMutation from '../../hooks/use-restore-site-mutation';
import type { SiteExcerptData } from '@automattic/sites';
const BadgeDIFM = styled.span`
@@ -42,80 +31,20 @@ const DeletedStatus = styled.div`
}
`;
-const RestoreButton = styled( Button )`
- color: var( --color-link ) !important;
- font-size: 12px;
- text-decoration: underline;
-`;
-
interface SiteStatusProps {
site: SiteExcerptData;
}
export const SiteStatus = ( { site }: SiteStatusProps ) => {
const { __ } = useI18n();
- const queryClient = useQueryClient();
- const reduxDispatch = useReduxDispatch();
const translatedStatus = useSiteLaunchStatusLabel( site );
const isDIFMInProgress = useSelector( ( state ) => isDIFMLiteInProgress( state, site.ID ) );
- const { mutate: restoreSite, isPending: isRestoring } = useRestoreSiteMutation( {
- onSuccess() {
- queryClient.invalidateQueries( {
- queryKey: [
- USE_SITE_EXCERPTS_QUERY_KEY,
- SITE_EXCERPT_REQUEST_FIELDS,
- SITE_EXCERPT_REQUEST_OPTIONS,
- [],
- 'all',
- ],
- } );
- queryClient.invalidateQueries( {
- queryKey: [
- USE_SITE_EXCERPTS_QUERY_KEY,
- SITE_EXCERPT_REQUEST_FIELDS,
- SITE_EXCERPT_REQUEST_OPTIONS,
- [],
- 'deleted',
- ],
- } );
- reduxDispatch(
- successNotice( __( 'The site has been restored.' ), {
- duration: 3000,
- } )
- );
- },
- onError: ( error ) => {
- if ( error.status === 403 ) {
- reduxDispatch(
- errorNotice( __( 'Only an administrator can restore a deleted site.' ), {
- duration: 5000,
- } )
- );
- } else {
- reduxDispatch(
- errorNotice( __( 'We were unable to restore the site.' ), { duration: 5000 } )
- );
- }
- },
- } );
-
- const handleRestoreSite = () => {
- restoreSite( site.ID );
- };
-
if ( site.is_deleted ) {
return (
{ __( 'Deleted' ) }
- { isRestoring ? (
-
- ) : (
-
- { __( 'Restore' ) }
-
- ) }
);
}
diff --git a/client/sites/components/sites-dataviews/style.scss b/client/sites/components/sites-dataviews/style.scss
index 9aaec4df8e1a6..12f4907bb49b1 100644
--- a/client/sites/components/sites-dataviews/style.scss
+++ b/client/sites/components/sites-dataviews/style.scss
@@ -7,22 +7,25 @@
overflow: hidden;
padding-bottom: 0;
-
- .dataviews-view-table__row td:last-of-type {
- pointer-events: none;
-
- button,
- a {
- pointer-events: auto;
- }
- }
-
.sites-dataviews__site {
display: flex;
flex-direction: row;
padding-bottom: 8px;
padding-top: 8px;
+ // not to cut off the focus outline
+ padding-left: 2px;
overflow: hidden;
+ cursor: pointer;
+
+ &:hover,
+ &:focus {
+ .sites-dataviews__site-title {
+ color: var(--color-accent);
+ }
+ .sites-dataviews__site-url {
+ color: var(--color-accent);
+ }
+ }
.list-tile {
margin-right: 0;
@@ -35,7 +38,7 @@
}
.sites-dataviews__site-name {
- align-self: flex-start;
+ align-self: center;
display: inline-block;
text-align: left;
text-overflow: ellipsis;
@@ -52,31 +55,10 @@
white-space: nowrap;
}
- .dataviews-view-list .sites-dataviews__site-url,
- .dataviews-view-list .sites-dataviews__site-wp-admin-url,
- .dataviews-view-table .sites-dataviews__site-url,
- .dataviews-view-table .sites-dataviews__site-wp-admin-url {
+ .sites-dataviews__site-url {
color: var(--color-neutral-70);
font-size: rem(14px);
font-weight: 400;
-
- &:focus {
- border-radius: 2px;
- box-shadow: 0 0 0 2px var(--color-primary-light);
- }
-
- &:visited {
- color: var(--color-neutral-70);
- }
-
- &:hover,
- &:hover:visited {
- color: var(--color-link);
- }
-
- svg {
- vertical-align: sub;
- }
}
.site-sort__clickable {
@@ -138,20 +120,12 @@
flex-shrink: 0;
}
- .sites-dataviews__site-name {
- padding: 0;
+ .sites-dataviews__site {
+ padding: 2px 0 2px 2px;
}
- .dataviews-pagination {
- .components-base-control {
- width: unset !important;
- margin-right: 0 !important;
- }
+ .sites-dataviews__site-name {
+ padding: 0;
+ align-self: flex-start;
}
}
-
-.main.sites-dashboard.sites-dashboard__layout:has(.dataviews-pagination) {
- .dataviews-view-table {
- margin: 0;
- }
-}
\ No newline at end of file
diff --git a/client/sites/components/style.scss b/client/sites/components/style.scss
index 6d94e68736665..cf4cdb1416a0e 100644
--- a/client/sites/components/style.scss
+++ b/client/sites/components/style.scss
@@ -114,10 +114,6 @@
}
}
- .components-button.is-compact.has-icon:not(.has-text).dataviews-filters-button {
- min-width: 40px;
- }
-
.item-preview__content {
padding: 10px 10px 88px; /* 88px matches the padding from PR #39201. */
@@ -198,13 +194,6 @@
}
}
- thead .dataviews-view-table__row th span {
- font-size: rem(11px);
- font-weight: 500;
- line-height: 14px;
- color: var(--color-accent-80);
- }
-
.sites-overview__content-wrapper {
max-width: none;
}
diff --git a/client/sites/controller.tsx b/client/sites/controller.tsx
index 8790e0a7946e7..e63f7e6291d7b 100644
--- a/client/sites/controller.tsx
+++ b/client/sites/controller.tsx
@@ -2,13 +2,14 @@ import page from '@automattic/calypso-router';
import { siteLaunchStatusGroupValues } from '@automattic/sites';
import { Global, css } from '@emotion/react';
import { removeQueryArgs } from '@wordpress/url';
+import i18n from 'i18n-calypso';
import AsyncLoad from 'calypso/components/async-load';
import PageViewTracker from 'calypso/lib/analytics/page-view-tracker';
-import { removeNotice } from 'calypso/state/notices/actions';
+import { removeNotice, successNotice } from 'calypso/state/notices/actions';
import { setAllSitesSelected } from 'calypso/state/ui/actions';
import { getSelectedSite } from 'calypso/state/ui/selectors';
import SitesDashboard from './components/sites-dashboard';
-import { areHostingFeaturesSupported } from './features';
+import { areHostingFeaturesSupported } from './hosting-features/features';
import type { Context, Context as PageJSContext } from '@automattic/calypso-router';
const getStatusFilterValue = ( status?: string ) => {
@@ -87,10 +88,6 @@ export function sitesDashboard( context: Context, next: () => void ) {
}
}
- .main.sites-dashboard.sites-dashboard__layout:has( .dataviews-pagination ) {
- padding-bottom: 0;
- }
-
// Update body margin to account for the sidebar width
@media only screen and ( min-width: 782px ) {
div.layout.is-global-sidebar-visible {
@@ -160,3 +157,22 @@ export function redirectToHostingFeaturesIfNotAtomic( context: PageJSContext, ne
next();
}
+
+export function showHostingFeaturesNoticeIfPresent( context: PageJSContext, next: () => void ) {
+ // Update the url and show the notice after a redirect
+ if ( context.query && context.query.hosting_features === 'activated' ) {
+ context.store.dispatch(
+ successNotice( i18n.translate( 'Hosting features activated successfully!' ), {
+ displayOnNextPage: true,
+ } )
+ );
+ // Remove query param without triggering a re-render
+ window.history.replaceState(
+ null,
+ '',
+ removeQueryArgs( window.location.href, 'hosting_features' )
+ );
+ }
+
+ next();
+}
diff --git a/client/sites/hooks/use-restore-site-mutation.ts b/client/sites/hooks/use-restore-site-mutation.ts
index dc56fca33c186..02ee01e0706af 100644
--- a/client/sites/hooks/use-restore-site-mutation.ts
+++ b/client/sites/hooks/use-restore-site-mutation.ts
@@ -12,7 +12,6 @@ interface APIError {
interface APIResponse {
success: true;
}
-
function restoreSite( siteId: number ) {
return wpcom.req.post( {
method: 'put',
diff --git a/client/sites/hosting-features/components/hosting-activation-button.tsx b/client/sites/hosting-features/components/hosting-activation-button.tsx
new file mode 100644
index 0000000000000..228ef8612198c
--- /dev/null
+++ b/client/sites/hosting-features/components/hosting-activation-button.tsx
@@ -0,0 +1,68 @@
+import { FEATURE_SFTP } from '@automattic/calypso-products';
+import page from '@automattic/calypso-router';
+import { Dialog } from '@automattic/components';
+import { addQueryArgs } from '@wordpress/url';
+import { translate } from 'i18n-calypso';
+import { useState } from 'react';
+import EligibilityWarnings from 'calypso/blocks/eligibility-warnings';
+import { HostingHeroButton } from 'calypso/components/hosting-hero';
+import { useSelector, useDispatch } from 'calypso/state';
+import { recordTracksEvent } from 'calypso/state/analytics/actions';
+import { getSelectedSiteId } from 'calypso/state/ui/selectors';
+
+interface HostingActivationButtonProps {
+ redirectUrl?: string;
+}
+
+export default function HostingActivationButton( { redirectUrl }: HostingActivationButtonProps ) {
+ const dispatch = useDispatch();
+ const { searchParams } = new URL( document.location.toString() );
+ const showActivationModal = searchParams.get( 'activate' ) !== null;
+ const [ showEligibility, setShowEligibility ] = useState( showActivationModal );
+
+ const siteId = useSelector( getSelectedSiteId );
+
+ const handleTransfer = ( options: { geo_affinity?: string } ) => {
+ dispatch( recordTracksEvent( 'calypso_hosting_features_activate_confirm' ) );
+ const params = new URLSearchParams( {
+ siteId: String( siteId ),
+ redirect_to: addQueryArgs( redirectUrl, {
+ hosting_features: 'activated',
+ } ),
+ feature: FEATURE_SFTP,
+ initiate_transfer_context: 'hosting',
+ initiate_transfer_geo_affinity: options.geo_affinity || '',
+ } );
+ page( `/setup/transferring-hosted-site?${ params }` );
+ };
+
+ return (
+ <>
+
{
+ dispatch( recordTracksEvent( 'calypso_hosting_features_activate_click' ) );
+ return setShowEligibility( true );
+ } }
+ >
+ { translate( 'Activate now' ) }
+
+
+
+ >
+ );
+}
diff --git a/client/sites/hosting-features/components/hosting-activation.tsx b/client/sites/hosting-features/components/hosting-activation.tsx
new file mode 100644
index 0000000000000..b5460e77ec392
--- /dev/null
+++ b/client/sites/hosting-features/components/hosting-activation.tsx
@@ -0,0 +1,17 @@
+import { useTranslate } from 'i18n-calypso';
+import { PanelDescription, PanelHeading, PanelSection } from 'calypso/sites/components/panel';
+import HostingActivationButton from './hosting-activation-button';
+
+export default function HostingActivation( { redirectUrl }: { redirectUrl: string } ) {
+ const translate = useTranslate();
+
+ return (
+
+ { translate( 'Activate hosting features' ) }
+
+ { translate( 'Activate now to start using this hosting feature.' ) }
+
+
+
+ );
+}
diff --git a/client/sites/features.ts b/client/sites/hosting-features/features.ts
similarity index 75%
rename from client/sites/features.ts
rename to client/sites/hosting-features/features.ts
index 9711cfb0e10e2..294f7ce2103b7 100644
--- a/client/sites/features.ts
+++ b/client/sites/hosting-features/features.ts
@@ -1,4 +1,4 @@
-import { FEATURE_SFTP } from '@automattic/calypso-products';
+import { FEATURE_SFTP, WPCOM_FEATURES_ATOMIC } from '@automattic/calypso-products';
import { SiteExcerptData } from '@automattic/sites';
import { useSelector } from 'calypso/state';
import getSiteFeatures from 'calypso/state/selectors/get-site-features';
@@ -18,6 +18,17 @@ export function useAreHostingFeaturesSupported() {
return areHostingFeaturesSupported( site );
}
+export function useAreHostingFeaturesSupportedAfterActivation() {
+ const features = useSelectedSiteSelector( getSiteFeatures );
+ const hasAtomicFeature = useSelectedSiteSelector( siteHasFeature, WPCOM_FEATURES_ATOMIC );
+
+ if ( ! features ) {
+ return null;
+ }
+
+ return hasAtomicFeature;
+}
+
export function useAreAdvancedHostingFeaturesSupported() {
const site = useSelector( getSelectedSite );
const features = useSelectedSiteSelector( getSiteFeatures );
diff --git a/client/sites/marketing/tools/index.tsx b/client/sites/marketing/tools/index.tsx
index 499cfb44d7c5b..8609de40cde8a 100644
--- a/client/sites/marketing/tools/index.tsx
+++ b/client/sites/marketing/tools/index.tsx
@@ -1,14 +1,17 @@
+import { recordTracksEvent } from '@automattic/calypso-analytics';
import page from '@automattic/calypso-router';
import { Gridicon } from '@automattic/components';
import { localizeUrl } from '@automattic/i18n-utils';
+import Search from '@automattic/search';
+import { isMobile } from '@automattic/viewport';
import { Button } from '@wordpress/components';
+import clsx from 'clsx';
import { useTranslate } from 'i18n-calypso';
-import { Fragment } from 'react';
+import { Fragment, useMemo, useState } from 'react';
import QueryJetpackPlugins from 'calypso/components/data/query-jetpack-plugins';
import PageViewTracker from 'calypso/lib/analytics/page-view-tracker';
import { pluginsPath } from 'calypso/my-sites/marketing/paths';
-import { useDispatch, useSelector } from 'calypso/state';
-import { recordTracksEvent as recordTracksEventAction } from 'calypso/state/analytics/actions';
+import { useSelector } from 'calypso/state';
import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selectors';
import * as T from 'calypso/types';
import MarketingToolsFeature from './feature';
@@ -19,17 +22,26 @@ import './style.scss';
export default function MarketingTools() {
const translate = useTranslate();
- const dispatch = useDispatch();
- const recordTracksEvent = ( event: string ) => dispatch( recordTracksEventAction( event ) );
+ const [ searchTerm, setSearchTerm ] = useState( '' );
const selectedSiteSlug: T.SiteSlug | null = useSelector( getSelectedSiteSlug );
const siteId = useSelector( getSelectedSiteId ) || 0;
- const marketingFeatures = getMarketingFeaturesData(
- selectedSiteSlug,
- recordTracksEvent,
- translate,
- localizeUrl
- );
+ const marketingFeatures = getMarketingFeaturesData( selectedSiteSlug, translate, localizeUrl );
+
+ const marketingFeaturesFiltered = useMemo( () => {
+ return marketingFeatures.filter(
+ ( feature ) =>
+ feature.title.toLowerCase().includes( searchTerm.toLowerCase() ) ||
+ feature.description.toLowerCase().includes( searchTerm.toLowerCase() )
+ );
+ }, [ searchTerm, marketingFeatures ] );
+
+ const handleSearch = ( term: string ) => {
+ setSearchTerm( term );
+ recordTracksEvent( `calypso_marketing_tools_business_tools_search`, {
+ search_term: term,
+ } );
+ };
const handleBusinessToolsClick = () => {
recordTracksEvent( 'calypso_marketing_tools_business_tools_button_click' );
@@ -43,9 +55,26 @@ export default function MarketingTools() {
-
+
+
+
- { marketingFeatures.map( ( feature, index ) => {
+ { marketingFeaturesFiltered.map( ( feature, index ) => {
return (
void,
translate: ( text: string ) => string,
localizeUrl: ( url: string ) => string
): MarketingToolsFeatureData[] => {
diff --git a/client/sites/marketing/tools/style.scss b/client/sites/marketing/tools/style.scss
index b45dbfc88a93a..da3402e6cfc8a 100644
--- a/client/sites/marketing/tools/style.scss
+++ b/client/sites/marketing/tools/style.scss
@@ -20,6 +20,33 @@
background-color: #fff;
}
+.search-component.marketing-tools__searchbox {
+ box-shadow: 0 0 0 1px var(--studio-gray-10);
+
+ .search-component__input[type="search"] {
+ padding-left: 16px;
+ }
+}
+
+
+
+.marketing-tools__searchbox-container {
+ position: relative;
+ width: 250px;
+ height: 40px;
+ margin-top: 24px;
+ margin-bottom: 16px;
+ &.marketing-tools__searchbox-container--mobile {
+ width: auto;
+ margin-left: 16px;
+ margin-right: 16px;
+ }
+}
+.panel-with-sidebar .marketing-tools__searchbox-container--mobile {
+ margin-left: 0;
+ margin-right: 0;
+}
+
.tools__header-body {
display: flex;
flex-direction: row;
@@ -29,7 +56,7 @@
flex-wrap: wrap;
border-radius: 4px;
background-color: var(--color-neutral-0);
- box-shadow: none;
+ box-shadow: none;
}
.tools__header-info {
diff --git a/client/sites/settings/caching/index.tsx b/client/sites/settings/caching/index.tsx
index f75eabc39f3cc..15f8bbc5acf12 100644
--- a/client/sites/settings/caching/index.tsx
+++ b/client/sites/settings/caching/index.tsx
@@ -1,26 +1,68 @@
+import { WPCOM_FEATURES_ATOMIC, getPlanBusinessTitle } from '@automattic/calypso-products';
+import { addQueryArgs } from '@wordpress/url';
import { useTranslate } from 'i18n-calypso';
+import UpsellNudge from 'calypso/blocks/upsell-nudge';
import InlineSupportLink from 'calypso/components/inline-support-link';
import NavigationHeader from 'calypso/components/navigation-header';
-import Notice from 'calypso/components/notice';
-import { useAreHostingFeaturesSupported } from 'calypso/sites/features';
+import { Panel } from 'calypso/sites/components/panel';
+import HostingActivation from 'calypso/sites/hosting-features/components/hosting-activation';
+import {
+ useAreHostingFeaturesSupported,
+ useAreHostingFeaturesSupportedAfterActivation,
+} from 'calypso/sites/hosting-features/features';
+import { useSelector } from 'calypso/state';
+import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selectors';
import CachingForm from './form';
-import './style.scss';
-
export default function CachingSettings() {
const translate = useTranslate();
const isSupported = useAreHostingFeaturesSupported();
+ const isSupportedAfterActivation = useAreHostingFeaturesSupportedAfterActivation();
+
+ const siteId = useSelector( getSelectedSiteId );
+ const siteSlug = useSelector( getSelectedSiteSlug );
+
+ const renderSetting = () => {
+ if ( isSupported ) {
+ return ;
+ }
+
+ if ( isSupportedAfterActivation === null ) {
+ return null;
+ }
+
+ const redirectUrl = `/sites/settings/caching/${ siteId }`;
+
+ if ( isSupportedAfterActivation ) {
+ return ;
+ }
+
+ const href = addQueryArgs( `/checkout/${ siteId }/business`, {
+ redirect_to: redirectUrl,
+ } );
- const renderNotSupportedNotice = () => {
return (
-
- { translate( 'This setting is not supported for this site.' ) }
-
+ },
+ args: { businessPlanName: getPlanBusinessTitle() },
+ }
+ ) }
+ tracksImpressionName="calypso_settings_caching_upgrade_impression"
+ event="calypso_settings_caching_upgrade_upsell"
+ tracksClickName="calypso_settings_caching_upgrade_click"
+ href={ href }
+ callToAction={ translate( 'Upgrade' ) }
+ feature={ WPCOM_FEATURES_ATOMIC }
+ showIcon
+ />
);
};
return (
-
+
- { isSupported ? : renderNotSupportedNotice() }
-
+ { renderSetting() }
+
);
}
diff --git a/client/sites/settings/caching/style.scss b/client/sites/settings/caching/style.scss
deleted file mode 100644
index 16661f0c717a3..0000000000000
--- a/client/sites/settings/caching/style.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.tools-caching {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
diff --git a/client/sites/settings/controller.tsx b/client/sites/settings/controller.tsx
index 8a842ccbff446..cb97be2bed810 100644
--- a/client/sites/settings/controller.tsx
+++ b/client/sites/settings/controller.tsx
@@ -2,10 +2,7 @@ import { __ } from '@wordpress/i18n';
import { useSelector } from 'react-redux';
import { getSelectedSiteSlug } from 'calypso/state/ui/selectors';
import { SidebarItem, Sidebar, PanelWithSidebar } from '../components/panel-sidebar';
-import {
- useAreAdvancedHostingFeaturesSupported,
- useAreHostingFeaturesSupported,
-} from '../features';
+import { useAreAdvancedHostingFeaturesSupported } from '../hosting-features/features';
import AdministrationSettings from './administration';
import useIsAdministrationSettingSupported from './administration/hooks/use-is-administration-setting-supported';
import DeleteSite from './administration/tools/delete-site';
@@ -22,7 +19,6 @@ export function SettingsSidebar() {
const shouldShowAdministration = useIsAdministrationSettingSupported();
- const shouldShowHostingFeatures = useAreHostingFeaturesSupported();
const shouldShowAdvancedHostingFeatures = useAreAdvancedHostingFeaturesSupported();
return (
@@ -34,12 +30,7 @@ export function SettingsSidebar() {
>
{ __( 'Administration' ) }
-
- { __( 'Caching' ) }
-
+ { __( 'Caching' ) }
{
- const sitePlans = useSitePlans( { siteId } );
+ const sitePlans = useSitePlans( { coupon: undefined, siteId } );
return useMemo(
() =>
diff --git a/packages/data-stores/src/plans/hooks/use-intro-offers.ts b/packages/data-stores/src/plans/hooks/use-intro-offers.ts
index 8c0c3ee44a87d..7cccdf2732d2e 100644
--- a/packages/data-stores/src/plans/hooks/use-intro-offers.ts
+++ b/packages/data-stores/src/plans/hooks/use-intro-offers.ts
@@ -20,7 +20,7 @@ interface Props {
* or `undefined` if we haven't observed any metadata yet
*/
const useIntroOffers = ( { siteId, coupon }: Props ): IntroOffersIndex | undefined => {
- const sitePlans = useSitePlans( { siteId } );
+ const sitePlans = useSitePlans( { coupon: undefined, siteId } );
const plans = usePlans( { coupon } );
return useMemo( () => {
diff --git a/packages/data-stores/src/plans/hooks/use-pricing-meta-for-grid-plans.ts b/packages/data-stores/src/plans/hooks/use-pricing-meta-for-grid-plans.ts
index 22f94d4034a95..a110a0f7c389e 100644
--- a/packages/data-stores/src/plans/hooks/use-pricing-meta-for-grid-plans.ts
+++ b/packages/data-stores/src/plans/hooks/use-pricing-meta-for-grid-plans.ts
@@ -83,7 +83,7 @@ const usePricingMetaForGridPlans = ( {
// plans - should have a definition for all plans, being the main source of API data
const plans = Plans.usePlans( { coupon } );
// sitePlans - unclear if all plans are included
- const sitePlans = Plans.useSitePlans( { siteId } );
+ const sitePlans = Plans.useSitePlans( { coupon, siteId } );
const currentPlan = Plans.useCurrentPlan( { siteId } );
const introOffers = Plans.useIntroOffers( { siteId, coupon } );
const purchasedPlan = Purchases.useSitePurchaseById( {
@@ -227,7 +227,11 @@ const usePricingMetaForGridPlans = ( {
};
// Do not return discounted prices if discount is due to plan proration
+ // If there is, however, a sale coupon, show the discounted price
+ // without proration. This isn't ideal, but is intentional. Because of
+ // this, the price will differ between the plans grid and checkout screen.
if (
+ ! sitePlan?.pricing?.hasSaleCoupon &&
! withProratedDiscounts &&
sitePlan?.pricing?.costOverrides?.[ 0 ]?.overrideCode ===
COST_OVERRIDE_REASONS.RECENT_PLAN_PRORATION
diff --git a/packages/data-stores/src/plans/queries/lib/use-query-keys-factory.ts b/packages/data-stores/src/plans/queries/lib/use-query-keys-factory.ts
index 1cb54697b58ae..bdb5f3671fa6b 100644
--- a/packages/data-stores/src/plans/queries/lib/use-query-keys-factory.ts
+++ b/packages/data-stores/src/plans/queries/lib/use-query-keys-factory.ts
@@ -1,5 +1,9 @@
const useQueryKeysFactory = () => ( {
- sitePlans: ( siteId?: string | number | null ) => [ 'site-plans', siteId ],
+ sitePlans: ( coupon?: string, siteId?: string | number | null ) => [
+ 'site-plans',
+ siteId,
+ coupon,
+ ],
plans: ( coupon?: string ) => [ 'plans', coupon ],
} );
diff --git a/packages/data-stores/src/plans/queries/use-site-plans.ts b/packages/data-stores/src/plans/queries/use-site-plans.ts
index 8a309bbc19a23..15a022a82d845 100644
--- a/packages/data-stores/src/plans/queries/use-site-plans.ts
+++ b/packages/data-stores/src/plans/queries/use-site-plans.ts
@@ -14,6 +14,11 @@ interface PricedAPISitePlansIndex {
}
interface Props {
+ /**
+ * To match the use-plans hook, `coupon` is required on purpose to mitigate risk of not passing
+ * something through when we should
+ */
+ coupon: string | undefined;
siteId: string | number | null | undefined;
}
@@ -22,15 +27,18 @@ interface Props {
* - Plans from `/sites/[siteId]/plans`, unlike `/plans`, are returned indexed by product_id, and do not include that in the plan's payload.
* - UI works with product/plan slugs everywhere, so returned index is transformed to be keyed by product_slug
*/
-function useSitePlans( { siteId }: Props ): UseQueryResult< SitePlansIndex > {
+function useSitePlans( { coupon, siteId }: Props ): UseQueryResult< SitePlansIndex > {
const queryKeys = useQueryKeysFactory();
+ const params = new URLSearchParams();
+ coupon && params.append( 'coupon_code', coupon );
return useQuery( {
- queryKey: queryKeys.sitePlans( siteId ),
+ queryKey: queryKeys.sitePlans( coupon, siteId ),
queryFn: async (): Promise< SitePlansIndex > => {
const data: PricedAPISitePlansIndex = await wpcomRequest( {
path: `/sites/${ encodeURIComponent( siteId as string ) }/plans`,
apiVersion: '1.3',
+ query: params.toString(),
} );
return Object.fromEntries(
@@ -52,6 +60,7 @@ function useSitePlans( { siteId }: Props ): UseQueryResult< SitePlansIndex > {
hasRedeemedDomainCredit: plan?.has_redeemed_domain_credit,
purchaseId: plan.id ? Number( plan.id ) : undefined,
pricing: {
+ hasSaleCoupon: plan.has_sale_coupon,
currencyCode: plan.currency_code,
introOffer: unpackIntroOffer( plan ),
costOverrides: unpackCostOverrides( plan ),
diff --git a/packages/data-stores/src/plans/types.ts b/packages/data-stores/src/plans/types.ts
index 106722bf58c6f..d4d748d7e9098 100644
--- a/packages/data-stores/src/plans/types.ts
+++ b/packages/data-stores/src/plans/types.ts
@@ -120,6 +120,7 @@ export interface PlanPricing {
}
export interface SitePlanPricing extends Omit< PlanPricing, 'billPeriod' > {
+ hasSaleCoupon?: boolean;
costOverrides?: CostOverride[];
}
@@ -269,6 +270,7 @@ export interface PricedAPIPlan extends PricedAPIPlanPricing, PricedAPIPlanIntrod
export interface PricedAPISitePlan
extends PricedAPISitePlanPricing,
PricedAPIPlanIntroductoryOffer {
+ has_sale_coupon?: boolean;
/* product_id: number; // not included in the plan's payload */
product_slug: StorePlanSlug;
current_plan?: boolean;
diff --git a/packages/domain-picker/src/utils/index.ts b/packages/domain-picker/src/utils/index.ts
index 4fc6ae5d3a35f..085ecf1bdbcc8 100644
--- a/packages/domain-picker/src/utils/index.ts
+++ b/packages/domain-picker/src/utils/index.ts
@@ -5,6 +5,7 @@ import {
WOOEXPRESS_FLOW,
DOMAIN_FOR_GRAVATAR_FLOW,
isDomainForGravatarFlow,
+ isHundredYearPlanFlow,
isHundredYearDomainFlow,
} from '@automattic/onboarding';
import type { DomainSuggestions } from '@automattic/data-stores';
@@ -66,7 +67,7 @@ export function getDomainSuggestionsVendor(
if ( isDomainForGravatarFlow( options.flowName ) ) {
return 'gravatar';
}
- if ( isHundredYearDomainFlow( options.flowName ) ) {
+ if ( isHundredYearPlanFlow( options.flowName ) || isHundredYearDomainFlow( options.flowName ) ) {
return '100-year-domains';
}
if ( options.flowName === LINK_IN_BIO_TLD_FLOW ) {
diff --git a/packages/help-center/src/components/_variables.scss b/packages/help-center/src/components/_variables.scss
index 05992fe3a7568..8cd56a50fa2bb 100644
--- a/packages/help-center/src/components/_variables.scss
+++ b/packages/help-center/src/components/_variables.scss
@@ -1,2 +1,3 @@
$head-foot-height: 50px;
$help-center-z-index: 100000;
+$help-center-blue: #3858e9;
diff --git a/packages/help-center/src/components/help-center-chat-history.tsx b/packages/help-center/src/components/help-center-chat-history.tsx
index b85cef158e562..39c756947a4f7 100644
--- a/packages/help-center/src/components/help-center-chat-history.tsx
+++ b/packages/help-center/src/components/help-center-chat-history.tsx
@@ -68,12 +68,12 @@ export const HelpCenterChatHistory = () => {
const [ conversations, setConversations ] = useState< ZendeskConversation[] >( [] );
const [ selectedTab, setSelectedTab ] = useState( TAB_STATES.recent );
const { getConversations } = useSmooch();
- const { data: supportInteractionsResolved } = useGetSupportInteractions(
- 'zendesk',
- 100,
- 'resolved'
- );
- const { data: supportInteractionsOpen } = useGetSupportInteractions( 'zendesk', 10, 'open' );
+ const { data: supportInteractionsResolved, isLoading: isLoadingResolvedInteractions } =
+ useGetSupportInteractions( 'zendesk', 100, 'resolved' );
+ const { data: supportInteractionsClosed, isLoading: isLoadingClosedInteractions } =
+ useGetSupportInteractions( 'zendesk', 100, 'closed' );
+ const { data: supportInteractionsOpen, isLoading: isLoadingOpenInteractions } =
+ useGetSupportInteractions( 'zendesk', 10, 'open' );
const { isChatLoaded, unreadCount } = useSelect( ( select ) => {
const store = select( HELP_CENTER_STORE ) as HelpCenterSelect;
@@ -89,16 +89,15 @@ export const HelpCenterChatHistory = () => {
const { setUnreadCount } = useDataStoreDispatch( HELP_CENTER_STORE );
useEffect( () => {
- if (
- isChatLoaded &&
- getConversations &&
- ( ( supportInteractionsResolved && supportInteractionsResolved?.length > 0 ) ||
- ( supportInteractionsOpen && supportInteractionsOpen?.length > 0 ) )
- ) {
+ const isLoadingInteractions =
+ isLoadingResolvedInteractions || isLoadingClosedInteractions || isLoadingOpenInteractions;
+
+ if ( isChatLoaded && getConversations && ! isLoadingInteractions ) {
const conversations = getConversations();
const supportInteractions = [
...( supportInteractionsResolved || [] ),
...( supportInteractionsOpen || [] ),
+ ...( supportInteractionsClosed || [] ),
];
const filteredConversations = getConversationsFromSupportInteractions(
diff --git a/packages/help-center/src/components/help-center-chat.tsx b/packages/help-center/src/components/help-center-chat.tsx
index a92ef33ad9e81..f9468e4533345 100644
--- a/packages/help-center/src/components/help-center-chat.tsx
+++ b/packages/help-center/src/components/help-center-chat.tsx
@@ -37,6 +37,8 @@ export function HelpCenterChat( {
}
}, [] );
+ const odieVersion = config.isEnabled( 'help-center-experience' ) ? '14.0.3' : null;
+
return (
}
+ version={ odieVersion }
>
diff --git a/packages/help-center/src/components/help-center-header.scss b/packages/help-center/src/components/help-center-header.scss
index b06e63e0c8c62..e08e1a8979ba9 100644
--- a/packages/help-center/src/components/help-center-header.scss
+++ b/packages/help-center/src/components/help-center-header.scss
@@ -89,3 +89,39 @@
}
}
}
+
+.clear-conversation__wrapper {
+ padding: 4px 0;
+ text-align: center;
+
+ svg {
+ fill: var(--studio-gray-70);
+ }
+
+ :hover, :focus {
+ background-color: $help-center-blue;
+ color: var(--studio-white);
+
+ svg {
+ fill: var(--studio-white);
+ }
+ }
+
+ button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ padding: 8px;
+ cursor: pointer;
+ gap: 4px;
+ color: var(--studio-gray-70);
+ border: none;
+ background: unset;
+
+ div {
+ margin-bottom: 2px;
+ font-size: 0.875rem;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/help-center/src/components/help-center-header.tsx b/packages/help-center/src/components/help-center-header.tsx
index 396bb0700316a..e3e98e5b04ef1 100644
--- a/packages/help-center/src/components/help-center-header.tsx
+++ b/packages/help-center/src/components/help-center-header.tsx
@@ -12,7 +12,6 @@ import { useI18n } from '@wordpress/react-i18n';
import clsx from 'clsx';
import { Route, Routes, useLocation, useSearchParams } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
-import PopoverMenuItem from 'calypso/components/popover-menu/item';
import { usePostByUrl } from '../hooks';
import { useResetSupportInteraction } from '../hooks/use-reset-support-interaction';
import { DragIcon } from '../icons';
@@ -85,13 +84,12 @@ const ChatEllipsisMenu = () => {
popoverClassName="help-center help-center__container-header-menu"
position="bottom"
>
-
-
- { __( 'Clear Conversation' ) }
-
+
+
+
);
};
diff --git a/packages/odie-client/src/components/closed-conversation-footer/index.tsx b/packages/odie-client/src/components/closed-conversation-footer/index.tsx
new file mode 100644
index 0000000000000..3b7792df107eb
--- /dev/null
+++ b/packages/odie-client/src/components/closed-conversation-footer/index.tsx
@@ -0,0 +1,37 @@
+import { Button } from '@wordpress/components';
+import { comment, Icon } from '@wordpress/icons';
+import { useI18n } from '@wordpress/react-i18n';
+import { v4 as uuidv4 } from 'uuid';
+import { useOdieAssistantContext } from '../../context';
+import { useManageSupportInteraction } from '../../data';
+import './style.scss';
+
+export const ClosedConversationFooter = () => {
+ const { __ } = useI18n();
+ const { trackEvent, chat, shouldUseHelpCenterExperience } = useOdieAssistantContext();
+
+ const { startNewInteraction } = useManageSupportInteraction();
+
+ const handleOnClick = async () => {
+ trackEvent( 'chat_new_from_closed_conversation' );
+ await startNewInteraction( {
+ event_source: 'help-center',
+ event_external_id: uuidv4(),
+ } );
+ };
+
+ if ( ! shouldUseHelpCenterExperience || chat.status !== 'closed' ) {
+ return null;
+ }
+
+ return (
+
+ { __( 'This conversation has been completed', __i18n_text_domain__ ) }
+
+
+
+ );
+};
diff --git a/packages/odie-client/src/components/closed-conversation-footer/style.scss b/packages/odie-client/src/components/closed-conversation-footer/style.scss
new file mode 100644
index 0000000000000..37dd65a94abff
--- /dev/null
+++ b/packages/odie-client/src/components/closed-conversation-footer/style.scss
@@ -0,0 +1,56 @@
+@import "@automattic/typography/styles/variables";
+
+.odie-closed-conversation-footer {
+ border-top: 1px solid var(--studio-gray-5, #DCDCDE);
+ background: var(--studio-white, #FFF);
+ box-sizing: border-box;
+
+ width: 100%;
+ display: flex;
+ padding: 16px;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+
+ span {
+ color: var(--studio-gray-40, #787C82);
+ font-size: $font-body-small;
+ line-height: 20px;
+ }
+
+ .odie-closed-conversation-footer__button {
+ display: flex;
+ height: 40px;
+ padding: 10px 24px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ align-self: stretch;
+
+ border-radius: 4px;
+ border: 1px solid var(--studio-gray-10, #C3C4C7);
+ background: var(--studio-white, #FFF);
+ color: var(--studio-gray-100, #101517);
+
+ svg {
+ color: var(--studio-gray-40);
+ }
+
+ &:hover {
+ border-color: var(--color-neutral-20) !important;
+ }
+
+ &:focus {
+ outline: var(--studio-blue-50, #0675c4) solid 2px !important;
+ }
+
+ &:focus,
+ &:hover {
+ color: var(--studio-gray-100, #101517) !important;
+
+ svg {
+ color: var(--studio-gray-40);
+ }
+ }
+ }
+}
diff --git a/packages/odie-client/src/components/message/messages-container.tsx b/packages/odie-client/src/components/message/messages-container.tsx
index 915295d719bca..db97142a193e9 100644
--- a/packages/odie-client/src/components/message/messages-container.tsx
+++ b/packages/odie-client/src/components/message/messages-container.tsx
@@ -44,7 +44,7 @@ export const MessagesContainer = ( { currentUser }: ChatMessagesProps ) => {
useZendeskMessageListener();
useAutoScroll( messagesContainerRef );
useEffect( () => {
- chat?.status === 'loaded' && setChatLoaded( true );
+ ( chat?.status === 'loaded' || chat?.status === 'closed' ) && setChatLoaded( true );
}, [ chat ] );
const shouldLoadChat: boolean =
diff --git a/packages/odie-client/src/components/message/style_redesign.scss b/packages/odie-client/src/components/message/style_redesign.scss
index 9c04b50bc7d15..8d696aa7bcdc1 100644
--- a/packages/odie-client/src/components/message/style_redesign.scss
+++ b/packages/odie-client/src/components/message/style_redesign.scss
@@ -463,7 +463,7 @@
font-size: $font-body;
line-height: 1;
appearance: none;
- background-color: var(--color-primary);
+ background-color: $blueberry-color;
color: var(--studio-white);
border: 1px none;
display: flex;
@@ -480,6 +480,16 @@
}
}
+ .odie-send-message-input-container {
+ textarea.odie-send-message-input {
+ &:focus {
+ box-shadow: 0 0 0 2px $blueberry-color;
+ outline: none;
+ border-color: transparent;
+ }
+ }
+ }
+
.odie-chatbox-message-avatar-wapuu-liked {
-webkit-animation: wapuu-joy-animation 1300ms both;
animation: wapuu-joy-animation 1300ms both;
@@ -636,4 +646,8 @@
text-align: center;
padding-top: 6px;
}
+
+ .odie-send-message-input-spinner {
+ color: $blueberry-color !important;
+ }
}
diff --git a/packages/odie-client/src/data/use-odie-chat.ts b/packages/odie-client/src/data/use-odie-chat.ts
index 7daea78433fb6..f6b95aafcf1c7 100644
--- a/packages/odie-client/src/data/use-odie-chat.ts
+++ b/packages/odie-client/src/data/use-odie-chat.ts
@@ -2,26 +2,33 @@ import { useQuery } from '@tanstack/react-query';
import apiFetch from '@wordpress/api-fetch';
import wpcomRequest, { canAccessWpcomApis } from 'wpcom-proxy-request';
import { useOdieAssistantContext } from '../context';
-import { OdieChat, ReturnedChat } from '../types';
+import type { OdieChat, ReturnedChat } from '../types';
/**
* Get the ODIE chat and manage the cache to save on API calls.
*/
export const useOdieChat = ( chatId: number | null ) => {
- const { botNameSlug } = useOdieAssistantContext();
+ const { botNameSlug, version } = useOdieAssistantContext();
return useQuery< OdieChat, Error >( {
- queryKey: [ 'odie-chat', botNameSlug, chatId ],
+ queryKey: [ 'odie-chat', botNameSlug, chatId, version ],
queryFn: async (): Promise< OdieChat > => {
+ const queryParams = new URLSearchParams( {
+ page_number: '1',
+ items_per_page: '30',
+ include_feedback: 'true',
+ ...( version && { version } ),
+ } ).toString();
+
const data = (
canAccessWpcomApis()
? await wpcomRequest( {
method: 'GET',
- path: `/odie/chat/${ botNameSlug }/${ chatId }?page_number=1&items_per_page=30&include_feedback=true`,
+ path: `/odie/chat/${ botNameSlug }/${ chatId }?${ queryParams }`,
apiNamespace: 'wpcom/v2',
} )
: await apiFetch( {
- path: `/help-center/odie/chat/${ botNameSlug }/${ chatId }?page_number=1&items_per_page=30&include_feedback=true`,
+ path: `/help-center/odie/chat/${ botNameSlug }/${ chatId }?${ queryParams }`,
method: 'GET',
} )
) as ReturnedChat;
diff --git a/packages/odie-client/src/data/use-send-odie-feedback.ts b/packages/odie-client/src/data/use-send-odie-feedback.ts
index 4cfd65d23b819..4612c379983f5 100644
--- a/packages/odie-client/src/data/use-send-odie-feedback.ts
+++ b/packages/odie-client/src/data/use-send-odie-feedback.ts
@@ -8,7 +8,7 @@ import type { Chat } from '../types';
* @returns useMutation return object.
*/
export const useSendOdieFeedback = () => {
- const { botNameSlug, chat } = useOdieAssistantContext();
+ const { botNameSlug, chat, version } = useOdieAssistantContext();
const queryClient = useQueryClient();
return useMutation( {
@@ -17,7 +17,7 @@ export const useSendOdieFeedback = () => {
method: 'POST',
path: `/odie/chat/${ botNameSlug }/${ chat.odieId }/${ messageId }/feedback`,
apiNamespace: 'wpcom/v2',
- body: { rating_value: ratingValue },
+ body: { rating_value: ratingValue, ...( version && { version } ) },
} );
},
onSuccess: ( data, { messageId, ratingValue } ) => {
diff --git a/packages/odie-client/src/data/use-send-odie-message.ts b/packages/odie-client/src/data/use-send-odie-message.ts
index 8d879a23b4ab7..5ac0ac157a755 100644
--- a/packages/odie-client/src/data/use-send-odie-message.ts
+++ b/packages/odie-client/src/data/use-send-odie-message.ts
@@ -61,12 +61,20 @@ export const useSendOdieMessage = () => {
method: 'POST',
path: `/odie/chat/${ botNameSlug }${ chatIdSegment }`,
apiNamespace: 'wpcom/v2',
- body: { message: message.content, version, context: { selectedSiteId } },
+ body: {
+ message: message.content,
+ ...( version && { version } ),
+ context: { selectedSiteId },
+ },
} )
: await apiFetch( {
path: `/help-center/odie/chat/${ botNameSlug }${ chatIdSegment }`,
method: 'POST',
- data: { message: message.content, version, context: { selectedSiteId } },
+ data: {
+ message: message.content,
+ ...( version && { version } ),
+ context: { selectedSiteId },
+ },
} );
},
onMutate: () => {
diff --git a/packages/odie-client/src/hooks/use-get-combined-chat.ts b/packages/odie-client/src/hooks/use-get-combined-chat.ts
index 759fd0b673f67..4dbc4dc3cfefc 100644
--- a/packages/odie-client/src/hooks/use-get-combined-chat.ts
+++ b/packages/odie-client/src/hooks/use-get-combined-chat.ts
@@ -62,7 +62,7 @@ export const useGetCombinedChat = ( shouldUseHelpCenterExperience: boolean | und
...( conversation.messages as Message[] ),
],
provider: 'zendesk',
- status: 'loaded',
+ status: currentSupportInteraction?.status === 'closed' ? 'closed' : 'loaded',
} );
}
} );
diff --git a/packages/odie-client/src/index.tsx b/packages/odie-client/src/index.tsx
index ef0a310fafc71..f60948866338c 100644
--- a/packages/odie-client/src/index.tsx
+++ b/packages/odie-client/src/index.tsx
@@ -1,5 +1,9 @@
+import { HelpCenterSelect } from '@automattic/data-stores';
+import { HELP_CENTER_STORE } from '@automattic/help-center/src/stores';
+import { useSelect } from '@wordpress/data';
import clsx from 'clsx';
import { useEffect } from 'react';
+import { ClosedConversationFooter } from './components/closed-conversation-footer';
import { MessagesContainer } from './components/message/messages-container';
import { OdieSendMessageButton } from './components/send-message-input';
import { useOdieAssistantContext, OdieAssistantProvider } from './context';
@@ -8,6 +12,12 @@ import './style.scss';
export const OdieAssistant: React.FC = () => {
const { trackEvent, shouldUseHelpCenterExperience, currentUser } = useOdieAssistantContext();
+ const { currentSupportInteraction } = useSelect( ( select ) => {
+ const store = select( HELP_CENTER_STORE ) as HelpCenterSelect;
+ return {
+ currentSupportInteraction: store.getCurrentSupportInteraction(),
+ };
+ }, [] );
useEffect( () => {
trackEvent( 'chatbox_view' );
@@ -23,7 +33,8 @@ export const OdieAssistant: React.FC = () => {
-
+ { currentSupportInteraction?.status !== 'closed' &&
}
+ { currentSupportInteraction?.status === 'closed' &&
}
);
};
diff --git a/packages/odie-client/src/types.ts b/packages/odie-client/src/types.ts
index 4a510af79a617..012cddc996eca 100644
--- a/packages/odie-client/src/types.ts
+++ b/packages/odie-client/src/types.ts
@@ -148,7 +148,7 @@ export type Message = {
created_at?: string;
};
-export type ChatStatus = 'loading' | 'loaded' | 'sending' | 'dislike' | 'transfer';
+export type ChatStatus = 'loading' | 'loaded' | 'sending' | 'dislike' | 'transfer' | 'closed';
export type ReturnedChat = { chat_id: number; messages: Message[]; wpcom_user_id: number };
diff --git a/packages/onboarding/src/utils/flows.ts b/packages/onboarding/src/utils/flows.ts
index 48275c22f5031..a8972468615db 100644
--- a/packages/onboarding/src/utils/flows.ts
+++ b/packages/onboarding/src/utils/flows.ts
@@ -208,6 +208,10 @@ export const isDomainForGravatarFlow = ( flowName: string | null | undefined ) =
return Boolean( flowName && [ DOMAIN_FOR_GRAVATAR_FLOW ].includes( flowName ) );
};
+export const isHundredYearPlanFlow = ( flowName: string | null | undefined ) => {
+ return Boolean( flowName && [ HUNDRED_YEAR_PLAN_FLOW ].includes( flowName ) );
+};
+
export const isHundredYearDomainFlow = ( flowName: string | null | undefined ) => {
return Boolean( flowName && [ HUNDRED_YEAR_DOMAIN_FLOW ].includes( flowName ) );
};
diff --git a/packages/plans-grid-next/src/types.ts b/packages/plans-grid-next/src/types.ts
index 6cc5c4f0c5568..208b27d0e6c71 100644
--- a/packages/plans-grid-next/src/types.ts
+++ b/packages/plans-grid-next/src/types.ts
@@ -264,7 +264,6 @@ export type PlanTypeSelectorProps = {
basePlansPath?: string | null;
intervalType: UrlFriendlyTermType;
customerType: string;
- withDiscount?: string;
enableStickyBehavior?: boolean;
stickyPlanTypeSelectorOffset?: number;
onPlanIntervalUpdate: ( interval: SupportedUrlFriendlyTermType ) => void;