diff --git a/src/hooks/sync-sites/sync-sites-context.tsx b/src/hooks/sync-sites/sync-sites-context.tsx index 9a8807266..991d5eab7 100644 --- a/src/hooks/sync-sites/sync-sites-context.tsx +++ b/src/hooks/sync-sites/sync-sites-context.tsx @@ -1,10 +1,9 @@ import { __, sprintf } from '@wordpress/i18n'; -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createContext, useCallback, useContext, useEffect } from 'react'; import { useListenDeepLinkConnection } from 'src/hooks/sync-sites/use-listen-deep-link-connection'; import { generateStateId } from 'src/hooks/sync-sites/use-pull-push-states'; -import { PullStates, UseSyncPull, useSyncPull } from 'src/hooks/sync-sites/use-sync-pull'; +import { UseSyncPull, useSyncPull } from 'src/hooks/sync-sites/use-sync-pull'; import { - PushStates, UseSyncPush, useSyncPush, mapImportResponseToPushState, @@ -14,7 +13,7 @@ import { useFormatLocalizedTimestamps } from 'src/hooks/use-format-localized-tim import { useSyncStatesProgressInfo } from 'src/hooks/use-sync-states-progress-info'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { useAppDispatch } from 'src/stores'; -import { useConnectedSitesData, useSyncSitesData, connectedSitesActions } from 'src/stores/sync'; +import { useConnectedSitesData, useSyncSitesData, connectedSitesActions, syncOperationsActions } from 'src/stores/sync'; import type { ImportResponse } from 'src/hooks/use-sync-states-progress-info'; type GetLastSyncTimeText = ( timestamp: string | null, type: 'pull' | 'push' ) => string; @@ -34,7 +33,6 @@ const SyncSitesContext = createContext< SyncSitesContextType | undefined >( unde export function SyncSitesProvider( { children }: { children: React.ReactNode } ) { const { formatRelativeTime } = useFormatLocalizedTimestamps(); - const [ pullStates, setPullStates ] = useState< PullStates >( {} ); const getLastSyncTimeText = useCallback< GetLastSyncTimeText >( ( timestamp, type ) => { @@ -90,17 +88,12 @@ export function SyncSitesProvider( { children }: { children: React.ReactNode } ) const { pullSite, isAnySitePulling, isSiteIdPulling, clearPullState, getPullState, cancelPull } = useSyncPull( { - pullStates, - setPullStates, onPullSuccess: ( remoteSiteId, localSiteId ) => updateSiteTimestamp( remoteSiteId, localSiteId, 'pull' ), } ); - const [ pushStates, setPushStates ] = useState< PushStates >( {} ); const { pushSite, isAnySitePushing, isSiteIdPushing, clearPushState, getPushState, cancelPush } = useSyncPush( { - pushStates, - setPushStates, onPushSuccess: ( remoteSiteId, localSiteId ) => updateSiteTimestamp( remoteSiteId, localSiteId, 'push' ), } ); @@ -121,8 +114,6 @@ export function SyncSitesProvider( { children }: { children: React.ReactNode } ) const allSites = await getIpcApi().getSiteDetails(); const allConnectedSites = await getIpcApi().getConnectedWpcomSites(); - const restoredStates: PushStates = {}; - for ( const connectedSite of allConnectedSites ) { try { const localSite = allSites.find( ( site ) => site.id === connectedSite.localSiteId ); @@ -132,25 +123,29 @@ export function SyncSitesProvider( { children }: { children: React.ReactNode } ) continue; } - const response = await client.req.get< ImportResponse >( + const response = await client.req.get( `/sites/${ connectedSite.id }/studio-app/sync/import`, { apiNamespace: 'wpcom/v2', } - ); + ) as ImportResponse; const status = mapImportResponseToPushState( response, pushStatesProgressInfo ); // Only restore the pushStates if the operation is still in progress if ( status ) { - const stateId = generateStateId( connectedSite.localSiteId, connectedSite.id ); - restoredStates[ stateId ] = { + dispatch( syncOperationsActions.updatePushState( { + selectedSiteId: connectedSite.localSiteId, remoteSiteId: connectedSite.id, - status, - selectedSite: localSite, - remoteSiteUrl: connectedSite.url, - }; + state: { + remoteSiteId: connectedSite.id, + status, + selectedSite: localSite, + remoteSiteUrl: connectedSite.url, + }, + } ) ); + const stateId = generateStateId( connectedSite.localSiteId, connectedSite.id ); getIpcApi().addSyncOperation( stateId, status ); } } catch ( error ) { @@ -158,17 +153,13 @@ export function SyncSitesProvider( { children }: { children: React.ReactNode } ) console.error( `Failed to check push progress for site ${ connectedSite.id }:`, error ); } } - - if ( Object.keys( restoredStates ).length > 0 ) { - setPushStates( ( prev ) => ( { ...prev, ...restoredStates } ) ); - } }; initializePushStates().catch( ( error ) => { // Initialization is not critical to app functionality, but log the error console.error( 'Failed to initialize push states from server:', error ); } ); - }, [ client, pushStatesProgressInfo ] ); + }, [ client, pushStatesProgressInfo, dispatch ] ); return ( boolean; type UseSyncPullProps = { - pullStates: PullStates; - setPullStates: React.Dispatch< React.SetStateAction< PullStates > >; onPullSuccess?: OnPullSuccess; }; @@ -65,10 +64,8 @@ export type UseSyncPull = { }; export function useSyncPull( { - pullStates, - setPullStates, onPullSuccess, -}: UseSyncPullProps ): UseSyncPull { +}: UseSyncPullProps = {} ): UseSyncPull { const { __ } = useI18n(); const { client } = useAuth(); const { importFile, clearImportState } = useImportExport(); @@ -80,11 +77,38 @@ export function useSyncPull( { isKeyCancelled, getBackupStatusWithProgress, } = useSyncStatesProgressInfo(); - const { - updateState, - getState: getPullState, - clearState, - } = usePullPushStates< SyncBackupState >( pullStates, setPullStates ); + + const dispatch = useAppDispatch(); + const pullStates = useRootSelector( syncOperationsSelectors.selectPullStates ); + + const updateState = useCallback< UpdateState< SyncBackupState > >( + ( selectedSiteId, remoteSiteId, state ) => { + dispatch( syncOperationsActions.updatePullState( { + selectedSiteId, + remoteSiteId, + state, + } ) ); + }, + [ dispatch ] + ); + + const getPullState = useCallback< GetState< SyncBackupState > >( + ( selectedSiteId, remoteSiteId ) => { + const stateId = generateStateId( selectedSiteId, remoteSiteId ); + return pullStates[ stateId ]; + }, + [ pullStates ] + ); + + const clearState = useCallback< ClearState >( + ( selectedSiteId, remoteSiteId ) => { + dispatch( syncOperationsActions.clearPullState( { + selectedSiteId, + remoteSiteId, + } ) ); + }, + [ dispatch ] + ); const updatePullState = useCallback< UpdateState< SyncBackupState > >( ( selectedSiteId, remoteSiteId, state ) => { diff --git a/src/hooks/sync-sites/use-sync-push.ts b/src/hooks/sync-sites/use-sync-push.ts index 2b2c1c963..21989b985 100644 --- a/src/hooks/sync-sites/use-sync-push.ts +++ b/src/hooks/sync-sites/use-sync-push.ts @@ -8,7 +8,6 @@ import { generateStateId, GetState, UpdateState, - usePullPushStates, } from 'src/hooks/sync-sites/use-pull-push-states'; import { useAuth } from 'src/hooks/use-auth'; import { @@ -17,6 +16,8 @@ import { } from 'src/hooks/use-sync-states-progress-info'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { getHostnameFromUrl } from 'src/lib/url-utils'; +import { useAppDispatch, useRootSelector } from 'src/stores'; +import { syncOperationsActions, syncOperationsSelectors } from 'src/stores/sync'; import type { SyncSite } from 'src/hooks/use-fetch-wpcom-sites/types'; import type { ImportResponse } from 'src/hooks/use-sync-states-progress-info'; import type { SyncOption } from 'src/types'; @@ -43,8 +44,6 @@ type PushSite = ( type IsSiteIdPushing = ( selectedSiteId: string, remoteSiteId?: number ) => boolean; type UseSyncPushProps = { - pushStates: PushStates; - setPushStates: React.Dispatch< React.SetStateAction< PushStates > >; onPushSuccess?: OnPushSuccess; }; @@ -83,18 +82,45 @@ export function mapImportResponseToPushState( return null; } -export function useSyncPush( { - pushStates, - setPushStates, - onPushSuccess, -}: UseSyncPushProps ): UseSyncPush { +export function useSyncPush( { onPushSuccess }: UseSyncPushProps = {} ): UseSyncPush { const { __ } = useI18n(); const { client } = useAuth(); - const { - updateState, - getState: getPushState, - clearState, - } = usePullPushStates< SyncPushState >( pushStates, setPushStates ); + + const dispatch = useAppDispatch(); + const pushStates = useRootSelector( syncOperationsSelectors.selectPushStates ); + + const updateState = useCallback< UpdateState< SyncPushState > >( + ( selectedSiteId, remoteSiteId, state ) => { + dispatch( + syncOperationsActions.updatePushState( { + selectedSiteId, + remoteSiteId, + state, + } ) + ); + }, + [ dispatch ] + ); + + const getPushState = useCallback< GetState< SyncPushState > >( + ( selectedSiteId, remoteSiteId ) => { + const stateId = generateStateId( selectedSiteId, remoteSiteId ); + return pushStates[ stateId ]; + }, + [ pushStates ] + ); + + const clearState = useCallback< ClearState >( + ( selectedSiteId, remoteSiteId ) => { + dispatch( + syncOperationsActions.clearPushState( { + selectedSiteId, + remoteSiteId, + } ) + ); + }, + [ dispatch ] + ); const { pushStatesProgressInfo, isKeyPushing, diff --git a/src/hooks/use-sync-states-progress-info.ts b/src/hooks/use-sync-states-progress-info.ts index 9ac5d5094..71f23c082 100644 --- a/src/hooks/use-sync-states-progress-info.ts +++ b/src/hooks/use-sync-states-progress-info.ts @@ -207,11 +207,14 @@ export function useSyncStatesProgressInfo() { : response.status; let newProgressInfo: PullStateProgressInfo | null = null; if ( response.status === 'in-progress' ) { - newProgressInfo = pullStatesProgressInfo[ frontendStatus ]; - // Update progress from the initial value to the new step proportionally to the response.progress - // on every update of the response.progress - newProgressInfo.progress = - IN_PROGRESS_INITIAL_VALUE + IN_PROGRESS_TO_DOWNLOADING_STEP * ( response.percent / 100 ); + newProgressInfo = { + ...pullStatesProgressInfo[ frontendStatus ], + // Update progress from the initial value to the new step proportionally to the response.progress + // on every update of the response.progress + progress: + IN_PROGRESS_INITIAL_VALUE + + IN_PROGRESS_TO_DOWNLOADING_STEP * ( response.percent / 100 ), + }; } const statusWithProgress = newProgressInfo || pullStatesProgressInfo[ frontendStatus ]; diff --git a/src/stores/index.ts b/src/stores/index.ts index a3b9140a5..b5774a82b 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -28,6 +28,7 @@ import { connectedSitesReducer, loadAllConnectedSites, } from 'src/stores/sync/connected-sites-slice'; +import { syncOperationsReducer } from 'src/stores/sync/sync-operations-slice'; import { wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api'; import { wordpressVersionsApi } from './wordpress-versions-api'; import type { SupportedLocale } from 'common/lib/locale'; @@ -42,6 +43,7 @@ export type RootState = { snapshot: ReturnType< typeof snapshotReducer >; sync: ReturnType< typeof syncReducer >; connectedSites: ReturnType< typeof connectedSitesReducer >; + syncOperations: ReturnType< typeof syncOperationsReducer >; wordpressVersionsApi: ReturnType< typeof wordpressVersionsApi.reducer >; wpcomApi: ReturnType< typeof wpcomApi.reducer >; wpcomPublicApi: ReturnType< typeof wpcomPublicApi.reducer >; @@ -98,6 +100,7 @@ export const rootReducer = combineReducers( { snapshot: snapshotReducer, sync: syncReducer, connectedSites: connectedSitesReducer, + syncOperations: syncOperationsReducer, wordpressVersionsApi: wordpressVersionsApi.reducer, wpcomApi: wpcomApi.reducer, wpcomPublicApi: wpcomPublicApi.reducer, diff --git a/src/stores/sync/index.ts b/src/stores/sync/index.ts index c462f4f34..da3cb0bdb 100644 --- a/src/stores/sync/index.ts +++ b/src/stores/sync/index.ts @@ -14,4 +14,9 @@ export { useSyncSitesData, useConnectedSitesOperations, } from './connected-sites-hooks'; +export { + syncOperationsReducer, + syncOperationsActions, + syncOperationsSelectors, +} from './sync-operations-slice'; export * from './sync-types'; diff --git a/src/stores/sync/sync-operations-slice.ts b/src/stores/sync/sync-operations-slice.ts new file mode 100644 index 000000000..efd803d5d --- /dev/null +++ b/src/stores/sync/sync-operations-slice.ts @@ -0,0 +1,104 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { generateStateId } from 'src/hooks/sync-sites/use-pull-push-states'; +import type { SyncBackupState, PullStates } from 'src/hooks/sync-sites/use-sync-pull'; +import type { SyncPushState, PushStates } from 'src/hooks/sync-sites/use-sync-push'; + +interface SyncOperationsState { + pullStates: PullStates; + pushStates: PushStates; +} + +const initialState: SyncOperationsState = { + pullStates: {}, + pushStates: {}, +}; + +type UpdatePullStatePayload = { + selectedSiteId: string; + remoteSiteId: number; + state: Partial< SyncBackupState >; +}; + +type UpdatePushStatePayload = { + selectedSiteId: string; + remoteSiteId: number; + state: Partial< SyncPushState >; +}; + +type ClearStatePayload = { + selectedSiteId: string; + remoteSiteId: number; +}; + +const syncOperationsSlice = createSlice( { + name: 'syncOperations', + initialState, + reducers: { + updatePullState: ( state, action: PayloadAction< UpdatePullStatePayload > ) => { + const { selectedSiteId, remoteSiteId, state: updateState } = action.payload; + const stateId = generateStateId( selectedSiteId, remoteSiteId ); + + state.pullStates[ stateId ] = { + ...state.pullStates[ stateId ], + ...updateState, + } as SyncBackupState; + }, + + clearPullState: ( state, action: PayloadAction< ClearStatePayload > ) => { + const { selectedSiteId, remoteSiteId } = action.payload; + const stateId = generateStateId( selectedSiteId, remoteSiteId ); + delete state.pullStates[ stateId ]; + }, + + updatePushState: ( state, action: PayloadAction< UpdatePushStatePayload > ) => { + const { selectedSiteId, remoteSiteId, state: updateState } = action.payload; + const stateId = generateStateId( selectedSiteId, remoteSiteId ); + + state.pushStates[ stateId ] = { + ...state.pushStates[ stateId ], + ...updateState, + } as SyncPushState; + }, + + clearPushState: ( state, action: PayloadAction< ClearStatePayload > ) => { + const { selectedSiteId, remoteSiteId } = action.payload; + const stateId = generateStateId( selectedSiteId, remoteSiteId ); + delete state.pushStates[ stateId ]; + }, + + setPullStates: ( state, action: PayloadAction< PullStates > ) => { + state.pullStates = action.payload; + }, + + setPushStates: ( state, action: PayloadAction< PushStates > ) => { + state.pushStates = action.payload; + }, + + clearAllStates: ( state ) => { + state.pullStates = {}; + state.pushStates = {}; + }, + }, +} ); + +export const syncOperationsActions = syncOperationsSlice.actions; +export const syncOperationsReducer = syncOperationsSlice.reducer; + +export const syncOperationsSelectors = { + selectPullStates: ( state: { syncOperations: SyncOperationsState } ) => + state.syncOperations.pullStates, + selectPushStates: ( state: { syncOperations: SyncOperationsState } ) => + state.syncOperations.pushStates, + selectPullState: + ( selectedSiteId: string, remoteSiteId: number ) => + ( state: { syncOperations: SyncOperationsState } ) => { + const stateId = generateStateId( selectedSiteId, remoteSiteId ); + return state.syncOperations.pullStates[ stateId ]; + }, + selectPushState: + ( selectedSiteId: string, remoteSiteId: number ) => + ( state: { syncOperations: SyncOperationsState } ) => { + const stateId = generateStateId( selectedSiteId, remoteSiteId ); + return state.syncOperations.pushStates[ stateId ]; + }, +};