From 06e460702111a3763de279ee98005e435a7e5dcd Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 9 Nov 2024 09:17:04 -0800 Subject: [PATCH 1/4] open recent files --- quadratic-client/src/app/actions.ts | 2 + quadratic-client/src/app/events/events.ts | 2 + quadratic-client/src/app/ui/QuadraticUI.tsx | 6 ++- .../TopBar/TopBarMenus/FileMenubarMenu.tsx | 48 ++++++++++++++++++- .../TopBar/TopBarMenus/updateRecentFiles.ts | 38 +++++++++++++++ .../dashboard/components/FilesListItem.tsx | 2 + quadratic-client/src/routes/file.$uuid.tsx | 5 ++ .../src/shared/components/Icons.tsx | 4 ++ 8 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts diff --git a/quadratic-client/src/app/actions.ts b/quadratic-client/src/app/actions.ts index 90cd486f7e..4cc6b02cde 100644 --- a/quadratic-client/src/app/actions.ts +++ b/quadratic-client/src/app/actions.ts @@ -1,4 +1,5 @@ import { EditorInteractionState } from '@/app/atoms/editorInteractionStateAtom'; +import { updateRecentFiles } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; import { getActionFileDuplicate } from '@/routes/api.files.$uuid'; import { apiClient } from '@/shared/api/apiClient'; import { GlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; @@ -89,6 +90,7 @@ export const deleteFile = { if (window.confirm('Please confirm you want to delete this file.')) { try { await apiClient.files.delete(uuid); + updateRecentFiles(uuid, '', false); window.location.href = '/'; } catch (e) { addGlobalSnackbar('Failed to delete file. Try again.', { severity: 'error' }); diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 2e293df7a4..20d3da309f 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -129,6 +129,8 @@ interface EventTypes { suggestionDropdownKeyboard: (key: 'ArrowDown' | 'ArrowUp' | 'Enter' | 'Escape' | 'Tab') => void; hashContentChanged: (sheetId: string, hashX: number, hashY: number) => void; + + recentFiles: (url: string, name: string, loaded: boolean) => void; } export const events = new EventEmitter(); diff --git a/quadratic-client/src/app/ui/QuadraticUI.tsx b/quadratic-client/src/app/ui/QuadraticUI.tsx index 9e5229f81b..7b9517d52a 100644 --- a/quadratic-client/src/app/ui/QuadraticUI.tsx +++ b/quadratic-client/src/app/ui/QuadraticUI.tsx @@ -20,6 +20,7 @@ import FeedbackMenu from '@/app/ui/menus/FeedbackMenu'; import SheetBar from '@/app/ui/menus/SheetBar'; import Toolbar from '@/app/ui/menus/Toolbar'; import { TopBar } from '@/app/ui/menus/TopBar/TopBar'; +import { updateRecentFiles } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; import { ValidationPanel } from '@/app/ui/menus/Validations/ValidationPanel'; import { QuadraticSidebar } from '@/app/ui/QuadraticSidebar'; import { UpdateAlertVersion } from '@/app/ui/UpdateAlertVersion'; @@ -99,7 +100,10 @@ export default function QuadraticUI() { setShowRenameFileMenu(false)} - onSave={(newValue) => renameFile(newValue)} + onSave={(newValue) => { + updateRecentFiles(uuid, newValue, true); + renameFile(newValue); + }} value={name} /> )} diff --git a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx index b94c8ed71d..4ed93d0998 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx +++ b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx @@ -4,11 +4,23 @@ import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAt import { useFileContext } from '@/app/ui/components/FileProvider'; import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; import { MenubarItemAction } from '@/app/ui/menus/TopBar/TopBarMenus/MenubarItemAction'; +import { RECENT_FILES_KEY, RecentFile } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; import { useRootRouteLoaderData } from '@/routes/_root'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; -import { DeleteIcon, DraftIcon, FileCopyIcon } from '@/shared/components/Icons'; +import { DeleteIcon, DraftIcon, FileCopyIcon, FileOpenIcon } from '@/shared/components/Icons'; import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData'; -import { MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger } from '@/shared/shadcn/ui/menubar'; +import useLocalStorage from '@/shared/hooks/useLocalStorage'; +import { + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarSeparator, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from '@/shared/shadcn/ui/menubar'; +import { useMemo } from 'react'; import { useSubmit } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; @@ -26,6 +38,36 @@ export const FileMenubarMenu = () => { const { addGlobalSnackbar } = useGlobalSnackbar(); const isAvailableArgs = useIsAvailableArgs(); + const [recentFiles] = useLocalStorage(RECENT_FILES_KEY, []); + const recentFilesMenuItems = useMemo(() => { + if (recentFiles.length === 0) return null; + + return ( + <> + + + + Open recent file + + + {recentFiles + .filter((file) => file.uuid !== fileUuid) + .map((file) => ( + { + window.location.href = `/file/${file.uuid}`; + }} + key={file.uuid} + > + {file.name} + + ))} + + + + ); + }, [fileUuid, recentFiles]); + if (!isAuthenticated) return null; return ( @@ -44,6 +86,8 @@ export const FileMenubarMenu = () => { )} + {recentFilesMenuItems} + diff --git a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts new file mode 100644 index 0000000000..ba96b59b02 --- /dev/null +++ b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts @@ -0,0 +1,38 @@ +//! This updates localStorage with a list of recently opened files on the user's +//! machine. This is called when a file is opened (successfully or not). The +//! file menu uses useLocalStorage to access this data (it cannot be done here +//! or in an Atom b/c of the timing of when the file is opened). + +export interface RecentFile { + uuid: string; + name: string; +} + +const MAX_RECENT_FILES = 10; +export const RECENT_FILES_KEY = 'recent_files'; + +export const updateRecentFiles = (uuid: string, name: string, loaded: boolean, onlyIfExists = false) => { + try { + if (loaded) { + const existing = localStorage.getItem(RECENT_FILES_KEY); + const recentFiles = existing ? JSON.parse(existing) : []; + if (onlyIfExists && !recentFiles.find((file: RecentFile) => file.uuid === uuid)) { + return; + } + const newRecentFiles = [{ uuid, name }, ...recentFiles.filter((file: RecentFile) => file.uuid !== uuid)]; + while (newRecentFiles.length > MAX_RECENT_FILES) { + newRecentFiles.pop(); + } + localStorage.setItem(RECENT_FILES_KEY, JSON.stringify(newRecentFiles)); + } else { + const existing = localStorage.getItem(RECENT_FILES_KEY); + const recentFiles = existing ? JSON.parse(existing) : []; + localStorage.setItem( + RECENT_FILES_KEY, + JSON.stringify(recentFiles.filter((file: RecentFile) => file.uuid !== uuid)) + ); + } + } catch (e) { + console.warn('Unable to update recent files', e); + } +}; diff --git a/quadratic-client/src/dashboard/components/FilesListItem.tsx b/quadratic-client/src/dashboard/components/FilesListItem.tsx index ff1efdb0f2..8a94bea683 100644 --- a/quadratic-client/src/dashboard/components/FilesListItem.tsx +++ b/quadratic-client/src/dashboard/components/FilesListItem.tsx @@ -26,6 +26,7 @@ import { Link, SubmitOptions, useFetcher, useMatch, useSubmit } from 'react-rout import { FilesListExampleFile, FilesListUserFile } from './FilesList'; import { FilesListItemCore } from './FilesListItemCore'; import { Layout, Sort, ViewPreferences } from './FilesListViewControlsDropdown'; +import { updateRecentFiles } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; export function FilesListItems({ children, @@ -126,6 +127,7 @@ export function FilesListItemUserFile({ // Update on the server and optimistically in the UI const data: FileAction['request.rename'] = { action: 'rename', name: value }; fetcherRename.submit(data, fetcherSubmitOpts); + updateRecentFiles(uuid, value, true, true); }; const handleDelete = () => { diff --git a/quadratic-client/src/routes/file.$uuid.tsx b/quadratic-client/src/routes/file.$uuid.tsx index cc4515f9cb..3d6342c296 100644 --- a/quadratic-client/src/routes/file.$uuid.tsx +++ b/quadratic-client/src/routes/file.$uuid.tsx @@ -5,6 +5,7 @@ import { thumbnail } from '@/app/gridGL/pixiApp/thumbnail'; import { isEmbed } from '@/app/helpers/isEmbed'; import initRustClient from '@/app/quadratic-rust-client/quadratic_rust_client'; import { VersionComparisonResult, compareVersions } from '@/app/schemas/compareVersions'; +import { updateRecentFiles } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; import { QuadraticApp } from '@/app/ui/QuadraticApp'; import { quadraticCore } from '@/app/web-workers/quadraticCore/quadraticCore'; import { initWorkers } from '@/app/web-workers/workers'; @@ -42,6 +43,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs): Promise { export const ZoomOutIcon: IconComponent = (props) => { return zoom_out; }; + +export const FileOpenIcon: IconComponent = (props) => { + return file_open; +}; From f88ff2554e92c21a3594e04fa53c67add1a8e2d5 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sat, 9 Nov 2024 09:20:52 -0800 Subject: [PATCH 2/4] remove empty file names (probably introduced via bug) --- .../src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx index 4ed93d0998..a6fc4e48cb 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx +++ b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx @@ -51,7 +51,7 @@ export const FileMenubarMenu = () => { {recentFiles - .filter((file) => file.uuid !== fileUuid) + .filter((file) => file.uuid !== fileUuid && file.name.trim().length > 0) .map((file) => ( { From dc2e80ee56a783939c5fd0880be83101ca3faa66 Mon Sep 17 00:00:00 2001 From: David Figatner Date: Sun, 10 Nov 2024 07:43:48 -0800 Subject: [PATCH 3/4] finish up delete --- .../src/app/ui/components/FileProvider.tsx | 4 +++- .../ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx | 8 +++++--- .../ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts | 10 ++++++++++ .../src/dashboard/components/FilesListItem.tsx | 3 ++- quadratic-client/src/routes/file.$uuid.tsx | 2 +- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/quadratic-client/src/app/ui/components/FileProvider.tsx b/quadratic-client/src/app/ui/components/FileProvider.tsx index fc48202771..c9d63fab6f 100644 --- a/quadratic-client/src/app/ui/components/FileProvider.tsx +++ b/quadratic-client/src/app/ui/components/FileProvider.tsx @@ -1,5 +1,6 @@ import { hasPermissionToEditFile } from '@/app/actions'; import { editorInteractionStatePermissionsAtom } from '@/app/atoms/editorInteractionStateAtom'; +import { updateRecentFiles } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; import { apiClient } from '@/shared/api/apiClient'; import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData'; import mixpanel from 'mixpanel-browser'; @@ -42,8 +43,9 @@ export const FileProvider = ({ children }: { children: React.ReactElement }) => (newName) => { mixpanel.track('[Files].renameCurrentFile', { newFilename: newName }); setName(newName); + updateRecentFiles(uuid, newName, true); }, - [setName] + [setName, uuid] ); // Create and save the fn used by the sheetController to save the file diff --git a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx index a6fc4e48cb..a5ea3839b1 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx +++ b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx @@ -4,7 +4,7 @@ import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAt import { useFileContext } from '@/app/ui/components/FileProvider'; import { useIsAvailableArgs } from '@/app/ui/hooks/useIsAvailableArgs'; import { MenubarItemAction } from '@/app/ui/menus/TopBar/TopBarMenus/MenubarItemAction'; -import { RECENT_FILES_KEY, RecentFile } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; +import { clearRecentFiles, RECENT_FILES_KEY, RecentFile } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; import { useRootRouteLoaderData } from '@/routes/_root'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; import { DeleteIcon, DraftIcon, FileCopyIcon, FileOpenIcon } from '@/shared/components/Icons'; @@ -40,14 +40,14 @@ export const FileMenubarMenu = () => { const [recentFiles] = useLocalStorage(RECENT_FILES_KEY, []); const recentFilesMenuItems = useMemo(() => { - if (recentFiles.length === 0) return null; + if (recentFiles.length <= 1) return null; return ( <> - Open recent file + Open recent {recentFiles @@ -62,6 +62,8 @@ export const FileMenubarMenu = () => { {file.name} ))} + + Clear recently open diff --git a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts index ba96b59b02..9c45f6c5d6 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts +++ b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles.ts @@ -11,6 +11,9 @@ export interface RecentFile { const MAX_RECENT_FILES = 10; export const RECENT_FILES_KEY = 'recent_files'; +// Updates the recent files list in localStorage. If loaded is false, then the +// file is deleted. If onlyIfExists = true, then the file is only added if it +// already exists in the list. export const updateRecentFiles = (uuid: string, name: string, loaded: boolean, onlyIfExists = false) => { try { if (loaded) { @@ -31,8 +34,15 @@ export const updateRecentFiles = (uuid: string, name: string, loaded: boolean, o RECENT_FILES_KEY, JSON.stringify(recentFiles.filter((file: RecentFile) => file.uuid !== uuid)) ); + window.dispatchEvent(new Event('local-storage')); } } catch (e) { console.warn('Unable to update recent files', e); } }; + +// Clears the recent files list in localStorage +export const clearRecentFiles = () => { + localStorage.removeItem(RECENT_FILES_KEY); + window.dispatchEvent(new Event('local-storage')); +}; diff --git a/quadratic-client/src/dashboard/components/FilesListItem.tsx b/quadratic-client/src/dashboard/components/FilesListItem.tsx index 8a94bea683..2025a70628 100644 --- a/quadratic-client/src/dashboard/components/FilesListItem.tsx +++ b/quadratic-client/src/dashboard/components/FilesListItem.tsx @@ -1,3 +1,4 @@ +import { updateRecentFiles } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; import { Action as FileAction, @@ -26,7 +27,6 @@ import { Link, SubmitOptions, useFetcher, useMatch, useSubmit } from 'react-rout import { FilesListExampleFile, FilesListUserFile } from './FilesList'; import { FilesListItemCore } from './FilesListItemCore'; import { Layout, Sort, ViewPreferences } from './FilesListViewControlsDropdown'; -import { updateRecentFiles } from '@/app/ui/menus/TopBar/TopBarMenus/updateRecentFiles'; export function FilesListItems({ children, @@ -134,6 +134,7 @@ export function FilesListItemUserFile({ if (window.confirm(`Confirm you want to delete the file: “${name}”`)) { const data = getActionFileDelete(); fetcherDelete.submit(data, fetcherSubmitOpts); + updateRecentFiles(uuid, '', false); } }; diff --git a/quadratic-client/src/routes/file.$uuid.tsx b/quadratic-client/src/routes/file.$uuid.tsx index 3d6342c296..7c02a40be4 100644 --- a/quadratic-client/src/routes/file.$uuid.tsx +++ b/quadratic-client/src/routes/file.$uuid.tsx @@ -43,7 +43,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs): Promise Date: Tue, 19 Nov 2024 11:05:03 -0700 Subject: [PATCH 4/4] Update FileMenubarMenu.tsx --- .../src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx index e9e7625b1d..978232c823 100644 --- a/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx +++ b/quadratic-client/src/app/ui/menus/TopBar/TopBarMenus/FileMenubarMenu.tsx @@ -65,7 +65,7 @@ export const FileMenubarMenu = () => { ))} - Clear recently open + Clear