From daf6b037800a857fe4033f244b9d4d09daadcaaa Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Wed, 17 Dec 2025 10:25:52 +0100 Subject: [PATCH 1/3] Add error node --- src/components/tree-view.tsx | 21 ++++++-- src/modules/sync/components/sync-dialog.tsx | 60 ++++++++++++++++----- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/components/tree-view.tsx b/src/components/tree-view.tsx index 05c530affc..b78722db0f 100644 --- a/src/components/tree-view.tsx +++ b/src/components/tree-view.tsx @@ -1,11 +1,11 @@ import { CheckboxControl, Icon, Spinner } from '@wordpress/components'; -import { file, moreHorizontal, page, plugins, brush } from '@wordpress/icons'; +import { file, moreHorizontal, page, plugins, brush, cautionFilled } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; import React from 'react'; import { RightArrowIcon } from 'src/components/icons/right-arrow'; import { cx } from 'src/lib/cx'; -type TreeNodeType = 'folder' | 'file' | 'plugin' | 'theme' | 'more'; +type TreeNodeType = 'folder' | 'file' | 'plugin' | 'theme' | 'more' | 'error'; export type TreeNode = { id: string; name: string; @@ -27,6 +27,7 @@ const TREE_NODE_ICONS: Record< TreeNodeType, React.JSX.Element > = { plugin: plugins, theme: brush, more: moreHorizontal, + error: cautionFilled, }; const updateNode = ( node: TreeNode, partialNode: Partial< TreeNode > ): TreeNode => { @@ -85,6 +86,7 @@ const TreeItem = ( { siblingsLength, disabled, renderAfterChildren, + renderEmptyContent, }: { node: TreeNode; onPatchNode: ( id: string, patchNode: Partial< TreeNode > ) => void; @@ -95,6 +97,7 @@ const TreeItem = ( { isLast?: boolean; disabled?: boolean; renderAfterChildren?: ( nodeId: string ) => React.ReactNode; + renderEmptyContent?: ( nodeId: string ) => React.ReactNode; } ) => { const { __ } = useI18n(); const isFirstLevel = level === 1; @@ -164,9 +167,13 @@ const TreeItem = ( { className={ cx( 'ps-6', isFirstLevel && 'border border-gray-300 rounded-sm py-2' ) } > { node.children.length === 0 ? ( -
- { __( 'Empty' ) } -
+ renderEmptyContent && renderEmptyContent( node.id ) ? ( + renderEmptyContent( node.id ) + ) : ( +
+ { __( 'Empty' ) } +
+ ) ) : ( node.children.map( ( child, idx ) => ( ) ) ) } @@ -194,6 +202,7 @@ export type TreeViewProps = { onExpand?: ( node: TreeNode ) => Promise< void >; disabled?: boolean; renderAfterChildren?: ( nodeId: string ) => React.ReactNode; + renderEmptyContent?: ( nodeId: string ) => React.ReactNode; }; export const TreeView = ( { @@ -202,6 +211,7 @@ export const TreeView = ( { onExpand, disabled, renderAfterChildren, + renderEmptyContent, }: TreeViewProps ) => { const handlePatchNode = ( id: string, partialNode: Partial< TreeNode > ) => { setTree( ( prev: TreeNode[] ) => updateNodeById( prev, id, partialNode ) ); @@ -221,6 +231,7 @@ export const TreeView = ( { isLast={ index === tree.length - 1 } disabled={ disabled } renderAfterChildren={ renderAfterChildren } + renderEmptyContent={ renderEmptyContent } /> ) ) } diff --git a/src/modules/sync/components/sync-dialog.tsx b/src/modules/sync/components/sync-dialog.tsx index 1ead8966ef..9f2f14645a 100644 --- a/src/modules/sync/components/sync-dialog.tsx +++ b/src/modules/sync/components/sync-dialog.tsx @@ -1,6 +1,12 @@ -import { SelectControl, Notice, __experimentalHeading as Heading } from '@wordpress/components'; +import { + SelectControl, + Notice, + __experimentalHeading as Heading, + Icon, +} from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { sprintf, __ } from '@wordpress/i18n'; +import { cautionFilled } from '@wordpress/icons'; import { useI18n } from '@wordpress/react-i18n'; import { format } from 'date-fns'; import { useState, useEffect, useCallback } from 'react'; @@ -52,8 +58,11 @@ const useDynamicTreeState = ( skip: type === 'push', } ); const { fetchChildren } = useRemoteFileTree(); - const { fetchChildren: fetchLocalChildren, isLoading: isLoadingLocalFileTree } = - useLocalFileTree(); + const { + fetchChildren: fetchLocalChildren, + isLoading: isLoadingLocalFileTree, + error: localFileTreeError, + } = useLocalFileTree(); // If the site was just created and if there is no rewind_id yet, // then all options are pre-checked to allow only a full sync @@ -92,15 +101,11 @@ const useDynamicTreeState = ( if ( type === 'push' ) { let isCancelled = false; const loadLocalTree = async () => { - try { - const localTree = await fetchLocalChildren( localSiteId, 'wp-content' ); - if ( ! isCancelled ) { - setTreeState( ( treeState ) => - updateNodeById( treeState, 'wp-content', { children: localTree } ) - ); - } - } catch ( error ) { - console.error( 'Failed to load local file tree:', error ); + const localTree = await fetchLocalChildren( localSiteId, 'wp-content' ); + if ( ! isCancelled ) { + setTreeState( ( treeState ) => + updateNodeById( treeState, 'wp-content', { children: localTree } ) + ); } }; void loadLocalTree(); @@ -118,6 +123,13 @@ const useDynamicTreeState = ( localSiteId, ] ); + // Handle local file tree errors by clearing children to show custom error message + useEffect( () => { + if ( type === 'push' && localFileTreeError ) { + setTreeState( ( treeState ) => updateNodeById( treeState, 'wp-content', { children: [] } ) ); + } + }, [ type, localFileTreeError, setTreeState ] ); + return { rewindId, fetchChildren, @@ -125,6 +137,7 @@ const useDynamicTreeState = ( isLoadingRewindId, isErrorRewindId, isLoadingLocalFileTree, + localFileTreeError, }; }; @@ -155,8 +168,14 @@ export function SyncDialog( { formattedOverAmount, } = useSelectedItemsPushSize( localSite.id, treeState, type ); - const { fetchChildren, rewindId, isLoadingRewindId, isErrorRewindId, isLoadingLocalFileTree } = - useDynamicTreeState( type, localSite.id, remoteSite.id, setTreeState ); + const { + fetchChildren, + rewindId, + isLoadingRewindId, + isErrorRewindId, + isLoadingLocalFileTree, + localFileTreeError, + } = useDynamicTreeState( type, localSite.id, remoteSite.id, setTreeState ); const [ wpVersion ] = useGetWpVersion( localSite ); const { data: wpVersions = [] } = useGetWordPressVersions( { @@ -355,6 +374,19 @@ export function SyncDialog( { } return null; } } + renderEmptyContent={ ( nodeId ) => { + if ( nodeId === 'wp-content' && type === 'push' && localFileTreeError ) { + return ( +
+ + { __( + 'Error retrieving files and directories. Please close and reopen this dialog to try again.' + ) } +
+ ); + } + return null; + } } /> ) } From 6cd14e63ab14a15a37e4e9447813b2cfa348e66d Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Wed, 17 Dec 2025 11:25:20 +0100 Subject: [PATCH 2/3] Add error handling for fetching remote children --- src/modules/sync/components/sync-dialog.tsx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/modules/sync/components/sync-dialog.tsx b/src/modules/sync/components/sync-dialog.tsx index 9f2f14645a..28e5019a14 100644 --- a/src/modules/sync/components/sync-dialog.tsx +++ b/src/modules/sync/components/sync-dialog.tsx @@ -57,7 +57,7 @@ const useDynamicTreeState = ( } = useLatestRewindId( remoteSiteId, { skip: type === 'push', } ); - const { fetchChildren } = useRemoteFileTree(); + const { fetchChildren, error: remoteFileTreeError } = useRemoteFileTree(); const { fetchChildren: fetchLocalChildren, isLoading: isLoadingLocalFileTree, @@ -130,6 +130,13 @@ const useDynamicTreeState = ( } }, [ type, localFileTreeError, setTreeState ] ); + // Handle remote file tree errors by clearing children to show custom error message + useEffect( () => { + if ( type === 'pull' && remoteFileTreeError ) { + setTreeState( ( treeState ) => updateNodeById( treeState, 'wp-content', { children: [] } ) ); + } + }, [ type, remoteFileTreeError, setTreeState ] ); + return { rewindId, fetchChildren, @@ -138,6 +145,7 @@ const useDynamicTreeState = ( isErrorRewindId, isLoadingLocalFileTree, localFileTreeError, + remoteFileTreeError, }; }; @@ -175,6 +183,7 @@ export function SyncDialog( { isErrorRewindId, isLoadingLocalFileTree, localFileTreeError, + remoteFileTreeError, } = useDynamicTreeState( type, localSite.id, remoteSite.id, setTreeState ); const [ wpVersion ] = useGetWpVersion( localSite ); @@ -385,6 +394,16 @@ export function SyncDialog( { ); } + if ( nodeId === 'wp-content' && type === 'pull' && remoteFileTreeError ) { + return ( +
+ + { __( + 'Error retrieving remote files and directories. Please close and reopen this dialog to try again.' + ) } +
+ ); + } return null; } } /> From 0e7de728d1e55fe36241442ca9727f7cf61b40ad Mon Sep 17 00:00:00 2001 From: Kateryna Kodonenko Date: Wed, 17 Dec 2025 11:36:27 +0100 Subject: [PATCH 3/3] Fix use effect --- src/modules/sync/components/sync-dialog.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/modules/sync/components/sync-dialog.tsx b/src/modules/sync/components/sync-dialog.tsx index 28e5019a14..8facd1057c 100644 --- a/src/modules/sync/components/sync-dialog.tsx +++ b/src/modules/sync/components/sync-dialog.tsx @@ -123,19 +123,12 @@ const useDynamicTreeState = ( localSiteId, ] ); - // Handle local file tree errors by clearing children to show custom error message + // Handle file tree errors by clearing children to show custom error message useEffect( () => { - if ( type === 'push' && localFileTreeError ) { + if ( ( type === 'push' && localFileTreeError ) || ( type === 'pull' && remoteFileTreeError ) ) { setTreeState( ( treeState ) => updateNodeById( treeState, 'wp-content', { children: [] } ) ); } - }, [ type, localFileTreeError, setTreeState ] ); - - // Handle remote file tree errors by clearing children to show custom error message - useEffect( () => { - if ( type === 'pull' && remoteFileTreeError ) { - setTreeState( ( treeState ) => updateNodeById( treeState, 'wp-content', { children: [] } ) ); - } - }, [ type, remoteFileTreeError, setTreeState ] ); + }, [ type, localFileTreeError, remoteFileTreeError, setTreeState ] ); return { rewindId,