diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTilePagesMetric.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTilePagesMetric.js index da0fe52fed4..13ceca2ffb2 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTilePagesMetric.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTilePagesMetric.js @@ -49,17 +49,12 @@ import { import { ERROR_CODE_MISSING_REQUIRED_SCOPE } from '@/js/util/errors'; import BadgeWithTooltip from '@/js/components/BadgeWithTooltip'; import AudienceTilePagesMetricContent from './AudienceTilePagesMetricContent'; -import AudienceErrorModal from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceErrorModal'; import { AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION } from '@/js/googlesitekit/widgets/default-areas'; import useViewContext from '@/js/hooks/useViewContext'; import { trackEvent } from '@/js/util'; import useFormValue from '@/js/hooks/useFormValue'; export default function AudienceTilePagesMetric( { - // TODO: The prop `audienceTileNumber` is part of a temporary workaround to ensure `AudienceErrorModal` is only rendered once - // within `AudienceTilesWidget`. This should be removed once the `AudienceErrorModal` render is extracted - // from `AudienceTilePagesMetric` and it's rendered once at a higher level instead. See https://github.com/google/site-kit-wp/issues/9543. - audienceTileNumber, audienceSlug, TileIcon, title, @@ -107,37 +102,25 @@ export default function AudienceTilePagesMetric( { select( MODULES_ANALYTICS_4 ).isFetchingSyncAvailableCustomDimensions() ); - const customDimensionError = useSelect( ( select ) => + // Error state handled at parent level; keep selector side-effects minimal if needed. + useSelect( ( select ) => select( MODULES_ANALYTICS_4 ).getCreateCustomDimensionError( postTypeDimension ) ); - const propertyID = useSelect( ( select ) => - select( MODULES_ANALYTICS_4 ).getPropertyID() - ); - - const { clearError } = useDispatch( MODULES_ANALYTICS_4 ); const { setValues } = useDispatch( CORE_FORMS ); - const { setPermissionScopeError, clearPermissionScopeError } = - useDispatch( CORE_USER ); + const { setPermissionScopeError } = useDispatch( CORE_USER ); - const isRetryingCustomDimensionCreate = useFormValue( - AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, - 'isRetrying' - ); - - const autoSubmit = useFormValue( - AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, - 'autoSubmit' - ); + // Retry state now lifted to parent component. + useFormValue( AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, 'isRetrying' ); - const setupErrorCode = useSelect( ( select ) => - select( CORE_SITE ).getSetupErrorCode() - ); - const { setSetupErrorCode } = useDispatch( CORE_SITE ); + useFormValue( AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, 'autoSubmit' ); + useSelect( ( select ) => select( CORE_SITE ).getSetupErrorCode() ); + useDispatch( CORE_SITE ); // retain dispatch instance (no local usage after refactor) - const hasOAuthError = autoSubmit && setupErrorCode === 'access_denied'; + // OAuth error handled by parent; local component no longer consumes this. + // OAuth error previously used for modal; no longer needed after modal extraction. const onCreateCustomDimension = useCallback( ( { isRetrying } = {} ) => { @@ -173,24 +156,7 @@ export default function AudienceTilePagesMetric( { ] ); - const onCancel = useCallback( () => { - setValues( AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, { - autoSubmit: false, - isRetrying: false, - } ); - setSetupErrorCode( null ); - clearPermissionScopeError(); - clearError( 'createCustomDimension', [ - propertyID, - CUSTOM_DIMENSION_DEFINITIONS.googlesitekit_post_type, - ] ); - }, [ - clearError, - clearPermissionScopeError, - propertyID, - setSetupErrorCode, - setValues, - ] ); + // Cancel handler moved to parent for centralized modal control. const isMobileBreakpoint = [ BREAKPOINT_SMALL, BREAKPOINT_TABLET ].includes( breakpoint @@ -235,42 +201,12 @@ export default function AudienceTilePagesMetric( { onCreateCustomDimension={ onCreateCustomDimension } isSaving={ isSaving } /> - { /* - TODO: The `audienceTileNumber` check is part of a temporary workaround to ensure `AudienceErrorModal` is only rendered once - within `AudienceTilesWidget`. This should be removed, and the `AudienceErrorModal` render extracted - from here to be rendered once at a higher level instead. See https://github.com/google/site-kit-wp/issues/9543. - */ } - { audienceTileNumber === 0 && - ( ( customDimensionError && ! isSaving ) || - ( isRetryingCustomDimensionCreate && - ! isAutoCreatingCustomDimensionsForAudience ) || - hasOAuthError ) && ( - - onCreateCustomDimension( { isRetrying: true } ) - } - onCancel={ onCancel } - inProgress={ isSaving } - hasOAuthError={ hasOAuthError } - trackEventCategory={ `${ viewContext }_audiences-top-content-cta` } - /> - ) } ); } AudienceTilePagesMetric.propTypes = { - audienceTileNumber: PropTypes.number, audienceSlug: PropTypes.string.isRequired, TileIcon: PropTypes.elementType.isRequired, title: PropTypes.string.isRequired, diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.js index d78fe5e7497..5522e16fef1 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.js @@ -55,10 +55,6 @@ import BadgeWithTooltip from '@/js/components/BadgeWithTooltip'; import useViewContext from '@/js/hooks/useViewContext'; import AudienceTileZeroData from './AudienceTileZeroData'; export default function AudienceTile( { - // TODO: The prop `audienceTileNumber` is part of a temporary workaround to ensure `AudienceErrorModal` is only rendered once - // within `AudienceTilesWidget`. This should be removed once the `AudienceErrorModal` render is extracted - // from `AudienceTilePagesMetric` and it's rendered once at a higher level instead. See https://github.com/google/site-kit-wp/issues/9543. - audienceTileNumber = 0, audienceSlug, title, infoTooltip, @@ -284,7 +280,6 @@ export default function AudienceTile( { ( postTypeDimensionExists && ! hasInvalidCustomDimensionError ) ) && ( + { visibleAudiences.map( ( audienceResourceName, index ) => { + // Respect tabbed breakpoint visibility rules. + if ( isTabbedBreakpoint && index !== activeTileIndex ) { + return null; + } + + const tileData = getAudienceTileData( + audienceResourceName, + index + ); + const { + audienceName, + audienceSlug, + visitors, + prevVisitors, + visitsPerVisitors, + prevVisitsPerVisitors, + pagesPerVisit, + prevPagesPerVisit, + pageviews, + prevPageviews, + topCities, + topContent, + topContentTitles, + isZeroData, + isPartialData, + } = tileData; + + // While reports or zero/partial flags are still undefined, show loading state. + const reportsNotReady = + loading || + ! topCitiesReportsLoaded?.[ audienceResourceName ] || + ! topContentReportsLoaded?.[ audienceResourceName ] || + ! topContentPageTitlesReportsLoaded?.[ + audienceResourceName + ] || + isZeroData === undefined || + isPartialData === undefined; + + if ( reportsNotReady ) { + return ( + + + + ); + } + + // Show per-tile error component if errors exist. + const perTileErrors = + individualTileErrors?.[ audienceResourceName ]; + if ( perTileErrors?.length > 0 ) { + return ( + + ); + } + + // Filter out rows with unset values for top cities. + const filteredTopCitiesRows = topCities?.rows + ? reportRowsWithSetValues( topCities.rows ) + : []; + + // Build top cities structure (limit to first 3 entries). + const topCitiesProp = { + dimensionValues: [ + filteredTopCitiesRows?.[ 0 ]?.dimensionValues?.[ 0 ], + filteredTopCitiesRows?.[ 1 ]?.dimensionValues?.[ 0 ], + filteredTopCitiesRows?.[ 2 ]?.dimensionValues?.[ 0 ], + ], + metricValues: [ + filteredTopCitiesRows?.[ 0 ]?.metricValues?.[ 0 ], + filteredTopCitiesRows?.[ 1 ]?.metricValues?.[ 0 ], + filteredTopCitiesRows?.[ 2 ]?.metricValues?.[ 0 ], + ], + total: visitors, + }; + + // Build top content structure (limit to first 3 rows). + const topContentProp = { + dimensionValues: [ + topContent?.rows?.[ 0 ]?.dimensionValues?.[ 0 ], + topContent?.rows?.[ 1 ]?.dimensionValues?.[ 0 ], + topContent?.rows?.[ 2 ]?.dimensionValues?.[ 0 ], + ], + metricValues: [ + topContent?.rows?.[ 0 ]?.metricValues?.[ 0 ], + topContent?.rows?.[ 1 ]?.metricValues?.[ 0 ], + topContent?.rows?.[ 2 ]?.metricValues?.[ 0 ], + ], + }; + + return ( + + } + visitors={ { + currentValue: visitors, + previousValue: prevVisitors, + } } + visitsPerVisitor={ { + currentValue: visitsPerVisitors, + previousValue: prevVisitsPerVisitors, + } } + pagesPerVisit={ { + currentValue: pagesPerVisit, + previousValue: prevPagesPerVisit, + } } + pageviews={ { + currentValue: pageviews, + previousValue: prevPageviews, + } } + percentageOfTotalPageViews={ + totalPageviews !== 0 + ? pageviews / totalPageviews + : 0 + } + topCities={ topCitiesProp } + topContent={ topContentProp } + topContentTitles={ topContentTitles } + hasInvalidCustomDimensionError={ + hasInvalidCustomDimensionError + } + Widget={ Widget } + audienceResourceName={ audienceResourceName } + isZeroData={ isZeroData } + isPartialData={ isPartialData } + isTileHideable={ visibleAudiences.length > 1 } + onHideTile={ () => + handleDismiss( audienceResourceName ) + } + /> + ); + } ) } + + ); +} + +AudienceTilesList.propTypes = { + activeTileIndex: PropTypes.number.isRequired, + visibleAudiences: PropTypes.array.isRequired, + loading: PropTypes.bool.isRequired, + topCitiesReportsLoaded: PropTypes.object.isRequired, + topContentReportsLoaded: PropTypes.object.isRequired, + topContentPageTitlesReportsLoaded: PropTypes.object.isRequired, + individualTileErrors: PropTypes.object.isRequired, + totalPageviews: PropTypes.number.isRequired, + hasInvalidCustomDimensionError: PropTypes.bool.isRequired, + Widget: PropTypes.elementType.isRequired, + getAudienceTileData: PropTypes.func.isRequired, + handleDismiss: PropTypes.func.isRequired, +}; + +export default AudienceTilesList; diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles/Body.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles/Body.js index beaa45ecad4..4c0da108da8 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles/Body.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles/Body.js @@ -1,39 +1,22 @@ /** - * AudienceTilesWidget Body component. - * - * Site Kit by Google, Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * AudienceTilesWidget Body component (clean rebuild). */ -/** - * External dependencies - */ import PropTypes from 'prop-types'; - -/** - * WordPress dependencies - */ import { useCallback, useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ +import { addQueryArgs } from '@wordpress/url'; +import { __ } from '@wordpress/i18n'; import { useDispatch, useInViewSelect, useSelect } from 'googlesitekit-data'; import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants'; -import { MODULES_ANALYTICS_4 } from '@/js/modules/analytics-4/datastore/constants'; +import { CORE_FORMS } from '@/js/googlesitekit/datastore/forms/constants'; +import { CORE_SITE } from '@/js/googlesitekit/datastore/site/constants'; +import { + MODULES_ANALYTICS_4, + EDIT_SCOPE, + CUSTOM_DIMENSION_DEFINITIONS, + AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, +} from '@/js/modules/analytics-4/datastore/constants'; import { isInvalidCustomDimensionError } from '@/js/modules/analytics-4/utils/custom-dimensions'; -import { reportRowsWithSetValues } from '@/js/modules/analytics-4/utils/report-rows-with-set-values'; import useAudienceTilesReports from '@/js/modules/analytics-4/hooks/useAudienceTilesReports'; import { BREAKPOINT_SMALL, @@ -41,12 +24,15 @@ import { useBreakpoint, } from '@/js/hooks/useBreakpoint'; import useViewOnly from '@/js/hooks/useViewOnly'; +import useViewContext from '@/js/hooks/useViewContext'; import AudienceSegmentationErrorWidget from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSegmentationErrorWidget'; -import AudienceTileLoading from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTileLoading'; -import AudienceTileError from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTileError'; -import AudienceTile from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile'; -import AudienceTooltipMessage from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTooltipMessage'; import MaybePlaceholderTile from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/MaybePlaceholderTile'; +import AudienceTilesList from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles/AudienceTilesList'; +import AudienceErrorModal from '@/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceErrorModal'; +import useFormValue from '@/js/hooks/useFormValue'; +import { ERROR_CODE_MISSING_REQUIRED_SCOPE } from '@/js/util/errors'; +import { AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION } from '@/js/googlesitekit/widgets/default-areas'; +import { trackEvent } from '@/js/util'; function hasZeroDataForAudience( report, dimensionName ) { const audienceData = report?.rows?.find( @@ -56,48 +42,30 @@ function hasZeroDataForAudience( report, dimensionName ) { return totalUsers === 0; } -export default function Body( { - activeTileIndex, - allTilesError, - individualTileErrors, - loading, - topCitiesReportsLoaded, - topContentReportsLoaded, - topContentPageTitlesReportsLoaded, - visibleAudiences, - Widget, -} ) { - const breakpoint = useBreakpoint(); +function useAudienceTilesController( { allTilesError, loading } ) { const isViewOnly = useViewOnly(); + const viewContext = useViewContext(); - const isTabbedBreakpoint = - breakpoint === BREAKPOINT_SMALL || breakpoint === BREAKPOINT_TABLET; - - const audiences = useInViewSelect( ( select ) => { - return select( MODULES_ANALYTICS_4 ).getOrSyncAvailableAudiences(); - }, [] ); - - // An array of audience resource names. + const audiences = useInViewSelect( + ( select ) => + select( MODULES_ANALYTICS_4 ).getOrSyncAvailableAudiences(), + [] + ); const configuredAudiences = useInViewSelect( ( select ) => select( CORE_USER ).getConfiguredAudiences(), [] ); - const [ siteKitAudiences, otherAudiences ] = useSelect( ( select ) => select( MODULES_ANALYTICS_4 ).getConfiguredSiteKitAndOtherAudiences() ) || [ [], [] ]; - const isSiteKitAudiencePartialData = useSelect( ( select ) => select( MODULES_ANALYTICS_4 ).hasAudiencePartialData( siteKitAudiences ) ); - const partialDataStates = useInViewSelect( ( select ) => - configuredAudiences?.reduce( ( acc, audienceResourceName ) => { - acc[ audienceResourceName ] = - select( MODULES_ANALYTICS_4 ).isAudiencePartialData( - audienceResourceName - ); + configuredAudiences?.reduce( ( acc, name ) => { + acc[ name ] = + select( MODULES_ANALYTICS_4 ).isAudiencePartialData( name ); return acc; }, {} ), [ configuredAudiences ] @@ -122,115 +90,82 @@ export default function Body( { function getAudienceTileMetrics( audienceResourceName ) { const isSiteKitAudience = siteKitAudiences.some( - ( audience ) => audience.name === audienceResourceName + ( a ) => a.name === audienceResourceName ); - - // Get the audience slug (e.g., 'new-visitors', 'returning-visitors'). const audienceSlug = siteKitAudiences.find( - ( audience ) => audience.name === audienceResourceName + ( a ) => a.name === audienceResourceName )?.audienceSlug; - - function findMetricsForDateRange( dateRange ) { + function find( range ) { let row; - if ( isSiteKitAudience && isSiteKitAudiencePartialData ) { - // Determine the dimension value ('new' or 'returning') for Site Kit audiences. - const dimensionValue = + const dimValue = audienceSlug === 'new-visitors' ? 'new' : 'returning'; - row = siteKitAudiencesReport?.rows?.find( ( { dimensionValues } ) => - dimensionValues?.[ 0 ]?.value === dimensionValue && - dimensionValues?.[ 1 ]?.value === dateRange + dimensionValues?.[ 0 ]?.value === dimValue && + dimensionValues?.[ 1 ]?.value === range ); } else { row = report?.rows?.find( ( { dimensionValues } ) => dimensionValues?.[ 0 ]?.value === audienceResourceName && - dimensionValues?.[ 1 ]?.value === dateRange + dimensionValues?.[ 1 ]?.value === range ); } - return [ - Number( row?.metricValues?.[ 0 ]?.value || 0 ), // totalUsers - Number( row?.metricValues?.[ 1 ]?.value || 0 ), // sessionsPerUser - Number( row?.metricValues?.[ 2 ]?.value || 0 ), // screenPageViewsPerSession - Number( row?.metricValues?.[ 3 ]?.value || 0 ), // screenPageViews + Number( row?.metricValues?.[ 0 ]?.value || 0 ), + Number( row?.metricValues?.[ 1 ]?.value || 0 ), + Number( row?.metricValues?.[ 2 ]?.value || 0 ), + Number( row?.metricValues?.[ 3 ]?.value || 0 ), ]; } - - const currentMetrics = findMetricsForDateRange( 'date_range_0' ); - const previousMetrics = findMetricsForDateRange( 'date_range_1' ); - - return { current: currentMetrics, previous: previousMetrics }; + return { + current: find( 'date_range_0' ), + previous: find( 'date_range_1' ), + }; } - function getAudienceTileData( audienceResourceName, audienceIndex ) { - const audienceName = - audiences?.filter( - ( { name } ) => name === audienceResourceName - )?.[ 0 ]?.displayName || ''; - - const audienceSlug = - audiences?.filter( - ( { name } ) => name === audienceResourceName - )?.[ 0 ]?.audienceSlug || ''; - + function getAudienceTileData( audienceResourceName, index ) { + const audience = audiences?.find( + ( { name } ) => name === audienceResourceName + ); + const audienceName = audience?.displayName || ''; + const audienceSlug = audience?.audienceSlug || ''; const { current, previous } = getAudienceTileMetrics( audienceResourceName ); - - const visitors = current[ 0 ]; - const prevVisitors = previous[ 0 ]; - - const visitsPerVisitors = current[ 1 ]; - const prevVisitsPerVisitors = previous[ 1 ]; - - const pagesPerVisit = current[ 2 ]; - const prevPagesPerVisit = previous[ 2 ]; - - const pageviews = current[ 3 ]; - const prevPageviews = previous[ 3 ]; - - const topCities = topCitiesReport?.[ audienceIndex ]; - - const topContent = topContentReport?.[ audienceIndex ]; - + const [ visitors, visitsPerVisitors, pagesPerVisit, pageviews ] = + current; + const [ + prevVisitors, + prevVisitsPerVisitors, + prevPagesPerVisit, + prevPageviews, + ] = previous; + const topCities = topCitiesReport?.[ index ]; + const topContent = topContentReport?.[ index ]; const topContentTitles = - topContentPageTitlesReport?.[ audienceIndex ]?.rows?.reduce( + topContentPageTitlesReport?.[ index ]?.rows?.reduce( ( acc, row ) => { acc[ row.dimensionValues[ 0 ].value ] = row.dimensionValues[ 1 ].value; - return acc; }, {} ) || {}; - const isSiteKitAudience = siteKitAudiences.some( - ( audience ) => audience.name === audienceResourceName + ( a ) => a.name === audienceResourceName ); - let reportToCheck = report; - let dimensionValue = audienceResourceName; - + let dimValue = audienceResourceName; if ( isSiteKitAudience && isSiteKitAudiencePartialData ) { - // If it's a Site Kit audience in a partial data state, use the siteKitAudiencesReport. reportToCheck = siteKitAudiencesReport; - - // Determine the dimension value ('new' or 'returning') for Site Kit audiences. - dimensionValue = - audienceSlug === 'new-visitors' ? 'new' : 'returning'; + dimValue = audienceSlug === 'new-visitors' ? 'new' : 'returning'; } - - const isZeroData = hasZeroDataForAudience( - reportToCheck, - dimensionValue - ); + const isZeroData = hasZeroDataForAudience( reportToCheck, dimValue ); const isPartialData = isSiteKitAudience ? false : partialDataStates[ audienceResourceName ]; - return { audienceName, audienceSlug, @@ -258,6 +193,113 @@ export default function Body( { isInvalidCustomDimensionError ); + const postTypeParam = + CUSTOM_DIMENSION_DEFINITIONS.googlesitekit_post_type.parameterName; + const isCreatingCustomDimension = useSelect( ( select ) => + select( MODULES_ANALYTICS_4 ).isCreatingCustomDimension( postTypeParam ) + ); + const isSyncingAvailableCustomDimensions = useSelect( ( select ) => + select( MODULES_ANALYTICS_4 ).isFetchingSyncAvailableCustomDimensions() + ); + const customDimensionError = useSelect( ( select ) => + select( MODULES_ANALYTICS_4 ).getCreateCustomDimensionError( + postTypeParam + ) + ); + const propertyID = useSelect( ( select ) => + select( MODULES_ANALYTICS_4 ).getPropertyID() + ); + const hasAnalyticsEditScope = useSelect( ( select ) => + select( CORE_USER ).hasScope( EDIT_SCOPE ) + ); + + const isAutoCreatingCustomDimensionsForAudience = useFormValue( + AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, + 'isAutoCreatingCustomDimensionsForAudience' + ); + const isRetryingCustomDimensionCreate = useFormValue( + AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, + 'isRetrying' + ); + const autoSubmit = useFormValue( + AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, + 'autoSubmit' + ); + const setupErrorCode = useSelect( ( select ) => + select( CORE_SITE ).getSetupErrorCode() + ); + const hasOAuthError = autoSubmit && setupErrorCode === 'access_denied'; + const isSaving = + isAutoCreatingCustomDimensionsForAudience || + isCreatingCustomDimension || + isSyncingAvailableCustomDimensions; + + const { setValues } = useDispatch( CORE_FORMS ); + const { setPermissionScopeError, clearPermissionScopeError } = + useDispatch( CORE_USER ); + const { clearError } = useDispatch( MODULES_ANALYTICS_4 ); + const { setSetupErrorCode } = useDispatch( CORE_SITE ); + + const redirectURL = addQueryArgs( global.location.href, { + notification: 'audience_segmentation', + widgetArea: AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION, + } ); + const errorRedirectURL = addQueryArgs( global.location.href, { + widgetArea: AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION, + } ); + + const onCreateCustomDimension = useCallback( + ( { isRetrying } = {} ) => { + setValues( AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, { + autoSubmit: true, + isRetrying, + } ); + if ( ! hasAnalyticsEditScope ) { + setPermissionScopeError( { + code: ERROR_CODE_MISSING_REQUIRED_SCOPE, + message: __( + 'Additional permissions are required to create new audiences in Analytics.', + 'google-site-kit' + ), + data: { + status: 403, + scopes: [ EDIT_SCOPE ], + skipModal: true, + skipDefaultErrorNotifications: true, + redirectURL, + errorRedirectURL, + }, + } ); + } + }, + [ + hasAnalyticsEditScope, + redirectURL, + errorRedirectURL, + setPermissionScopeError, + setValues, + ] + ); + + const onCancel = useCallback( () => { + setValues( AUDIENCE_TILE_CUSTOM_DIMENSION_CREATE, { + autoSubmit: false, + isRetrying: false, + } ); + setSetupErrorCode( null ); + clearPermissionScopeError(); + clearError( 'createCustomDimension', [ + propertyID, + CUSTOM_DIMENSION_DEFINITIONS.googlesitekit_post_type, + ] ); + }, [ + clearError, + clearPermissionScopeError, + propertyID, + setSetupErrorCode, + setValues, + ] ); + const { dismissItem } = useDispatch( CORE_USER ); const { fetchSyncAvailableCustomDimensions } = useDispatch( MODULES_ANALYTICS_4 ); @@ -279,10 +321,65 @@ export default function Body( { isViewOnly, ] ); - // TODO: The variable `audienceTileNumber` is part of a temporary workaround to ensure `AudienceErrorModal` is only rendered once - // within `AudienceTilesWidget`. This should be removed once the `AudienceErrorModal` render is extracted - // from `AudienceTilePagesMetric` and it's rendered once at a higher level instead. See https://github.com/google/site-kit-wp/issues/9543. - let audienceTileNumber = 0; + const showTilesList = ! allTilesError || loading; + const showErrorModal = + ( ( customDimensionError && ! isSaving ) || + ( isRetryingCustomDimensionCreate && + ! isAutoCreatingCustomDimensionsForAudience ) || + hasOAuthError ) && + ! allTilesError && + ! loading; + + return { + // Data & reports + reportError, + totalPageviewsReportError, + totalPageviews, + hasInvalidCustomDimensionError, + customDimensionError, + isSaving, + hasOAuthError, + viewContext, + showTilesList, + showErrorModal, + getAudienceTileData, + onCreateCustomDimension, + onCancel, + handleDismiss, + }; +} + +export default function Body( props ) { + const { + activeTileIndex, + allTilesError, + individualTileErrors, + loading, + topCitiesReportsLoaded, + topContentReportsLoaded, + topContentPageTitlesReportsLoaded, + visibleAudiences, + Widget, + } = props; + // Local breakpoint for placeholder tile visibility (AudienceTilesList computes its own internally). + const breakpoint = useBreakpoint(); + const controller = useAudienceTilesController( { allTilesError, loading } ); + const { + reportError, + totalPageviewsReportError, + totalPageviews, + hasInvalidCustomDimensionError, + customDimensionError, + isSaving, + hasOAuthError, + viewContext, + showTilesList, + showErrorModal, + getAudienceTileData, + onCreateCustomDimension, + onCancel, + handleDismiss, + } = controller; return (
@@ -296,155 +393,57 @@ export default function Body( { ] } /> ) } - { ( allTilesError === false || loading ) && - visibleAudiences.map( ( audienceResourceName, index ) => { - // Conditionally render only the selected audience tile on mobile. - if ( isTabbedBreakpoint && index !== activeTileIndex ) { - return null; + { showTilesList && ( + - - - ); + individualTileErrors={ individualTileErrors } + totalPageviews={ totalPageviews } + hasInvalidCustomDimensionError={ + hasInvalidCustomDimensionError } - - // If errored, skip rendering. - if ( - individualTileErrors[ audienceResourceName ].length > 0 - ) { - return ( - + Widget={ Widget } + getAudienceTileData={ getAudienceTileData } + handleDismiss={ handleDismiss } + /> + ) } + { showErrorModal && ( + { + trackEvent( + `${ viewContext }_audiences-top-content-cta`, + 'retry_enable_metric' ); - } - - return ( - - } - visitors={ { - currentValue: visitors, - previousValue: prevVisitors, - } } - visitsPerVisitor={ { - currentValue: visitsPerVisitors, - previousValue: prevVisitsPerVisitors, - } } - pagesPerVisit={ { - currentValue: pagesPerVisit, - previousValue: prevPagesPerVisit, - } } - pageviews={ { - currentValue: pageviews, - previousValue: prevPageviews, - } } - percentageOfTotalPageViews={ - totalPageviews !== 0 - ? pageviews / totalPageviews - : 0 - } - topCities={ { - dimensionValues: [ - filteredTopCitiesRows?.[ 0 ] - ?.dimensionValues?.[ 0 ], - filteredTopCitiesRows?.[ 1 ] - ?.dimensionValues?.[ 0 ], - filteredTopCitiesRows?.[ 2 ] - ?.dimensionValues?.[ 0 ], - ], - metricValues: [ - filteredTopCitiesRows?.[ 0 ] - ?.metricValues?.[ 0 ], - filteredTopCitiesRows?.[ 1 ] - ?.metricValues?.[ 0 ], - filteredTopCitiesRows?.[ 2 ] - ?.metricValues?.[ 0 ], - ], - total: visitors, - } } - topContent={ { - dimensionValues: [ - topContent?.rows?.[ 0 ] - ?.dimensionValues?.[ 0 ], - topContent?.rows?.[ 1 ] - ?.dimensionValues?.[ 0 ], - topContent?.rows?.[ 2 ] - ?.dimensionValues?.[ 0 ], - ], - metricValues: [ - topContent?.rows?.[ 0 ] - ?.metricValues?.[ 0 ], - topContent?.rows?.[ 1 ] - ?.metricValues?.[ 0 ], - topContent?.rows?.[ 2 ] - ?.metricValues?.[ 0 ], - ], - } } - topContentTitles={ topContentTitles } - hasInvalidCustomDimensionError={ - hasInvalidCustomDimensionError - } - Widget={ Widget } - audienceResourceName={ audienceResourceName } - isZeroData={ isZeroData } - isPartialData={ isPartialData } - isTileHideable={ visibleAudiences.length > 1 } - onHideTile={ () => - handleDismiss( audienceResourceName ) - } - /> - ); - } ) } - { ! isTabbedBreakpoint && ( + onCreateCustomDimension( { isRetrying: true } ); + } } + onCancel={ () => { + trackEvent( + `${ viewContext }_audiences-top-content-cta`, + 'cancel_enable_metric' + ); + onCancel(); + } } + inProgress={ isSaving } + hasOAuthError={ hasOAuthError } + trackEventCategory={ `${ viewContext }_audiences-top-content-cta` } + /> + ) } + { ! ( + breakpoint === BREAKPOINT_SMALL || + breakpoint === BREAKPOINT_TABLET + ) && (