Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Forms: Add CTA to install/activate Akismet on empty spam dashboard

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import useConfigValue from '../../../../../hooks/use-config-value.ts';
import { usePluginInstallation } from '../hooks/use-plugin-installation.ts';
import { usePluginInstallation } from '../../../../../hooks/use-plugin-installation.ts';

type PluginActionButtonProps = {
slug: string;
Expand All @@ -27,18 +26,20 @@ const PluginActionButton = ( {
refreshStatus,
trackEventName,
}: PluginActionButtonProps ) => {
const { isInstalling, installPlugin } = usePluginInstallation(
slug,
pluginFile,
isInstalled,
trackEventName
);
const trackEventProps = {
screen: 'block-editor',
};

// Permissions from consolidated Forms config (shared across editor and dashboard)
const canUserInstallPlugins = useConfigValue( 'canInstallPlugins' );
const canUserActivatePlugins = useConfigValue( 'canActivatePlugins' );
const { isInstalling, installPlugin, canInstallPlugins, canActivatePlugins } =
usePluginInstallation( {
slug,
pluginPath: pluginFile,
isInstalled,
trackEventName,
trackEventProps,
} );

const canPerformAction = isInstalled ? canUserActivatePlugins : canUserInstallPlugins;
const canPerformAction = isInstalled ? canActivatePlugins : canInstallPlugins;
const [ isReconcilingStatus, setIsReconcilingStatus ] = useState( false );
const isDisabled = isInstalling || isReconcilingStatus || ! canPerformAction;

Expand Down Expand Up @@ -84,10 +85,10 @@ const PluginActionButton = ( {
);

const getTooltipText = (): string => {
if ( isInstalled && ! canUserActivatePlugins ) {
if ( isInstalled && ! canActivatePlugins ) {
return tooltipTextNoActivatePerms;
}
if ( ! isInstalled && ! canUserInstallPlugins ) {
if ( ! isInstalled && ! canInstallPlugins ) {
return tooltipTextNoInstallPerms;
}
return String( isInstalled ? tooltipTextActivate : tooltipTextInstall );
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,149 @@
/**
* External dependencies
*/
import { isSimpleSite } from '@automattic/jetpack-script-data';
import {
Button,
__experimentalText as Text, // eslint-disable-line @wordpress/no-unsafe-wp-apis
__experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
} from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { useCallback, useMemo } from '@wordpress/element';
import { __, _n, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import useConfigValue from '../../../hooks/use-config-value.ts';
import { usePluginInstallation } from '../../../hooks/use-plugin-installation.ts';
import { INTEGRATIONS_STORE } from '../../../store/integrations/index.ts';
import CreateFormButton from '../create-form-button/index.tsx';
/**
* Types
*/
import type {
IntegrationsDispatch,
SelectIntegrations,
} from '../../../store/integrations/index.ts';
import type { Integration } from '../../../types/index.ts';

type UseInstallAkismetReturn = {
shouldShowAkismetCta: boolean;
wrapperBody: string;
isInstallingAkismet: boolean;
canPerformAkismetAction: boolean;
wrapperButtonText: string;
handleAkismetSetup: () => Promise< void >;
};

type EmptyResponsesProps = {
status: string;
isSearch: boolean;
readStatusFilter?: 'unread' | 'read';
};

/**
* Hook to handle Akismet installation and activation.
*
* @return {UseInstallAkismetReturn} An object containing the necessary data and functions to handle Akismet installation and activation.
*/
const useInstallAkismet = (): UseInstallAkismetReturn => {
const { akismetIntegration } = useSelect( ( select: SelectIntegrations ) => {
const store = select( INTEGRATIONS_STORE );
const integrations = store.getIntegrations() || [];

return {
akismetIntegration: integrations.find(
( integration: Integration ) => integration.id === 'akismet'
),
};
}, [] ) as { akismetIntegration?: Integration };

const { refreshIntegrations } = useDispatch( INTEGRATIONS_STORE ) as IntegrationsDispatch;

const akismetIntegrationReady = useMemo(
() => !! akismetIntegration && ! akismetIntegration.__isPartial,
[ akismetIntegration ]
);

const isInstalled = !! akismetIntegration?.isInstalled;

const isAkismetActive = akismetIntegrationReady && isInstalled && !! akismetIntegration?.isActive;

const shouldShowAkismetCta = akismetIntegrationReady && ! isAkismetActive && ! isSimpleSite();

const akismetPluginFile = useMemo(
() => akismetIntegration?.pluginFile ?? 'akismet/akismet',
[ akismetIntegration?.pluginFile ]
);

const installAndActivateBody = __(
'Install and activate Jetpack Akismet Anti-spam to automatically filter form spam.',
'jetpack-forms'
);

const activateBody = __(
'Activate Jetpack Akismet Anti-spam to automatically filter form spam.',
'jetpack-forms'
);

const wrapperBody = isInstalled ? activateBody : installAndActivateBody;

const activateButtonText = __( 'Activate Akismet Anti-spam', 'jetpack-forms' );
const installAndActivateButtonText = __( 'Install Akismet Anti-spam', 'jetpack-forms' );
const wrapperButtonText = isInstalled ? activateButtonText : installAndActivateButtonText;

const {
isInstalling: isInstallingAkismet,
installPlugin,
canInstallPlugins,
canActivatePlugins,
} = usePluginInstallation( {
slug: 'akismet',
pluginPath: akismetPluginFile,
isInstalled,
onSuccess: refreshIntegrations,
trackEventName: 'jetpack_forms_upsell_akismet_click',
trackEventProps: {
screen: 'dashboard',
},
successNotices: {
install: {
message: __( 'Akismet installed and activated.', 'jetpack-forms' ),
options: { type: 'snackbar', id: 'akismet-install-success' },
},
activate: {
message: __( 'Akismet activated.', 'jetpack-forms' ),
options: { type: 'snackbar', id: 'akismet-install-success' },
},
},
errorNotice: {
message: __( 'Could not set up Akismet. Please try again.', 'jetpack-forms' ),
options: { type: 'snackbar', id: 'akismet-install-error' },
},
} );

const canPerformAkismetAction =
isInstalled && akismetIntegrationReady
? canActivatePlugins !== false
: canInstallPlugins !== false;

const handleAkismetSetup = useCallback( async () => {
if ( isInstallingAkismet || ! akismetIntegrationReady || ! canPerformAkismetAction ) {
return;
}

await installPlugin();
}, [ isInstallingAkismet, akismetIntegrationReady, canPerformAkismetAction, installPlugin ] );

return {
shouldShowAkismetCta,
wrapperBody,
isInstallingAkismet,
canPerformAkismetAction,
wrapperButtonText,
handleAkismetSetup,
};
};

const EmptyWrapper = ( { heading = '', body = '', actions = null } ) => (
<VStack alignment="center" spacing="2">
Expand All @@ -18,14 +157,16 @@ const EmptyWrapper = ( { heading = '', body = '', actions = null } ) => (
</VStack>
);

type EmptyResponsesProps = {
status: string;
isSearch: boolean;
readStatusFilter?: 'unread' | 'read';
};

const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesProps ) => {
const emptyTrashDays = useConfigValue( 'emptyTrashDays' ) ?? 0;
const {
shouldShowAkismetCta,
wrapperBody,
isInstallingAkismet,
canPerformAkismetAction,
wrapperButtonText,
handleAkismetSetup,
} = useInstallAkismet();

// Handle search and filter states first
const hasReadStatusFilter = !! readStatusFilter;
Expand Down Expand Up @@ -60,7 +201,28 @@ const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesP
'Spam responses are permanently deleted after 15 days.',
'jetpack-forms'
);

if ( status === 'spam' ) {
if ( shouldShowAkismetCta ) {
return (
<EmptyWrapper
heading={ noSpamHeading }
body={ wrapperBody }
actions={
<Button
variant="primary"
isBusy={ isInstallingAkismet }
disabled={ isInstallingAkismet || ! canPerformAkismetAction }
onClick={ handleAkismetSetup }
__next40pxDefaultSize
>
{ wrapperButtonText }
</Button>
}
/>
);
}

return <EmptyWrapper heading={ noSpamHeading } body={ noSpamMessage } />;
}

Expand Down
22 changes: 21 additions & 1 deletion projects/packages/forms/src/dashboard/inbox/stage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
*/
import jetpackAnalytics from '@automattic/jetpack-analytics';
import { JetpackLogo } from '@automattic/jetpack-components';
import { isSimpleSite } from '@automattic/jetpack-script-data';
import { Badge } from '@automattic/ui';
import { ExternalLink, Modal } from '@wordpress/components';
import { useResizeObserver, useViewportMatch } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { DataViews } from '@wordpress/dataviews/wp';
import { dateI18n, getSettings as getDateSettings } from '@wordpress/date';
import { useCallback, useMemo, useState } from '@wordpress/element';
Expand All @@ -19,6 +21,7 @@ import { useSearchParams } from 'react-router';
* Internal dependencies
*/
import useConfigValue from '../../../hooks/use-config-value.ts';
import { INTEGRATIONS_STORE } from '../../../store/integrations/index.ts';
import CreateFormButton from '../../components/create-form-button/index.tsx';
import EmptyResponses from '../../components/empty-responses/index.tsx';
import EmptySpamButton from '../../components/empty-spam-button/index.tsx';
Expand Down Expand Up @@ -137,6 +140,23 @@ export default function InboxView() {
totalItems,
totalPages,
} = useInboxData();
const isAkismetStatusPending = useSelect(
select => {
const store = select( INTEGRATIONS_STORE );
const integrations = store.getIntegrations() || [];
const isIntegrationsLoading = store.isIntegrationsLoading();
const akismetIntegration = integrations.find( integration => integration.id === 'akismet' );

return (
statusFilter === 'spam' &&
! isSimpleSite() &&
( isIntegrationsLoading || ! akismetIntegration || akismetIntegration.__isPartial )
);
},
[ statusFilter ]
);

const isInboxLoading = isLoadingData || isAkismetStatusPending;

useEffect( () => {
const _filters = view.filters?.reduce( ( accumulator, { field, value } ) => {
Expand Down Expand Up @@ -497,7 +517,7 @@ export default function InboxView() {
fields={ fields }
actions={ actions }
data={ records || EMPTY_ARRAY }
isLoading={ isLoadingData }
isLoading={ isInboxLoading }
view={ view }
onChangeView={ setView }
selection={ selection }
Expand Down
Loading
Loading