Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/pages/Home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import type {
} from '@/lib/electron-api';
import { TooltipProvider } from '@/components/ui/tooltip';
import { ELECTRON_RECENT_NOTES_STORAGE_KEY } from '@/lib/electron-recent-notes';
import type { LocalVaultSnapshot } from '@/lib/local-vault';
import { defaultSettings } from '@/lib/settings';
import { LOCAL_VAULT_TEAM_PATH } from './electron-home/local-vault-adapter';
import { LAST_WORKSPACE_SCOPE_KEY } from './electron-home/ui-preferences';
import { Home } from './Home';

Expand Down Expand Up @@ -2531,6 +2533,66 @@ describe('Home native-feel behavior', () => {
expect(await screen.findByDisplayValue('Draft Product Plan')).toBeInTheDocument();
});

it('waits for the Local Vault snapshot before opening a local recent note', async () => {
window.localStorage.setItem(ELECTRON_RECENT_NOTES_STORAGE_KEY, JSON.stringify([{
noteId: 'local-note',
teamPath: LOCAL_VAULT_TEAM_PATH,
title: 'Local Recent',
shortId: 'Local Recent.md',
lastOpenedAtMillis: 3000,
}]));
const localNote = {
id: 'local-note',
title: 'Local Recent',
relativePath: 'Local Recent.md',
parentPath: null,
createdAtMillis: 1,
updatedAtMillis: 2,
revision: { contentHash: 'local-hash', mtimeMs: 2 },
};
const snapshot: LocalVaultSnapshot = {
vaultId: 'vault-1',
rootPath: '/tmp/vault',
folders: [],
notes: [localNote],
};
let resolveSnapshot!: (snapshot: LocalVaultSnapshot) => void;
const snapshotPromise = new Promise<LocalVaultSnapshot>((resolve) => {
resolveSnapshot = resolve;
});
const getSnapshot = vi.fn(() => snapshotPromise);
const readNote = vi.fn(async () => ({ ...localNote, content: '# Local Recent' }));
const api = createApi({
settings: {
get: vi.fn(async () => createSafeSettings({
hasLocalVault: true,
localVault: { path: '/tmp/vault' },
})),
},
localVault: { getSnapshot, readNote },
});

renderHome(api);
await waitFor(() => expect(getSnapshot).toHaveBeenCalledOnce());
fireEvent.keyDown(window, { key: 'k', metaKey: true });
const palette = await screen.findByRole('dialog', { name: 'Command Palette' });
fireEvent.click(within(palette).getByText('Local Recent'));

expect(JSON.parse(window.localStorage.getItem(ELECTRON_RECENT_NOTES_STORAGE_KEY) ?? '[]')).toEqual([
expect.objectContaining({ noteId: 'local-note', teamPath: LOCAL_VAULT_TEAM_PATH }),
]);
expect(api.hackmd.listTeamNotes).not.toHaveBeenCalledWith(LOCAL_VAULT_TEAM_PATH);
expect(readNote).not.toHaveBeenCalled();

await act(async () => {
resolveSnapshot(snapshot);
await snapshotPromise;
});

expect(await screen.findByDisplayValue('Local Recent')).toBeInTheDocument();
expect(readNote).toHaveBeenCalledWith('local-note');
});

