diff --git a/src/components/tree-view.tsx b/src/components/tree-view.tsx
index 05c530aff..b78722db0 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 1ead8966e..8facd1057 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';
@@ -51,9 +57,12 @@ const useDynamicTreeState = (
} = useLatestRewindId( remoteSiteId, {
skip: type === 'push',
} );
- const { fetchChildren } = useRemoteFileTree();
- const { fetchChildren: fetchLocalChildren, isLoading: isLoadingLocalFileTree } =
- useLocalFileTree();
+ const { fetchChildren, error: remoteFileTreeError } = useRemoteFileTree();
+ 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 file tree errors by clearing children to show custom error message
+ useEffect( () => {
+ if ( ( type === 'push' && localFileTreeError ) || ( type === 'pull' && remoteFileTreeError ) ) {
+ setTreeState( ( treeState ) => updateNodeById( treeState, 'wp-content', { children: [] } ) );
+ }
+ }, [ type, localFileTreeError, remoteFileTreeError, setTreeState ] );
+
return {
rewindId,
fetchChildren,
@@ -125,6 +137,8 @@ const useDynamicTreeState = (
isLoadingRewindId,
isErrorRewindId,
isLoadingLocalFileTree,
+ localFileTreeError,
+ remoteFileTreeError,
};
};
@@ -155,8 +169,15 @@ 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,
+ remoteFileTreeError,
+ } = useDynamicTreeState( type, localSite.id, remoteSite.id, setTreeState );
const [ wpVersion ] = useGetWpVersion( localSite );
const { data: wpVersions = [] } = useGetWordPressVersions( {
@@ -355,6 +376,29 @@ 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.'
+ ) }
+
+ );
+ }
+ 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;
+ } }
/>
>
) }