- Updated .env.example to include configuration for OnlyOffice Document Server. - Modified the workspace configuration to remove the drive-suite path. - Adjusted TypeScript environment imports for consistency. - Enhanced Next.js configuration to disable canvas in Webpack. - Updated package.json to include new dependencies for OnlyOffice and PDF.js. - Added global styles for OnlyOffice theme integration in the CSS. - Created new layout and page components for the Drive feature, including public sharing and editing functionalities. - Updated metadata handling across various layouts to reflect the new app structure.
206 lines
6.3 KiB
TypeScript
206 lines
6.3 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type KeyboardEvent,
|
|
} from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import { Search, X } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { MAIL_SEARCH_SUGGESTIONS_DROPDOWN_CLASS } from "@/lib/mail-chrome-classes"
|
|
import { DRIVE_SEARCH_INPUT_WRAP_CLASS } from "@/lib/drive/drive-chrome-classes"
|
|
import { searchMailSettings } from "@/lib/mail-settings/search-settings"
|
|
import type { MailSettingsSearchEntry } from "@/lib/mail-settings/settings-search-index"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export function MailSettingsSearchBar({ className }: { className?: string }) {
|
|
const router = useRouter()
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const [query, setQuery] = useState("")
|
|
const [focused, setFocused] = useState(false)
|
|
const [open, setOpen] = useState(false)
|
|
const [selectedIndex, setSelectedIndex] = useState(-1)
|
|
|
|
const suggestions = useMemo(
|
|
() => searchMailSettings(query),
|
|
[query]
|
|
)
|
|
|
|
const showDropdown =
|
|
open && focused && query.trim().length > 0 && suggestions.length > 0
|
|
|
|
const navigateTo = useCallback(
|
|
(entry: MailSettingsSearchEntry) => {
|
|
router.push(entry.href)
|
|
setOpen(false)
|
|
setQuery("")
|
|
setSelectedIndex(-1)
|
|
inputRef.current?.blur()
|
|
},
|
|
[router]
|
|
)
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
if (!showDropdown && e.key !== "Escape") return
|
|
|
|
switch (e.key) {
|
|
case "ArrowDown":
|
|
e.preventDefault()
|
|
setSelectedIndex((i) =>
|
|
i < suggestions.length - 1 ? i + 1 : 0
|
|
)
|
|
break
|
|
case "ArrowUp":
|
|
e.preventDefault()
|
|
setSelectedIndex((i) =>
|
|
i > 0 ? i - 1 : suggestions.length - 1
|
|
)
|
|
break
|
|
case "Enter":
|
|
e.preventDefault()
|
|
if (selectedIndex >= 0 && suggestions[selectedIndex]) {
|
|
navigateTo(suggestions[selectedIndex])
|
|
} else if (suggestions[0]) {
|
|
navigateTo(suggestions[0])
|
|
}
|
|
break
|
|
case "Escape":
|
|
e.preventDefault()
|
|
setOpen(false)
|
|
setSelectedIndex(-1)
|
|
inputRef.current?.blur()
|
|
break
|
|
}
|
|
},
|
|
[showDropdown, selectedIndex, suggestions, navigateTo]
|
|
)
|
|
|
|
useEffect(() => {
|
|
setSelectedIndex(suggestions.length > 0 ? 0 : -1)
|
|
}, [query, suggestions.length])
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (!containerRef.current?.contains(event.target as Node)) {
|
|
setOpen(false)
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handleClickOutside)
|
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
}, [])
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
data-mail-settings-search
|
|
className={cn(
|
|
"relative flex w-full min-w-0 flex-col overflow-visible",
|
|
className
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
DRIVE_SEARCH_INPUT_WRAP_CLASS,
|
|
"text-[#5f6368] dark:text-[#9aa0a6]",
|
|
focused && "shadow-md ring-1 ring-gray-300 dark:ring-gray-600"
|
|
)}
|
|
>
|
|
<div className="pointer-events-none absolute left-3.5 flex items-center">
|
|
<Search className="size-5 shrink-0" />
|
|
</div>
|
|
<input
|
|
ref={inputRef}
|
|
type="search"
|
|
value={query}
|
|
onChange={(e) => {
|
|
setQuery(e.target.value)
|
|
setOpen(true)
|
|
}}
|
|
onFocus={() => {
|
|
setFocused(true)
|
|
if (query.trim()) setOpen(true)
|
|
}}
|
|
onBlur={() => setFocused(false)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Rechercher dans les réglages"
|
|
className={cn(
|
|
"h-full w-full rounded-full border-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground",
|
|
query ? "pl-11 pr-12" : "pl-11 pr-4"
|
|
)}
|
|
role="combobox"
|
|
aria-expanded={showDropdown}
|
|
aria-controls="mail-settings-search-listbox"
|
|
aria-autocomplete="list"
|
|
autoComplete="off"
|
|
/>
|
|
{query ? (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute right-2 shrink-0 rounded-full text-muted-foreground hover:text-foreground"
|
|
aria-label="Effacer la recherche"
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
onClick={() => {
|
|
setQuery("")
|
|
setOpen(false)
|
|
inputRef.current?.focus()
|
|
}}
|
|
>
|
|
<X className="size-4" />
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
|
|
{showDropdown ? (
|
|
<ul
|
|
id="mail-settings-search-listbox"
|
|
role="listbox"
|
|
className={cn(
|
|
"absolute left-0 right-0 top-[calc(100%+4px)] z-50 overflow-hidden rounded-2xl py-1",
|
|
MAIL_SEARCH_SUGGESTIONS_DROPDOWN_CLASS
|
|
)}
|
|
>
|
|
{suggestions.map((entry, index) => {
|
|
const selected = index === selectedIndex
|
|
return (
|
|
<li key={entry.id} role="presentation">
|
|
<button
|
|
type="button"
|
|
role="option"
|
|
aria-selected={selected}
|
|
className={cn(
|
|
"flex w-full items-start gap-3 px-4 py-2.5 text-left text-sm transition-colors",
|
|
selected
|
|
? "bg-accent text-accent-foreground"
|
|
: "hover:bg-accent/60"
|
|
)}
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
onClick={() => navigateTo(entry)}
|
|
onMouseEnter={() => setSelectedIndex(index)}
|
|
>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate font-medium text-foreground">
|
|
{entry.label}
|
|
</div>
|
|
<div className="truncate text-xs text-muted-foreground">
|
|
{entry.sectionLabel}
|
|
{entry.description ? ` · ${entry.description}` : null}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|