Skip to content
Merged
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: minor
Type: added

Forms: add integrations store.
14 changes: 11 additions & 3 deletions projects/packages/forms/src/dashboard/integrations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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' );
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
30 changes: 30 additions & 0 deletions projects/packages/forms/src/store/integrations/actions.ts
Original file line number Diff line number Diff line change
@@ -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();
20 changes: 20 additions & 0 deletions projects/packages/forms/src/store/integrations/index.ts
Original file line number Diff line number Diff line change
@@ -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';
56 changes: 56 additions & 0 deletions projects/packages/forms/src/store/integrations/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { __ } from '@wordpress/i18n';
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a semantic issue. I am not really sure what the correct way is so feel free to ignore it.
I just noticed that in gutenberg we sometime use isResolving see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/#isresolving

Should we use that instead of isLoading?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, but I think this wouldn't quite work. My understanding is that isResolving only tracks when resolvers run for a specific selector (e.g., getIntegrations). It wouldn't cover the refreshIntegrations 'thunk' that we added in this PR so we can actively trigger refreshes, so we'd lose loading state during those refreshes I think. And the refreshes are pretty key in a lot of places.

error: action.isLoading ? null : state.error,
};
case SET_INTEGRATIONS_ERROR:
return {
...state,
isLoading: false,
error: action.error ?? __( 'Unknown error', 'jetpack-forms' ),
};
case RECEIVE_INTEGRATIONS:
return {
...state,
items: action.items,
isLoading: false,
error: null,
};
case INVALIDATE_INTEGRATIONS:
return {
...state,
items: null,
isLoading: false,
};
default:
return state;
}
}
27 changes: 27 additions & 0 deletions projects/packages/forms/src/store/integrations/resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import apiFetch from '@wordpress/api-fetch';
import { __ } from '@wordpress/i18n';
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', 'jetpack-forms' );
dispatch( setIntegrationsError( message ) );
} finally {
dispatch( setIntegrationsLoading( false ) );
}
};

// Attach invalidation rule
getIntegrations.shouldInvalidate = ( action: IntegrationsAction ) =>
action.type === INVALIDATE_INTEGRATIONS;
6 changes: 6 additions & 0 deletions projects/packages/forms/src/store/integrations/selectors.ts
Original file line number Diff line number Diff line change
@@ -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;
28 changes: 28 additions & 0 deletions projects/packages/forms/src/store/integrations/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: enhancement

Forms: add integrations store.
Loading