diff --git a/projects/packages/forms/changelog/add-forms-integrations-store b/projects/packages/forms/changelog/add-forms-integrations-store new file mode 100644 index 0000000000000..be3295dada259 --- /dev/null +++ b/projects/packages/forms/changelog/add-forms-integrations-store @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Forms: add integrations store. diff --git a/projects/packages/forms/src/dashboard/integrations/index.tsx b/projects/packages/forms/src/dashboard/integrations/index.tsx index eba2a78274093..cb4fe4871664d 100644 --- a/projects/packages/forms/src/dashboard/integrations/index.tsx +++ b/projects/packages/forms/src/dashboard/integrations/index.tsx @@ -2,12 +2,13 @@ * External dependencies */ import jetpackAnalytics from '@automattic/jetpack-analytics'; +import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { useState, useCallback } from 'react'; /** * Internal dependencies */ -import { useIntegrationsStatus } from '../../blocks/contact-form/components/jetpack-integrations-modal/hooks/use-integrations-status'; +import { INTEGRATIONS_STORE } from '../../store/integrations'; import AkismetDashboardCard from './akismet-card'; import CreativeMailDashboardCard from './creative-mail-card'; import GoogleSheetsDashboardCard from './google-sheets-card'; @@ -18,10 +19,17 @@ import './style.scss'; /** * Types */ +import type { SelectIntegrations, IntegrationsDispatch } from '../../store/integrations'; import type { Integration } from '../../types'; const Integrations = () => { - const { integrations, refreshIntegrations } = useIntegrationsStatus(); + const { integrations } = useSelect( ( select: SelectIntegrations ) => { + const store = select( INTEGRATIONS_STORE ); + return { + integrations: store.getIntegrations() || [], + }; + }, [] ) as { integrations: Integration[] }; + const { refreshIntegrations } = useDispatch( INTEGRATIONS_STORE ) as IntegrationsDispatch; const [ expandedCards, setExpandedCards ] = useState( { akismet: false, googleSheets: false, @@ -63,7 +71,7 @@ const Integrations = () => { const handleToggleMailPoet = useCallback( () => toggleCard( 'mailpoet' ), [ toggleCard ] ); const findIntegrationById = ( id: string ) => - integrations?.find( ( integration: Integration ) => integration.id === id ); + integrations.find( integration => integration.id === id ); // Only supported integrations will be returned from endpoint. const akismetData = findIntegrationById( 'akismet' ); diff --git a/projects/packages/forms/src/store/integrations/action-types.ts b/projects/packages/forms/src/store/integrations/action-types.ts new file mode 100644 index 0000000000000..712041f3441fd --- /dev/null +++ b/projects/packages/forms/src/store/integrations/action-types.ts @@ -0,0 +1,4 @@ +export const RECEIVE_INTEGRATIONS = 'RECEIVE_INTEGRATIONS'; +export const INVALIDATE_INTEGRATIONS = 'INVALIDATE_INTEGRATIONS'; +export const SET_INTEGRATIONS_LOADING = 'SET_INTEGRATIONS_LOADING'; +export const SET_INTEGRATIONS_ERROR = 'SET_INTEGRATIONS_ERROR'; diff --git a/projects/packages/forms/src/store/integrations/actions.ts b/projects/packages/forms/src/store/integrations/actions.ts new file mode 100644 index 0000000000000..d35db2243808c --- /dev/null +++ b/projects/packages/forms/src/store/integrations/actions.ts @@ -0,0 +1,30 @@ +import { + RECEIVE_INTEGRATIONS, + INVALIDATE_INTEGRATIONS, + SET_INTEGRATIONS_LOADING, + SET_INTEGRATIONS_ERROR, +} from './action-types'; +import { getIntegrations } from './resolvers'; +import type { Integration } from '../../types'; + +export const receiveIntegrations = ( items: Integration[] ) => ( { + type: RECEIVE_INTEGRATIONS, + items, +} ); + +export const invalidateIntegrations = () => ( { + type: INVALIDATE_INTEGRATIONS, +} ); + +export const setIntegrationsLoading = ( isLoading: boolean ) => ( { + type: SET_INTEGRATIONS_LOADING, + isLoading, +} ); + +export const setIntegrationsError = ( error: string | null ) => ( { + type: SET_INTEGRATIONS_ERROR, + error, +} ); + +// Thunk-like action to immediately refresh from the endpoint +export const refreshIntegrations = () => getIntegrations(); diff --git a/projects/packages/forms/src/store/integrations/index.ts b/projects/packages/forms/src/store/integrations/index.ts new file mode 100644 index 0000000000000..29186b1d24510 --- /dev/null +++ b/projects/packages/forms/src/store/integrations/index.ts @@ -0,0 +1,20 @@ +import { createReduxStore, register } from '@wordpress/data'; +import * as actions from './actions'; +import reducer from './reducer'; +import * as resolvers from './resolvers'; +import * as selectors from './selectors'; + +export const INTEGRATIONS_STORE = 'jetpack/forms/integrations'; + +export const store = createReduxStore( INTEGRATIONS_STORE, { + reducer, + actions, + selectors, + resolvers, +} ); + +register( store ); + +export * from './actions'; +export * from './selectors'; +export * from './types'; diff --git a/projects/packages/forms/src/store/integrations/reducer.ts b/projects/packages/forms/src/store/integrations/reducer.ts new file mode 100644 index 0000000000000..40f8e14fcf900 --- /dev/null +++ b/projects/packages/forms/src/store/integrations/reducer.ts @@ -0,0 +1,55 @@ +import { + RECEIVE_INTEGRATIONS, + INVALIDATE_INTEGRATIONS, + SET_INTEGRATIONS_LOADING, + SET_INTEGRATIONS_ERROR, +} from './action-types'; +import type { IntegrationsState, IntegrationsAction } from './types'; + +const DEFAULT_STATE: IntegrationsState = { + items: null, + isLoading: false, + error: null, +}; + +/** + * Integrations store reducer. + * + * @param state - Current state + * @param action - Dispatched action + * @return Updated state + */ +export default function reducer( + state: IntegrationsState = DEFAULT_STATE, + action: IntegrationsAction +): IntegrationsState { + switch ( action.type ) { + case SET_INTEGRATIONS_LOADING: + return { + ...state, + isLoading: !! action.isLoading, + error: action.isLoading ? null : state.error, + }; + case SET_INTEGRATIONS_ERROR: + return { + ...state, + isLoading: false, + error: action.error ?? 'Unknown error', + }; + case RECEIVE_INTEGRATIONS: + return { + ...state, + items: action.items, + isLoading: false, + error: null, + }; + case INVALIDATE_INTEGRATIONS: + return { + ...state, + items: null, + isLoading: false, + }; + default: + return state; + } +} diff --git a/projects/packages/forms/src/store/integrations/resolvers.ts b/projects/packages/forms/src/store/integrations/resolvers.ts new file mode 100644 index 0000000000000..d634bb1e69b99 --- /dev/null +++ b/projects/packages/forms/src/store/integrations/resolvers.ts @@ -0,0 +1,26 @@ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; +import { INVALIDATE_INTEGRATIONS } from './action-types'; +import { receiveIntegrations, setIntegrationsError, setIntegrationsLoading } from './actions'; +import type { IntegrationsAction } from './types'; +import type { Integration } from '../../types'; + +export const getIntegrations = + () => + async ( { dispatch }: { dispatch: ( action: IntegrationsAction ) => void } ) => { + dispatch( setIntegrationsLoading( true ) ); + try { + const path = addQueryArgs( '/wp/v2/feedback/integrations', { version: 2 } ); + const result = await apiFetch< Integration[] >( { path } ); + dispatch( receiveIntegrations( result ) ); + } catch ( e ) { + const message = e instanceof Error ? e.message : 'Unknown error'; + dispatch( setIntegrationsError( message ) ); + } finally { + dispatch( setIntegrationsLoading( false ) ); + } + }; + +// Attach invalidation rule +getIntegrations.shouldInvalidate = ( action: IntegrationsAction ) => + action.type === INVALIDATE_INTEGRATIONS; diff --git a/projects/packages/forms/src/store/integrations/selectors.ts b/projects/packages/forms/src/store/integrations/selectors.ts new file mode 100644 index 0000000000000..3ebd2117ee7ed --- /dev/null +++ b/projects/packages/forms/src/store/integrations/selectors.ts @@ -0,0 +1,6 @@ +import type { IntegrationsState } from './types'; +import type { Integration } from '../../types'; + +export const getIntegrations = ( state: IntegrationsState ): Integration[] | null => state.items; +export const isIntegrationsLoading = ( state: IntegrationsState ): boolean => state.isLoading; +export const getIntegrationsError = ( state: IntegrationsState ): string | null => state.error; diff --git a/projects/packages/forms/src/store/integrations/types.ts b/projects/packages/forms/src/store/integrations/types.ts new file mode 100644 index 0000000000000..195f0172b11fe --- /dev/null +++ b/projects/packages/forms/src/store/integrations/types.ts @@ -0,0 +1,28 @@ +import { INTEGRATIONS_STORE } from '.'; +import type { Integration } from '../../types'; + +export type IntegrationsState = { + items: Integration[] | null; + isLoading: boolean; + error: string | null; +}; + +export type IntegrationsAction = { + type: string; + items?: Integration[]; + isLoading?: boolean; + error?: string | null; +}; + +export type IntegrationsSelectors = { + getIntegrations: () => Integration[] | null; + isIntegrationsLoading: () => boolean; + getIntegrationsError: () => string | null; +}; + +export type IntegrationsDispatch = { + refreshIntegrations: () => Promise< void >; + invalidateIntegrations: () => void; +}; + +export type SelectIntegrations = ( store: typeof INTEGRATIONS_STORE ) => IntegrationsSelectors; diff --git a/projects/plugins/jetpack/changelog/add-forms-integrations-store b/projects/plugins/jetpack/changelog/add-forms-integrations-store new file mode 100644 index 0000000000000..767811ab8711d --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-forms-integrations-store @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Forms: add integrations store.