mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
fix: topic selector (#129)
* feat: add jazz globa group cons * chore: remove topic selector atom * chore: use jazz from constant * chore: remove delete model and add new topic selector * chore: use jazz group id form constant in search component * chore: use jazz group id form constant in public home route * fix: topic selector in link * fix: topic section in detail topic * chore: update la editor * chore: content header tweak class * chore: add btn variant to topic selector * refactor: tweak border for link header * chore: page header * fix: page detail route
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Button } from "../ui/button"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
import { useAtom } from "jotai"
|
||||
@@ -16,7 +15,7 @@ export const ContentHeader = React.forwardRef<HTMLDivElement, ContentHeaderProps
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"flex min-h-10 min-w-0 max-w-[100vw] shrink-0 items-center gap-3 pl-8 pr-6 transition-opacity max-lg:pl-4 max-lg:pr-5",
|
||||
"flex min-h-10 min-w-0 max-w-[100vw] shrink-0 items-center gap-3 pl-8 pr-6 transition-opacity max-lg:px-4",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -55,7 +54,6 @@ export const SidebarToggleButton: React.FC = () => {
|
||||
>
|
||||
<PanelLeftIcon size={16} />
|
||||
</Button>
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
interface DeleteModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onConfirm: () => void
|
||||
title: string
|
||||
}
|
||||
|
||||
export default function DeletePageModal({ isOpen, onClose, onConfirm, title }: DeleteModalProps) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete "{title}"?</DialogTitle>
|
||||
<DialogDescription>This action cannot be undone.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" className="bg-red-700" onClick={onConfirm}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
179
web/components/custom/topic-selector.tsx
Normal file
179
web/components/custom/topic-selector.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useMemo, useCallback, useRef, forwardRef } from "react"
|
||||
import { atom, useAtom } from "jotai"
|
||||
import { useVirtualizer } from "@tanstack/react-virtual"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import { Command, CommandInput, CommandList, CommandItem, CommandGroup } from "@/components/ui/command"
|
||||
import { useCoState } from "@/lib/providers/jazz-provider"
|
||||
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
|
||||
import { ListOfTopics, Topic } from "@/lib/schema"
|
||||
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
||||
import { VariantProps } from "class-variance-authority"
|
||||
|
||||
interface TopicSelectorProps extends VariantProps<typeof buttonVariants> {
|
||||
showSearch?: boolean
|
||||
defaultLabel?: string
|
||||
searchPlaceholder?: string
|
||||
value?: string | null
|
||||
onChange?: (value: string) => void
|
||||
onTopicChange?: (value: Topic) => void
|
||||
className?: string
|
||||
renderSelectedText?: (value?: string | null) => React.ReactNode
|
||||
side?: "bottom" | "top" | "right" | "left"
|
||||
align?: "center" | "end" | "start"
|
||||
}
|
||||
|
||||
export const topicSelectorAtom = atom(false)
|
||||
|
||||
export const TopicSelector = forwardRef<HTMLButtonElement, TopicSelectorProps>(
|
||||
(
|
||||
{
|
||||
showSearch = true,
|
||||
defaultLabel = "Select topic",
|
||||
searchPlaceholder = "Search topic...",
|
||||
value,
|
||||
onChange,
|
||||
onTopicChange,
|
||||
className,
|
||||
renderSelectedText,
|
||||
side = "bottom",
|
||||
align = "end",
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [isTopicSelectorOpen, setIsTopicSelectorOpen] = useAtom(topicSelectorAtom)
|
||||
const group = useCoState(PublicGlobalGroup, JAZZ_GLOBAL_GROUP_ID, { root: { topics: [] } })
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(selectedTopicName: string, topic: Topic) => {
|
||||
onChange?.(selectedTopicName)
|
||||
onTopicChange?.(topic)
|
||||
setIsTopicSelectorOpen(false)
|
||||
},
|
||||
[onChange, setIsTopicSelectorOpen, onTopicChange]
|
||||
)
|
||||
|
||||
const displaySelectedText = useMemo(() => {
|
||||
if (renderSelectedText) {
|
||||
return renderSelectedText(value)
|
||||
}
|
||||
return <span className="truncate">{value || defaultLabel}</span>
|
||||
}, [value, defaultLabel, renderSelectedText])
|
||||
|
||||
return (
|
||||
<Popover open={isTopicSelectorOpen} onOpenChange={setIsTopicSelectorOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
size="sm"
|
||||
type="button"
|
||||
role="combobox"
|
||||
variant="secondary"
|
||||
className={cn("gap-x-2 text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{displaySelectedText}
|
||||
<LaIcon name="ChevronDown" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-52 rounded-lg p-0"
|
||||
side={side}
|
||||
align={align}
|
||||
onCloseAutoFocus={e => e.preventDefault()}
|
||||
>
|
||||
{group?.root.topics && (
|
||||
<TopicSelectorContent
|
||||
showSearch={showSearch}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
value={value}
|
||||
onSelect={handleSelect}
|
||||
topics={group.root.topics}
|
||||
/>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
TopicSelector.displayName = "TopicSelector"
|
||||
|
||||
interface TopicSelectorContentProps extends Omit<TopicSelectorProps, "onChange" | "onTopicChange"> {
|
||||
onSelect: (value: string, topic: Topic) => void
|
||||
topics: ListOfTopics
|
||||
}
|
||||
|
||||
const TopicSelectorContent: React.FC<TopicSelectorContentProps> = React.memo(
|
||||
({ showSearch, searchPlaceholder, value, onSelect, topics }) => {
|
||||
const [search, setSearch] = React.useState("")
|
||||
const filteredTopics = useMemo(
|
||||
() => topics.filter(topic => topic?.prettyName.toLowerCase().includes(search.toLowerCase())),
|
||||
[topics, search]
|
||||
)
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: filteredTopics.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 35,
|
||||
overscan: 5
|
||||
})
|
||||
|
||||
return (
|
||||
<Command>
|
||||
{showSearch && (
|
||||
<CommandInput placeholder={searchPlaceholder} className="h-9" value={search} onValueChange={setSearch} />
|
||||
)}
|
||||
<CommandList>
|
||||
<div ref={parentRef} style={{ height: "200px", overflow: "auto" }}>
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: "100%",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<CommandGroup>
|
||||
{rowVirtualizer.getVirtualItems().map(virtualRow => {
|
||||
const topic = filteredTopics[virtualRow.index]
|
||||
return (
|
||||
topic && (
|
||||
<CommandItem
|
||||
key={virtualRow.key}
|
||||
value={topic.name}
|
||||
onSelect={value => onSelect(value, topic)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`
|
||||
}}
|
||||
>
|
||||
<span>{topic.prettyName}</span>
|
||||
<LaIcon
|
||||
name="Check"
|
||||
className={cn("absolute right-3", topic.name === value ? "text-primary" : "text-transparent")}
|
||||
/>
|
||||
</CommandItem>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
</div>
|
||||
</div>
|
||||
</CommandList>
|
||||
</Command>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
TopicSelectorContent.displayName = "TopicSelectorContent"
|
||||
|
||||
export default TopicSelector
|
||||
Reference in New Issue
Block a user