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; + } } /> ) }