it('switches to a team workspace and opens a pending team recent note', async () => {
window.localStorage.setItem(ELECTRON_RECENT_NOTES_STORAGE_KEY, JSON.stringify([{
noteId: 'team-note',
Expand Down
10 changes: 8 additions & 2 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
} from './electron-home/useWorkbenchWorkspaceState';
import { getWorkspaceNavigationTeams } from './electron-home/workspace-navigation';

export function Home() {

Check warning on line 66 in src/pages/Home.tsx

View workflow job for this annotation

GitHub Actions / quality

react-doctor(no-giant-component)

Component "Home" is 744 lines long, which is hard to read & change. Split it into a few smaller components.
const { presets, presetId, resolvedMode, setPresetId, setTheme, theme } = useTheme();
const queryClient = useQueryClient();
const api = getDesktopAPI();
Expand Down Expand Up @@ -200,6 +200,12 @@
const documentsByKey = scope.type === 'local' ? localVault.documentsByKey : remoteDocumentsByKey;
const documentQueries = scope.type === 'local' ? localVault.documentQueries : remoteDocumentQueries;
const queries = remoteQueries;
const isWorkspaceFetching = scope.type === 'local'
? localVault.snapshotQuery.isFetching
: queries.notesQuery.isFetching;
const isWorkspaceLoading = scope.type === 'local'
? localVault.snapshotQuery.isLoading || localVault.snapshotQuery.isError
: queries.notesQuery.isLoading;
const hasConfiguredLocalVault = settings?.hasLocalVault === true;
const canUseCurrentWorkspace = hasToken || (scope.type === 'local' && hasConfiguredLocalVault);
const handleHackmdDisconnected = useCallback(() => {
Expand Down Expand Up @@ -453,8 +459,8 @@
expandNavigator,
focusNavigator: () => focusZone('navigator'),
handleShowFinderResults,
isNotesFetching: queries.notesQuery.isFetching,
isNotesLoading: queries.notesQuery.isLoading,
isWorkspaceFetching,
isWorkspaceLoading,
palette,
recentNotes,
removeRecentNoteEntry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ function createOptions(overrides: Partial<ElectronHomeCommandPaletteOptions> = {
expandNavigator: vi.fn(),
focusNavigator: vi.fn(),
handleShowFinderResults: vi.fn(),
isNotesFetching: false,
isNotesLoading: false,
isWorkspaceFetching: false,
isWorkspaceLoading: false,
palette: { mode: 'commands', open: false, search: '' },
recentNotes: [recent()],
removeRecentNoteEntry: vi.fn(),
Expand Down
16 changes: 8 additions & 8 deletions src/pages/electron-home/useElectronHomeCommandPalette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export type ElectronHomeCommandPaletteOptions = {
expandNavigator: () => void;
focusNavigator: () => void;
handleShowFinderResults: (query: string) => void;
isNotesFetching: boolean;
isNotesLoading: boolean;
isWorkspaceFetching: boolean;
isWorkspaceLoading: boolean;
palette: CommandPaletteState;
recentNotes: ElectronRecentNote[];
removeRecentNoteEntry: (noteId: string, teamPath: string | null) => void;
Expand All @@ -35,8 +35,8 @@ export function useElectronHomeCommandPalette({
expandNavigator,
focusNavigator,
handleShowFinderResults,
isNotesFetching,
isNotesLoading,
isWorkspaceFetching,
isWorkspaceLoading,
palette,
recentNotes,
removeRecentNoteEntry,
Expand All @@ -63,8 +63,8 @@ export function useElectronHomeCommandPalette({
clearPendingRecentNote,
queuePendingRecentNote,
} = usePendingRecentNoteRestore({
isNotesFetching,
isNotesLoading,
isWorkspaceFetching,
isWorkspaceLoading,
removeRecentNoteEntry,
revealNoteEntry,
scope,
Expand All @@ -84,8 +84,8 @@ export function useElectronHomeCommandPalette({
} = useWorkbenchQuickOpen({
expandNavigator,
focusNavigator,
isNotesFetching,
isNotesLoading,
isWorkspaceFetching,
isWorkspaceLoading,
clearPendingRecentNote,
queuePendingRecentNote,
removeRecentNoteEntry,
Expand Down
47 changes: 41 additions & 6 deletions src/pages/electron-home/usePendingRecentNoteRestore.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { NoteSummary } from '@/lib/electron-api';
import type { ElectronRecentNote } from '@/lib/electron-recent-notes';
import { buildHackmdFolderTree } from '@/lib/hackmd-folders';

import { LOCAL_VAULT_TEAM_PATH } from './local-vault-adapter';
import { usePendingRecentNoteRestore } from './usePendingRecentNoteRestore';

vi.mock('@/components/ui/toast', () => ({
Expand Down Expand Up @@ -61,8 +62,8 @@ describe('usePendingRecentNoteRestore', () => {
const revealNoteEntry = vi.fn(async () => true);

const { result } = renderHook(() => usePendingRecentNoteRestore({
isNotesFetching: false,
isNotesLoading: false,
isWorkspaceFetching: false,
isWorkspaceLoading: false,
removeRecentNoteEntry: vi.fn(),
revealNoteEntry,
scope: { type: 'personal', label: 'My Workspace' },
Expand All @@ -83,8 +84,8 @@ describe('usePendingRecentNoteRestore', () => {
const removeRecentNoteEntry = vi.fn();

const { result } = renderHook(() => usePendingRecentNoteRestore({
isNotesFetching: false,
isNotesLoading: false,
isWorkspaceFetching: false,
isWorkspaceLoading: false,
removeRecentNoteEntry,
revealNoteEntry: vi.fn(async () => true),
scope: { type: 'personal', label: 'My Workspace' },
Expand All @@ -106,8 +107,8 @@ describe('usePendingRecentNoteRestore', () => {
const revealNoteEntry = vi.fn(async () => true);

const { result } = renderHook(() => usePendingRecentNoteRestore({
isNotesFetching: false,
isNotesLoading: false,
isWorkspaceFetching: false,
isWorkspaceLoading: false,
removeRecentNoteEntry: vi.fn(),
revealNoteEntry,
scope: { type: 'personal', label: 'My Workspace' },
Expand All @@ -121,4 +122,38 @@ describe('usePendingRecentNoteRestore', () => {
expect(revealNoteEntry).not.toHaveBeenCalled();
expect(result.current.getPendingRecentNote()?.noteId).toBe('team-note');
});

it('waits for the Local Vault snapshot before restoring a local recent note', async () => {
const localRecent = recent({ noteId: 'local-note', teamPath: LOCAL_VAULT_TEAM_PATH });
const loadedTree = buildHackmdFolderTree([note({
id: 'local-note',
title: 'Local note',
teamPath: LOCAL_VAULT_TEAM_PATH,
})]);
const revealNoteEntry = vi.fn(async () => true);
const removeRecentNoteEntry = vi.fn();
const { result, rerender } = renderHook(({ isWorkspaceLoading, tree }) => usePendingRecentNoteRestore({
isWorkspaceFetching: false,
isWorkspaceLoading,
removeRecentNoteEntry,
revealNoteEntry,
scope: { type: 'local', label: 'Local Vault' },
tree,
}), {
initialProps: { isWorkspaceLoading: true, tree: buildHackmdFolderTree([]) },
});

act(() => {
result.current.queuePendingRecentNote(localRecent);
});

expect(result.current.getPendingRecentNote()).toEqual(localRecent);
expect(removeRecentNoteEntry).not.toHaveBeenCalled();

rerender({ isWorkspaceLoading: false, tree: loadedTree });

await waitFor(() => expect(revealNoteEntry).toHaveBeenCalledWith(loadedTree.allNotes[0]));
expect(removeRecentNoteEntry).not.toHaveBeenCalled();
expect(result.current.getPendingRecentNote()).toBeNull();
});
});
27 changes: 19 additions & 8 deletions src/pages/electron-home/usePendingRecentNoteRestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,38 @@ import {
import type { FolderTree, FolderTreeNote } from '@/lib/hackmd-folders';

import type { WorkspaceScope } from './types';
import { LOCAL_VAULT_TEAM_PATH } from './local-vault-adapter';

export type PendingRecentNoteRestoreOptions = {
isNotesFetching: boolean;
isNotesLoading: boolean;
isWorkspaceFetching: boolean;
isWorkspaceLoading: boolean;
removeRecentNoteEntry: (noteId: string, teamPath: string | null) => void;
revealNoteEntry: (entry: FolderTreeNote) => Promise<boolean>;
scope: WorkspaceScope;
tree: FolderTree;
};

export function recentNoteTargetsScope(note: ElectronRecentNote, scope: WorkspaceScope) {
if (note.teamPath === LOCAL_VAULT_TEAM_PATH) {
return scope.type === 'local';
}

if (note.teamPath === null) {
return scope.type === 'personal';
}

return scope.type === 'team' && scope.teamPath === note.teamPath;
}

export type PendingRecentNoteRestoreController = {
clearPendingRecentNote: () => void;
getPendingRecentNote: () => ElectronRecentNote | null;
queuePendingRecentNote: (note: ElectronRecentNote) => void;
};

export function usePendingRecentNoteRestore({
isNotesFetching,
isNotesLoading,
isWorkspaceFetching,
isWorkspaceLoading,
removeRecentNoteEntry,
revealNoteEntry,
scope,
Expand Down Expand Up @@ -55,9 +68,7 @@ export function usePendingRecentNoteRestore({
return;
}

const currentTeamPath = scope.type === 'team' ? scope.teamPath : null;
const isTargetScopeLoaded = scope.type !== 'history' && currentTeamPath === pendingRecentNote.teamPath;
if (!isTargetScopeLoaded || isNotesLoading || isNotesFetching) {
if (!recentNoteTargetsScope(pendingRecentNote, scope) || isWorkspaceLoading || isWorkspaceFetching) {
Comment thread
EastSun5566 marked this conversation as resolved.
Outdated
return;
}

Expand All @@ -70,7 +81,7 @@ export function usePendingRecentNoteRestore({

removeRecentNoteEntryRef.current(pendingRecentNote.noteId, pendingRecentNote.teamPath);
toast.info(`“${pendingRecentNote.title || 'Untitled'}” is no longer available in this workspace.`);
}, [isNotesFetching, isNotesLoading, scope, tree.allNotes]);
}, [isWorkspaceFetching, isWorkspaceLoading, scope, tree.allNotes]);

useEffect(() => {
restorePendingRecentNote();
Expand Down
18 changes: 16 additions & 2 deletions src/pages/electron-home/useWorkbenchQuickOpen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { QuickOpenFolderResult, QuickOpenWorkspaceResult } from '@/lib/elec
import type { ElectronRecentNote } from '@/lib/electron-recent-notes';
import { buildHackmdFolderTree } from '@/lib/hackmd-folders';

import { LOCAL_VAULT_TEAM_PATH } from './local-vault-adapter';
import { useWorkbenchQuickOpen, type WorkbenchQuickOpenOptions } from './useWorkbenchQuickOpen';

vi.mock('@/components/ui/toast', () => ({
Expand Down Expand Up @@ -75,8 +76,8 @@ function createOptions(overrides: Partial<WorkbenchQuickOpenOptions> = {}): Work
return {
expandNavigator: vi.fn(),
focusNavigator: vi.fn(),
isNotesFetching: false,
isNotesLoading: false,
isWorkspaceFetching: false,
isWorkspaceLoading: false,
clearPendingRecentNote: vi.fn(),
queuePendingRecentNote: vi.fn(),
removeRecentNoteEntry: vi.fn(),
Expand Down Expand Up @@ -130,6 +131,19 @@ describe('useWorkbenchQuickOpen', () => {
expect(options.focusNavigator).toHaveBeenCalledOnce();
});

it('switches to Local Vault before opening a local recent note', () => {
const options = createOptions();
const entry = recent({ noteId: 'local-note', teamPath: LOCAL_VAULT_TEAM_PATH, title: 'Local note' });
const { result } = renderHook(() => useWorkbenchQuickOpen(options));

result.current.handleQuickOpenRecentNote(entry);

expect(options.queuePendingRecentNote).toHaveBeenCalledWith(entry);
expect(options.setWorkspaceScope).toHaveBeenCalledWith({ type: 'local', label: 'Local Vault' });
expect(toast.info).toHaveBeenCalledWith('Loading Local Vault before opening “Local note”.');
expect(options.focusNavigator).toHaveBeenCalledOnce();
});

it('opens workspace and folder quick-open results through navigator focus', () => {
const options = createOptions();
const workspace: QuickOpenWorkspaceResult = {
Expand Down
Loading
Loading