-
Notifications
You must be signed in to change notification settings - Fork 963
feat(desktop): Recently Viewed section in v2 Quick Open #3488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export const RECENT_STORE_LIMIT = 25; | ||
| export const RECENT_DISPLAY_LIMIT = 10; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export { RECENT_DISPLAY_LIMIT, RECENT_STORE_LIMIT } from "./constants"; | ||
| export { | ||
| type RecentFile, | ||
| type RecentlyViewedFilesApi, | ||
| useRecentlyViewedFiles, | ||
| } from "./useRecentlyViewedFiles"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { eq } from "@tanstack/db"; | ||
| import { useLiveQuery } from "@tanstack/react-db"; | ||
| import { useCallback, useMemo } from "react"; | ||
| import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; | ||
| import { RECENT_STORE_LIMIT } from "./constants"; | ||
|
|
||
| export interface RecentFile { | ||
| relativePath: string; | ||
| absolutePath: string; | ||
| lastAccessedAt: number; | ||
| } | ||
|
|
||
| interface RecentFileInput { | ||
| relativePath: string; | ||
| absolutePath: string; | ||
| } | ||
|
|
||
| export interface RecentlyViewedFilesApi { | ||
| recentFiles: RecentFile[]; | ||
| recordView: (file: RecentFileInput) => void; | ||
| } | ||
|
|
||
| export function useRecentlyViewedFiles( | ||
| workspaceId: string, | ||
| ): RecentlyViewedFilesApi { | ||
| const collections = useCollections(); | ||
|
|
||
| const { data: rows = [] } = useLiveQuery( | ||
| (query) => | ||
| query | ||
| .from({ state: collections.v2WorkspaceLocalState }) | ||
| .where(({ state }) => eq(state.workspaceId, workspaceId)), | ||
| [collections, workspaceId], | ||
| ); | ||
| const recentFiles = useMemo(() => rows[0]?.recentlyViewedFiles ?? [], [rows]); | ||
|
|
||
| const recordView = useCallback( | ||
| (file: RecentFileInput) => { | ||
| if (!collections.v2WorkspaceLocalState.get(workspaceId)) return; | ||
| collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { | ||
| const current = draft.recentlyViewedFiles ?? []; | ||
| const withoutDup = current.filter( | ||
| (f) => f.relativePath !== file.relativePath, | ||
| ); | ||
| draft.recentlyViewedFiles = [ | ||
| { | ||
| relativePath: file.relativePath, | ||
| absolutePath: file.absolutePath, | ||
| lastAccessedAt: Date.now(), | ||
| }, | ||
| ...withoutDup, | ||
| ].slice(0, RECENT_STORE_LIMIT); | ||
| }); | ||
| }, | ||
| [collections, workspaceId], | ||
| ); | ||
|
|
||
| return { recentFiles, recordView }; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,12 @@ | ||||||||||||||||||||||||||||||||||||||||
| import * as DialogPrimitive from "@radix-ui/react-dialog"; | ||||||||||||||||||||||||||||||||||||||||
| import { CommandPrimitive } from "@superset/ui/command"; | ||||||||||||||||||||||||||||||||||||||||
| import { CommandPrimitive, CommandSeparator } from "@superset/ui/command"; | ||||||||||||||||||||||||||||||||||||||||
| import { SearchIcon } from "lucide-react"; | ||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||
| import { LuChevronDown, LuChevronRight } from "react-icons/lu"; | ||||||||||||||||||||||||||||||||||||||||
| import type { RecentFile } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useRecentlyViewedFiles"; | ||||||||||||||||||||||||||||||||||||||||
| import { RECENT_DISPLAY_LIMIT } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useRecentlyViewedFiles"; | ||||||||||||||||||||||||||||||||||||||||
| import { useFileSearch } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/hooks/useFileSearch/useFileSearch"; | ||||||||||||||||||||||||||||||||||||||||
| import { FileIcon } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/utils"; | ||||||||||||||||||||||||||||||||||||||||
| import { FileResultItem } from "./components/FileResultItem"; | ||||||||||||||||||||||||||||||||||||||||
| import { useV2FileSearch } from "./hooks/useV2FileSearch"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // 48px input + 10 * 40px items | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -17,6 +19,13 @@ export interface CommandPaletteProps { | |||||||||||||||||||||||||||||||||||||||
| onOpenChange: (open: boolean) => void; | ||||||||||||||||||||||||||||||||||||||||
| onSelectFile: (filePath: string) => void; | ||||||||||||||||||||||||||||||||||||||||
| variant?: "v1" | "v2"; | ||||||||||||||||||||||||||||||||||||||||
| recentlyViewedFiles?: RecentFile[]; | ||||||||||||||||||||||||||||||||||||||||
| openFilePaths?: Set<string>; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function getFileName(relativePath: string): string { | ||||||||||||||||||||||||||||||||||||||||
| const segments = relativePath.split("/"); | ||||||||||||||||||||||||||||||||||||||||
| return segments[segments.length - 1] ?? relativePath; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export function CommandPalette({ | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -25,6 +34,8 @@ export function CommandPalette({ | |||||||||||||||||||||||||||||||||||||||
| onOpenChange, | ||||||||||||||||||||||||||||||||||||||||
| onSelectFile, | ||||||||||||||||||||||||||||||||||||||||
| variant = "v1", | ||||||||||||||||||||||||||||||||||||||||
| recentlyViewedFiles, | ||||||||||||||||||||||||||||||||||||||||
| openFilePaths, | ||||||||||||||||||||||||||||||||||||||||
| }: CommandPaletteProps) { | ||||||||||||||||||||||||||||||||||||||||
| const [query, setQuery] = useState(""); | ||||||||||||||||||||||||||||||||||||||||
| const [filtersOpen, setFiltersOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -45,7 +56,45 @@ export function CommandPalette({ | |||||||||||||||||||||||||||||||||||||||
| variant === "v2" ? query : "", | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const results = variant === "v2" ? v2Search.results : v1Search.searchResults; | ||||||||||||||||||||||||||||||||||||||||
| const rawResults = | ||||||||||||||||||||||||||||||||||||||||
| variant === "v2" ? v2Search.results : v1Search.searchResults; | ||||||||||||||||||||||||||||||||||||||||
| const trimmedQuery = query.trim(); | ||||||||||||||||||||||||||||||||||||||||
| const hasQuery = trimmedQuery.length > 0; | ||||||||||||||||||||||||||||||||||||||||
| const showRecentSection = variant === "v2" && Boolean(recentlyViewedFiles); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const orderedRecent = useMemo<RecentFile[]>(() => { | ||||||||||||||||||||||||||||||||||||||||
| if (!showRecentSection || !recentlyViewedFiles) return []; | ||||||||||||||||||||||||||||||||||||||||
| const openSet = openFilePaths ?? new Set<string>(); | ||||||||||||||||||||||||||||||||||||||||
| const openFiles: RecentFile[] = []; | ||||||||||||||||||||||||||||||||||||||||
| const rest: RecentFile[] = []; | ||||||||||||||||||||||||||||||||||||||||
| for (const file of recentlyViewedFiles) { | ||||||||||||||||||||||||||||||||||||||||
| if (openSet.has(file.absolutePath)) { | ||||||||||||||||||||||||||||||||||||||||
| openFiles.push(file); | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| rest.push(file); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return [...openFiles, ...rest].slice(0, RECENT_DISPLAY_LIMIT); | ||||||||||||||||||||||||||||||||||||||||
| }, [showRecentSection, recentlyViewedFiles, openFilePaths]); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const filteredRecent = useMemo<RecentFile[]>(() => { | ||||||||||||||||||||||||||||||||||||||||
| if (!showRecentSection) return []; | ||||||||||||||||||||||||||||||||||||||||
| if (!hasQuery) return orderedRecent; | ||||||||||||||||||||||||||||||||||||||||
| const needle = trimmedQuery.toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||
| return orderedRecent.filter((file) => | ||||||||||||||||||||||||||||||||||||||||
| file.relativePath.toLowerCase().includes(needle), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }, [showRecentSection, hasQuery, trimmedQuery, orderedRecent]); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+80
to
+87
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The second condition —
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const recentAbsSet = useMemo( | ||||||||||||||||||||||||||||||||||||||||
| () => new Set(filteredRecent.map((f) => f.absolutePath)), | ||||||||||||||||||||||||||||||||||||||||
| [filteredRecent], | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const dedupedResults = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||
| if (!showRecentSection) return rawResults; | ||||||||||||||||||||||||||||||||||||||||
| return rawResults.filter((r) => !recentAbsSet.has(r.path)); | ||||||||||||||||||||||||||||||||||||||||
| }, [showRecentSection, rawResults, recentAbsSet]); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const handleOpenChange = useCallback( | ||||||||||||||||||||||||||||||||||||||||
| (nextOpen: boolean) => { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -67,6 +116,12 @@ export function CommandPalette({ | |||||||||||||||||||||||||||||||||||||||
| if (open) requestAnimationFrame(() => inputRef.current?.focus()); | ||||||||||||||||||||||||||||||||||||||||
| }, [open]); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const showHeading = showRecentSection && filteredRecent.length > 0; | ||||||||||||||||||||||||||||||||||||||||
| const showSeparator = | ||||||||||||||||||||||||||||||||||||||||
| showRecentSection && filteredRecent.length > 0 && dedupedResults.length > 0; | ||||||||||||||||||||||||||||||||||||||||
| const showEmptyState = | ||||||||||||||||||||||||||||||||||||||||
| filteredRecent.length === 0 && dedupedResults.length === 0; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <DialogPrimitive.Root open={open} onOpenChange={handleOpenChange} modal> | ||||||||||||||||||||||||||||||||||||||||
| <DialogPrimitive.Portal> | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -129,32 +184,40 @@ export function CommandPalette({ | |||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <CommandPrimitive.List className="max-h-[400px] overflow-x-hidden overflow-y-auto scroll-py-1 p-1"> | ||||||||||||||||||||||||||||||||||||||||
| {results.length === 0 && ( | ||||||||||||||||||||||||||||||||||||||||
| {showEmptyState && ( | ||||||||||||||||||||||||||||||||||||||||
| <CommandPrimitive.Empty className="py-6 text-center text-sm text-muted-foreground"> | ||||||||||||||||||||||||||||||||||||||||
| No files found. | ||||||||||||||||||||||||||||||||||||||||
| </CommandPrimitive.Empty> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| {results.map((file) => ( | ||||||||||||||||||||||||||||||||||||||||
| <CommandPrimitive.Item | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| {showHeading && ( | ||||||||||||||||||||||||||||||||||||||||
| <div className="px-2 pt-2 pb-1 text-muted-foreground text-xs"> | ||||||||||||||||||||||||||||||||||||||||
| Recently Viewed | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| {filteredRecent.map((file) => ( | ||||||||||||||||||||||||||||||||||||||||
| <FileResultItem | ||||||||||||||||||||||||||||||||||||||||
| key={`recent:${file.absolutePath}`} | ||||||||||||||||||||||||||||||||||||||||
| value={`recent:${file.absolutePath}`} | ||||||||||||||||||||||||||||||||||||||||
| fileName={getFileName(file.relativePath)} | ||||||||||||||||||||||||||||||||||||||||
| relativePath={file.relativePath} | ||||||||||||||||||||||||||||||||||||||||
| onSelect={() => handleSelectFile(file.absolutePath)} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| {showSeparator && ( | ||||||||||||||||||||||||||||||||||||||||
| <CommandSeparator alwaysRender className="my-1" /> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| {dedupedResults.map((file) => ( | ||||||||||||||||||||||||||||||||||||||||
| <FileResultItem | ||||||||||||||||||||||||||||||||||||||||
| key={file.id} | ||||||||||||||||||||||||||||||||||||||||
| value={file.path} | ||||||||||||||||||||||||||||||||||||||||
| fileName={file.name} | ||||||||||||||||||||||||||||||||||||||||
| relativePath={file.relativePath} | ||||||||||||||||||||||||||||||||||||||||
| onSelect={() => handleSelectFile(file.path)} | ||||||||||||||||||||||||||||||||||||||||
| className="group data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-2 text-sm outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| <FileIcon | ||||||||||||||||||||||||||||||||||||||||
| fileName={file.name} | ||||||||||||||||||||||||||||||||||||||||
| className="size-3.5 shrink-0" | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| <span className="max-w-[252px] truncate font-medium"> | ||||||||||||||||||||||||||||||||||||||||
| {file.name} | ||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||
| <span className="truncate text-muted-foreground text-xs"> | ||||||||||||||||||||||||||||||||||||||||
| {file.relativePath} | ||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||
| <kbd className="ml-auto hidden shrink-0 text-xs text-muted-foreground group-data-[selected=true]:block"> | ||||||||||||||||||||||||||||||||||||||||
| ↵ | ||||||||||||||||||||||||||||||||||||||||
| </kbd> | ||||||||||||||||||||||||||||||||||||||||
| </CommandPrimitive.Item> | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||
| </CommandPrimitive.List> | ||||||||||||||||||||||||||||||||||||||||
| </CommandPrimitive> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { CommandPrimitive } from "@superset/ui/command"; | ||
| import { FileIcon } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/utils"; | ||
|
|
||
| interface FileResultItemProps { | ||
| value: string; | ||
| fileName: string; | ||
| relativePath: string; | ||
| onSelect: () => void; | ||
|
Comment on lines
+4
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Removing |
||
| } | ||
|
|
||
| export function FileResultItem({ | ||
| value, | ||
| fileName, | ||
| relativePath, | ||
| onSelect, | ||
| }: FileResultItemProps) { | ||
| return ( | ||
| <CommandPrimitive.Item | ||
| value={value} | ||
| onSelect={onSelect} | ||
| className="group data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-2 text-sm outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" | ||
| > | ||
| <FileIcon fileName={fileName} className="size-3.5 shrink-0" /> | ||
| <span className="max-w-[252px] truncate font-medium">{fileName}</span> | ||
| <span className="truncate text-muted-foreground text-xs"> | ||
| {relativePath} | ||
| </span> | ||
| <kbd className="ml-auto hidden shrink-0 text-xs text-muted-foreground group-data-[selected=true]:block"> | ||
| ↵ | ||
| </kbd> | ||
| </CommandPrimitive.Item> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { FileResultItem } from "./FileResultItem"; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
openshadows the component'sopenpropconst open: RecentFile[] = []on line 68 shadows theopen: booleanprop accepted byCommandPalette. While it doesn't cause a runtime bug here (the outeropenisn't needed inside this memo), the shadowing is a footgun — any future edit that needs to read theopenprop inside this memo will silently get the array instead of the boolean.Consider renaming the local variable for clarity: