feat: refine ticket header save flow
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
01b7103200
commit
cebe1b9bf1
9 changed files with 616 additions and 587 deletions
|
|
@ -8,8 +8,11 @@ import { requireAdmin, requireStaff } from "./rbac";
|
|||
const QUEUE_RENAME_LOOKUP: Record<string, string> = {
|
||||
"Suporte N1": "Chamados",
|
||||
"suporte-n1": "Chamados",
|
||||
chamados: "Chamados",
|
||||
"Suporte N2": "Laboratório",
|
||||
"suporte-n2": "Laboratório",
|
||||
laboratorio: "Laboratório",
|
||||
Laboratorio: "Laboratório",
|
||||
};
|
||||
|
||||
function renameQueueString(value: string) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ import { requireCustomer, requireStaff, requireUser } from "./rbac";
|
|||
const QUEUE_RENAME_LOOKUP: Record<string, string> = {
|
||||
"Suporte N1": "Chamados",
|
||||
"suporte-n1": "Chamados",
|
||||
chamados: "Chamados",
|
||||
"Suporte N2": "Laboratório",
|
||||
"suporte-n2": "Laboratório",
|
||||
laboratorio: "Laboratório",
|
||||
Laboratorio: "Laboratório",
|
||||
};
|
||||
|
||||
function renameQueueString(value?: string | null): string | null {
|
||||
|
|
@ -770,8 +773,8 @@ export const changeQueue = mutation({
|
|||
export const updateCategories = mutation({
|
||||
args: {
|
||||
ticketId: v.id("tickets"),
|
||||
categoryId: v.id("ticketCategories"),
|
||||
subcategoryId: v.id("ticketSubcategories"),
|
||||
categoryId: v.union(v.id("ticketCategories"), v.null()),
|
||||
subcategoryId: v.union(v.id("ticketSubcategories"), v.null()),
|
||||
actorId: v.id("users"),
|
||||
},
|
||||
handler: async (ctx, { ticketId, categoryId, subcategoryId, actorId }) => {
|
||||
|
|
@ -780,23 +783,60 @@ export const updateCategories = mutation({
|
|||
throw new ConvexError("Ticket não encontrado")
|
||||
}
|
||||
await requireStaff(ctx, actorId, ticket.tenantId)
|
||||
|
||||
if (categoryId === null) {
|
||||
if (subcategoryId !== null) {
|
||||
throw new ConvexError("Subcategoria inválida")
|
||||
}
|
||||
if (!ticket.categoryId && !ticket.subcategoryId) {
|
||||
return { status: "unchanged" }
|
||||
}
|
||||
const now = Date.now()
|
||||
await ctx.db.patch(ticketId, {
|
||||
categoryId: undefined,
|
||||
subcategoryId: undefined,
|
||||
updatedAt: now,
|
||||
})
|
||||
const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
|
||||
await ctx.db.insert("ticketEvents", {
|
||||
ticketId,
|
||||
type: "CATEGORY_CHANGED",
|
||||
payload: {
|
||||
categoryId: null,
|
||||
categoryName: null,
|
||||
subcategoryId: null,
|
||||
subcategoryName: null,
|
||||
actorId,
|
||||
actorName: actor?.name,
|
||||
actorAvatar: actor?.avatarUrl,
|
||||
},
|
||||
createdAt: now,
|
||||
})
|
||||
return { status: "cleared" }
|
||||
}
|
||||
|
||||
const category = await ctx.db.get(categoryId)
|
||||
if (!category || category.tenantId !== ticket.tenantId) {
|
||||
throw new ConvexError("Categoria inválida")
|
||||
}
|
||||
const subcategory = await ctx.db.get(subcategoryId)
|
||||
if (!subcategory || subcategory.categoryId !== categoryId || subcategory.tenantId !== ticket.tenantId) {
|
||||
throw new ConvexError("Subcategoria inválida")
|
||||
|
||||
let subcategoryName: string | null = null
|
||||
if (subcategoryId !== null) {
|
||||
const subcategory = await ctx.db.get(subcategoryId)
|
||||
if (!subcategory || subcategory.categoryId !== categoryId || subcategory.tenantId !== ticket.tenantId) {
|
||||
throw new ConvexError("Subcategoria inválida")
|
||||
}
|
||||
subcategoryName = subcategory.name
|
||||
}
|
||||
|
||||
if (ticket.categoryId === categoryId && ticket.subcategoryId === subcategoryId) {
|
||||
if (ticket.categoryId === categoryId && (ticket.subcategoryId ?? null) === subcategoryId) {
|
||||
return { status: "unchanged" }
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
await ctx.db.patch(ticketId, {
|
||||
categoryId,
|
||||
subcategoryId,
|
||||
subcategoryId: subcategoryId ?? undefined,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
|
|
@ -808,7 +848,7 @@ export const updateCategories = mutation({
|
|||
categoryId,
|
||||
categoryName: category.name,
|
||||
subcategoryId,
|
||||
subcategoryName: subcategory.name,
|
||||
subcategoryName,
|
||||
actorId,
|
||||
actorName: actor?.name,
|
||||
actorAvatar: actor?.avatarUrl,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
|
|
@ -46,10 +46,10 @@
|
|||
"convex": "^1.27.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "15.5.3",
|
||||
"next": "15.5.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "^7.64.0",
|
||||
"recharts": "^2.15.4",
|
||||
"sanitize-html": "^2.17.0",
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
"@types/react-dom": "^19",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.3",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"prisma": "^6.16.2",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
|
|
|
|||
912
web/pnpm-lock.yaml
generated
912
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -54,7 +54,7 @@ type NavigationGroup = {
|
|||
}
|
||||
|
||||
const navigation: { versions: string[]; navMain: NavigationGroup[] } = {
|
||||
versions: ["0.0.1"],
|
||||
versions: ["Rever Tecnologia"],
|
||||
navMain: [
|
||||
{
|
||||
title: "Operação",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { format, formatDistanceToNow } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
import { IconClock, IconPlayerPause, IconPlayerPlay } from "@tabler/icons-react"
|
||||
|
|
@ -41,6 +41,9 @@ const sectionValueClass = "font-medium text-neutral-900"
|
|||
const subtleBadgeClass =
|
||||
"inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-2.5 py-0.5 text-[11px] font-medium text-neutral-600"
|
||||
|
||||
const EMPTY_CATEGORY_VALUE = "__none__"
|
||||
const EMPTY_SUBCATEGORY_VALUE = "__none__"
|
||||
|
||||
function formatDuration(durationMs: number) {
|
||||
if (durationMs <= 0) return "0s"
|
||||
const totalSeconds = Math.floor(durationMs / 1000)
|
||||
|
|
@ -91,21 +94,26 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const [editing, setEditing] = useState(false)
|
||||
const [subject, setSubject] = useState(ticket.subject)
|
||||
const [summary, setSummary] = useState(ticket.summary ?? "")
|
||||
const [categorySelection, setCategorySelection] = useState({
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
subcategoryId: ticket.subcategory?.id ?? "",
|
||||
})
|
||||
const [savingCategory, setSavingCategory] = useState(false)
|
||||
const lastSubmittedCategoryRef = useRef({
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
subcategoryId: ticket.subcategory?.id ?? "",
|
||||
})
|
||||
const [categorySelection, setCategorySelection] = useState<{ categoryId: string; subcategoryId: string }>(
|
||||
{
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
subcategoryId: ticket.subcategory?.id ?? "",
|
||||
}
|
||||
)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const selectedCategoryId = categorySelection.categoryId
|
||||
const selectedSubcategoryId = categorySelection.subcategoryId
|
||||
const dirty = useMemo(
|
||||
() => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""),
|
||||
[subject, summary, ticket.subject, ticket.summary]
|
||||
)
|
||||
const currentCategoryId = ticket.category?.id ?? ""
|
||||
const currentSubcategoryId = ticket.subcategory?.id ?? ""
|
||||
const categoryDirty = useMemo(() => {
|
||||
return selectedCategoryId !== currentCategoryId || selectedSubcategoryId !== currentSubcategoryId
|
||||
}, [selectedCategoryId, selectedSubcategoryId, currentCategoryId, currentSubcategoryId])
|
||||
const formDirty = dirty || categoryDirty
|
||||
|
||||
const activeCategory = useMemo(
|
||||
() => categories.find((category) => category.id === selectedCategoryId) ?? null,
|
||||
[categories, selectedCategoryId]
|
||||
|
|
@ -113,56 +121,81 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const secondaryOptions = useMemo(() => activeCategory?.secondary ?? [], [activeCategory])
|
||||
|
||||
async function handleSave() {
|
||||
if (!convexUserId) return
|
||||
toast.loading("Salvando alterações...", { id: "save-header" })
|
||||
if (!convexUserId || !formDirty) {
|
||||
setEditing(false)
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
|
||||
try {
|
||||
if (subject !== ticket.subject) {
|
||||
await updateSubject({ ticketId: ticket.id as Id<"tickets">, subject: subject.trim(), actorId: convexUserId as Id<"users"> })
|
||||
if (categoryDirty) {
|
||||
toast.loading("Atualizando categoria...", { id: "ticket-category" })
|
||||
try {
|
||||
await updateCategories({
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
categoryId: selectedCategoryId ? (selectedCategoryId as Id<"ticketCategories">) : null,
|
||||
subcategoryId: selectedSubcategoryId ? (selectedSubcategoryId as Id<"ticketSubcategories">) : null,
|
||||
actorId: convexUserId as Id<"users">,
|
||||
})
|
||||
toast.success("Categoria atualizada!", { id: "ticket-category" })
|
||||
} catch (categoryError) {
|
||||
toast.error("Não foi possível atualizar a categoria.", { id: "ticket-category" })
|
||||
setCategorySelection({
|
||||
categoryId: currentCategoryId,
|
||||
subcategoryId: currentSubcategoryId,
|
||||
})
|
||||
throw categoryError
|
||||
}
|
||||
}
|
||||
if ((summary ?? "") !== (ticket.summary ?? "")) {
|
||||
await updateSummary({ ticketId: ticket.id as Id<"tickets">, summary: (summary ?? "").trim(), actorId: convexUserId as Id<"users"> })
|
||||
|
||||
if (dirty) {
|
||||
toast.loading("Salvando alterações...", { id: "save-header" })
|
||||
if (subject !== ticket.subject) {
|
||||
await updateSubject({
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
subject: subject.trim(),
|
||||
actorId: convexUserId as Id<"users">,
|
||||
})
|
||||
}
|
||||
if ((summary ?? "") !== (ticket.summary ?? "")) {
|
||||
await updateSummary({
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
summary: (summary ?? "").trim(),
|
||||
actorId: convexUserId as Id<"users">,
|
||||
})
|
||||
}
|
||||
toast.success("Cabeçalho atualizado!", { id: "save-header" })
|
||||
}
|
||||
toast.success("Cabeçalho atualizado!", { id: "save-header" })
|
||||
setEditing(false)
|
||||
} catch {
|
||||
toast.error("Não foi possível salvar.", { id: "save-header" })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setSubject(ticket.subject)
|
||||
setSummary(ticket.summary ?? "")
|
||||
setCategorySelection({
|
||||
categoryId: currentCategoryId,
|
||||
subcategoryId: currentSubcategoryId,
|
||||
})
|
||||
setEditing(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const nextSelection = {
|
||||
if (editing) return
|
||||
setCategorySelection({
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
subcategoryId: ticket.subcategory?.id ?? "",
|
||||
}
|
||||
setCategorySelection(nextSelection)
|
||||
lastSubmittedCategoryRef.current = nextSelection
|
||||
}, [ticket.category?.id, ticket.subcategory?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing) return
|
||||
if (categoriesLoading) return
|
||||
if (categories.length === 0) return
|
||||
if (selectedCategoryId) return
|
||||
if (ticket.category?.id) return
|
||||
|
||||
const first = categories[0]
|
||||
const firstSecondary = first.secondary[0]
|
||||
setCategorySelection({
|
||||
categoryId: first.id,
|
||||
subcategoryId: firstSecondary?.id ?? "",
|
||||
})
|
||||
}, [categories, categoriesLoading, editing, selectedCategoryId, ticket.category?.id])
|
||||
}, [editing, ticket.category?.id, ticket.subcategory?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing) return
|
||||
if (!selectedCategoryId) return
|
||||
if (secondaryOptions.length === 0) {
|
||||
if (!selectedCategoryId) {
|
||||
if (selectedSubcategoryId) {
|
||||
setCategorySelection((prev) => ({ ...prev, subcategoryId: "" }))
|
||||
}
|
||||
|
|
@ -170,73 +203,11 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
}
|
||||
|
||||
const stillValid = secondaryOptions.some((option) => option.id === selectedSubcategoryId)
|
||||
if (stillValid) return
|
||||
|
||||
const fallback = secondaryOptions[0]
|
||||
if (fallback) {
|
||||
setCategorySelection((prev) => ({ ...prev, subcategoryId: fallback.id }))
|
||||
if (!stillValid && selectedSubcategoryId) {
|
||||
setCategorySelection((prev) => ({ ...prev, subcategoryId: "" }))
|
||||
}
|
||||
}, [editing, secondaryOptions, selectedCategoryId, selectedSubcategoryId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing) return
|
||||
if (!convexUserId) return
|
||||
const categoryId = selectedCategoryId
|
||||
const subcategoryId = selectedSubcategoryId
|
||||
if (!categoryId || !subcategoryId) return
|
||||
|
||||
const currentCategory = ticket.category?.id ?? ""
|
||||
const currentSubcategory = ticket.subcategory?.id ?? ""
|
||||
|
||||
if (categoryId === currentCategory && subcategoryId === currentSubcategory) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
categoryId === lastSubmittedCategoryRef.current.categoryId &&
|
||||
subcategoryId === lastSubmittedCategoryRef.current.subcategoryId
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
lastSubmittedCategoryRef.current = { categoryId, subcategoryId }
|
||||
setSavingCategory(true)
|
||||
toast.loading("Atualizando categoria...", { id: "ticket-category" })
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
await updateCategories({
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
categoryId: categoryId as Id<"ticketCategories">,
|
||||
subcategoryId: subcategoryId as Id<"ticketSubcategories">,
|
||||
actorId: convexUserId as Id<"users">,
|
||||
})
|
||||
if (!cancelled) {
|
||||
toast.success("Categoria atualizada!", { id: "ticket-category" })
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
toast.error("Não foi possível atualizar a categoria.", { id: "ticket-category" })
|
||||
const fallback = {
|
||||
categoryId: currentCategory,
|
||||
subcategoryId: currentSubcategory,
|
||||
}
|
||||
setCategorySelection(fallback)
|
||||
lastSubmittedCategoryRef.current = fallback
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setSavingCategory(false)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [editing, selectedCategoryId, selectedSubcategoryId, ticket.category?.id, ticket.subcategory?.id, ticket.id, updateCategories, convexUserId])
|
||||
|
||||
const workSummary = useMemo(() => {
|
||||
if (workSummaryRemote !== undefined) return workSummaryRemote ?? null
|
||||
if (!ticket.workSummary) return null
|
||||
|
|
@ -362,20 +333,25 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
<span className={sectionLabelClass}>Categoria primária</span>
|
||||
{editing ? (
|
||||
<Select
|
||||
disabled={savingCategory || categoriesLoading || categories.length === 0}
|
||||
value={selectedCategoryId || ""}
|
||||
disabled={saving || categoriesLoading}
|
||||
value={selectedCategoryId ? selectedCategoryId : EMPTY_CATEGORY_VALUE}
|
||||
onValueChange={(value) => {
|
||||
if (value === EMPTY_CATEGORY_VALUE) {
|
||||
setCategorySelection({ categoryId: "", subcategoryId: "" })
|
||||
return
|
||||
}
|
||||
const category = categories.find((item) => item.id === value)
|
||||
setCategorySelection({
|
||||
categoryId: value,
|
||||
subcategoryId: category?.secondary[0]?.id ?? "",
|
||||
subcategoryId: category?.secondary.find((option) => option.id === selectedSubcategoryId)?.id ?? "",
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={selectTriggerClass}>
|
||||
<SelectValue placeholder={categoriesLoading ? "Carregando..." : "Selecionar"} />
|
||||
<SelectValue placeholder={categoriesLoading ? "Carregando..." : "Sem categoria"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-lg border border-slate-200 bg-white text-neutral-800 shadow-sm">
|
||||
<SelectItem value={EMPTY_CATEGORY_VALUE}>Sem categoria</SelectItem>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
|
|
@ -392,10 +368,14 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
{editing ? (
|
||||
<Select
|
||||
disabled={
|
||||
savingCategory || categoriesLoading || !selectedCategoryId || secondaryOptions.length === 0
|
||||
saving || categoriesLoading || !selectedCategoryId
|
||||
}
|
||||
value={selectedSubcategoryId || ""}
|
||||
value={selectedSubcategoryId ? selectedSubcategoryId : EMPTY_SUBCATEGORY_VALUE}
|
||||
onValueChange={(value) => {
|
||||
if (value === EMPTY_SUBCATEGORY_VALUE) {
|
||||
setCategorySelection((prev) => ({ ...prev, subcategoryId: "" }))
|
||||
return
|
||||
}
|
||||
setCategorySelection((prev) => ({ ...prev, subcategoryId: value }))
|
||||
}}
|
||||
>
|
||||
|
|
@ -405,12 +385,13 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
!selectedCategoryId
|
||||
? "Selecione uma primária"
|
||||
: secondaryOptions.length === 0
|
||||
? "Sem secundárias"
|
||||
? "Sem subcategoria"
|
||||
: "Selecionar"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-lg border border-slate-200 bg-white text-neutral-800 shadow-sm">
|
||||
<SelectItem value={EMPTY_SUBCATEGORY_VALUE}>Sem subcategoria</SelectItem>
|
||||
{secondaryOptions.map((option) => (
|
||||
<SelectItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
|
|
@ -518,7 +499,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
<Button variant="ghost" size="sm" className="text-sm font-semibold text-neutral-700" onClick={handleCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button size="sm" className={startButtonClass} onClick={handleSave} disabled={!dirty}>
|
||||
<Button size="sm" className={startButtonClass} onClick={handleSave} disabled={!formDirty || saving}>
|
||||
Salvar
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -156,10 +156,14 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
|||
if (entry.type === "WORK_PAUSED" && typeof payload.sessionDurationMs === "number") {
|
||||
message = `Tempo registrado: ${formatDuration(payload.sessionDurationMs)}`
|
||||
}
|
||||
if (entry.type === "CATEGORY_CHANGED" && (payload.categoryName || payload.subcategoryName)) {
|
||||
message = `Categoria alterada para ${payload.categoryName ?? ""}${
|
||||
payload.subcategoryName ? ` • ${payload.subcategoryName}` : ""
|
||||
}`
|
||||
if (entry.type === "CATEGORY_CHANGED") {
|
||||
if (payload.categoryName || payload.subcategoryName) {
|
||||
message = `Categoria alterada para ${payload.categoryName ?? ""}${
|
||||
payload.subcategoryName ? ` • ${payload.subcategoryName}` : ""
|
||||
}`
|
||||
} else {
|
||||
message = "Categoria removida"
|
||||
}
|
||||
}
|
||||
if (!message) return null
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ function SidebarProvider({
|
|||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
suppressHydrationWarning
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function VersionSwitcher({
|
|||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none">
|
||||
<span className="font-medium">{label}</span>
|
||||
<span className="text-xs text-muted-foreground">v{selectedVersion}</span>
|
||||
<span className="text-xs text-muted-foreground">{selectedVersion}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
|
|
@ -56,7 +56,7 @@ export function VersionSwitcher({
|
|||
key={version}
|
||||
onSelect={() => setSelectedVersion(version)}
|
||||
>
|
||||
v{version} {version === selectedVersion && <Check className="ml-auto" />}
|
||||
{version} {version === selectedVersion && <Check className="ml-auto" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue