From 56a90b543ab385891aa8937902f2ff02fdff07e1 Mon Sep 17 00:00:00 2001 From: Reece Date: Tue, 1 Sep 2026 13:19:48 +0100 Subject: [PATCH] refactor(editor): one top bar in the file library, not two The library kept a header of its own, so with the workbench bar now showing it had two stacked bars - and two search boxes, since both carried a SuperSearch. Its Refresh, New folder, Upload and Upload-from-Mobile controls register into the workbench bar through the same hook the viewer and page editor use, and the header goes. What stays in the view is the breadcrumb and tab name: that says where you are rather than doing anything, and the bar has nowhere to put it. The handlers passed to the bar are memoised. Registration re-runs whenever they change identity, so an arrow defined in the call loops until React gives up. --- .../components/filesPage/FileManagerView.tsx | 199 +++++------------- .../core/components/filesPage/FilesPage.css | 98 +-------- .../useFileLibraryWorkbenchBarButtons.tsx | 101 +++++++++ 3 files changed, 169 insertions(+), 229 deletions(-) create mode 100644 frontend/editor/src/core/components/filesPage/useFileLibraryWorkbenchBarButtons.tsx diff --git a/frontend/editor/src/core/components/filesPage/FileManagerView.tsx b/frontend/editor/src/core/components/filesPage/FileManagerView.tsx index 4665784e79..843b11847c 100644 --- a/frontend/editor/src/core/components/filesPage/FileManagerView.tsx +++ b/frontend/editor/src/core/components/filesPage/FileManagerView.tsx @@ -22,8 +22,6 @@ import { useMediaQuery } from "@mantine/hooks"; import CloseIcon from "@mui/icons-material/Close"; import SearchIcon from "@mui/icons-material/Search"; import UploadFileIcon from "@mui/icons-material/UploadFile"; -import QrCode2Icon from "@mui/icons-material/QrCode2"; -import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; import GridViewIcon from "@mui/icons-material/GridView"; import ViewListIcon from "@mui/icons-material/ViewList"; import DeleteIcon from "@mui/icons-material/Delete"; @@ -32,11 +30,11 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; -import RefreshIcon from "@mui/icons-material/Refresh"; import { FilesToolbarBulkMenu } from "@app/components/filesPage/FilesToolbarBulkMenu"; import { FilesToolbarCount } from "@app/components/filesPage/FilesToolbarCount"; import { FilesToolbarFilterMenu } from "@app/components/filesPage/FilesToolbarFilterMenu"; import { FilesToolbarSortMenu } from "@app/components/filesPage/FilesToolbarSortMenu"; +import { useFileLibraryWorkbenchBarButtons } from "@app/components/filesPage/useFileLibraryWorkbenchBarButtons"; import { stripBasePath } from "@app/constants/app"; import { useAuth } from "@app/auth/UseSession"; @@ -63,8 +61,6 @@ import { StirlingFileStub } from "@app/types/fileContext"; import { FolderId, ROOT_FOLDER_ID } from "@app/types/folder"; import { FileGrid, FilesPageEntry } from "@app/components/filesPage/FileGrid"; -import SuperSearch from "@app/components/shared/superSearch/SuperSearch"; -import { useEditorSearchScopes } from "@app/hooks/useSuperSearch"; import { FileDetailsPanel } from "@app/components/filesPage/FileDetailsPanel"; import BulkUploadToServerModal from "@app/components/shared/BulkUploadToServerModal"; import MobileUploadModal from "@app/components/shared/MobileUploadModal"; @@ -92,7 +88,6 @@ export default function FileManagerView() { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); - const searchScopes = useEditorSearchScopes(); // Hide Shared tab when storageSharingEnabled is false. const { sharingEnabled } = useSharingEnabled(); @@ -914,6 +909,32 @@ export default function FileManagerView() { ); // null = New folder actionable; string = disabled tooltip reason. + // Lifted out of the header that used to render it: the workbench bar owns these + // controls now, and a handler defined inside JSX cannot be handed to it. + const handleRefresh = useCallback(async () => { + setRefreshing(true); + try { + // pullFromServer bumps the folder revision, which the FolderProvider's effect + // reacts to by re-running refresh() - no need to await folders.refresh() here. + const result = await folders.pullFromServer(); + if (!result.ok && result.reason !== "endpoint-missing") { + folders.setError( + result.reason === "network" + ? t("filesPage.syncError.network", "Could not reach the server.") + : result.reason === "server" + ? t( + "filesPage.syncError.server", + "Server error during folder sync.", + ) + : t("filesPage.syncError.client", "Folder sync failed."), + ); + } + await refresh(); + } finally { + setRefreshing(false); + } + }, [folders, refresh, t]); + const newFolderDisabledReason: string | null = useMemo(() => { // Guests can't use cloud folders at all - say so before any tab/storage // hint, since switching tabs wouldn't help them. @@ -945,23 +966,35 @@ export default function FileManagerView() { return null; }, [signInRequiredReason, currentTab, folders.serverReachable, t]); + // Stable identities: the bar re-registers whenever these change, and a fresh arrow + // per render turns that into an endless register -> render -> register loop. + const openFilePicker = useCallback(() => fileInputRef.current?.click(), []); + const openMobileUpload = useCallback( + () => setMobileUploadModalOpen(true), + [], + ); + + useFileLibraryWorkbenchBarButtons({ + onRefresh: handleRefresh, + refreshing, + refreshDisabledReason: signInRequiredReason, + onNewFolder: openNewFolderDialog, + newFolderDisabledReason, + onUpload: openFilePicker, + onUploadFromMobile: isMobileUploadAvailable ? openMobileUpload : undefined, + }); + return (
-
- {/* Breadcrumb only for folder-rooted tabs. */} + {/* Folder context, not a bar: the controls that used to sit beside this are + registered into the workbench bar the other views use. */} +
{(currentTab === "all" || currentTab === "cloud") && } {(currentTab === "local" || currentTab === "recent" || currentTab === "shared" || currentTab === "sharedByMe") && ( -
+ {currentTab === "local" ? t("filesPage.tabName.local", "Local") : currentTab === "recent" @@ -969,132 +1002,16 @@ export default function FileManagerView() { : currentTab === "shared" ? t("filesPage.tabName.shared", "Shared with me") : t("filesPage.tabName.sharedByMe", "Shared by me")} -
+ )} - {(() => { - // Both the inline desktop buttons and the mobile kebab menu need - // these handlers - extract once so we don't drift two copies. - const handleRefresh = async () => { - setRefreshing(true); - try { - // pullFromServer bumps the folder revision, which the - // FolderProvider's effect reacts to by re-running refresh() - - // no need to await folders.refresh() manually. - const result = await folders.pullFromServer(); - if (!result.ok && result.reason !== "endpoint-missing") { - folders.setError( - result.reason === "network" - ? t( - "filesPage.syncError.network", - "Could not reach the server.", - ) - : result.reason === "server" - ? t( - "filesPage.syncError.server", - "Server error during folder sync.", - ) - : t("filesPage.syncError.client", "Folder sync failed."), - ); - } - await refresh(); - } finally { - setRefreshing(false); - } - }; - return ( - <> -
- -
-
- - - - - - {newFolderDisabledReason ? ( - - - - - - ) : ( - - )} - - {isMobileUploadAvailable && ( - - setMobileUploadModalOpen(true)} - aria-label={t( - "filesPage.uploadFromMobile", - "Upload from Mobile", - )} - > - - - - )} - -
- - ); - })()} -
+
+ {folders.error && (