feat(editor): enable ticket mentions on new-ticket forms and fix @mention popup layering\n\n- New Ticket page/dialog/portal now support @ to link tickets\n- Mention popup uses fixed strategy + high z-index\n- Add minimal Tippy box styling to globals.css\n- Keeps existing server-side permissions for mentions

This commit is contained in:
codex-bot 2025-10-23 09:48:16 -03:00
parent b0f57009ac
commit 904134604c
5 changed files with 25 additions and 2 deletions

View file

@ -154,6 +154,11 @@
} }
@layer components { @layer components {
/* Tippy.js (menções do editor) */
.tippy-box {
@apply rounded-lg border border-slate-200 bg-white shadow-lg;
}
.tippy-content { @apply p-0; }
/* Tipografia básica para conteúdos rich text (Tiptap) */ /* Tipografia básica para conteúdos rich text (Tiptap) */
.rich-text { .rich-text {
@apply text-foreground; @apply text-foreground;

View file

@ -28,7 +28,7 @@ import { CategorySelectFields } from "@/components/tickets/category-select"
export default function NewTicketPage() { export default function NewTicketPage() {
const router = useRouter() const router = useRouter()
const { convexUserId, isStaff } = useAuth() const { convexUserId, isStaff, role } = useAuth()
const queuesEnabled = Boolean(isStaff && convexUserId) const queuesEnabled = Boolean(isStaff && convexUserId)
const queueArgs = queuesEnabled const queueArgs = queuesEnabled
? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> }
@ -79,6 +79,11 @@ export default function NewTicketPage() {
if (hasChamados) setQueueName("Chamados") if (hasChamados) setQueueName("Chamados")
}, [queueOptions, queueName]) }, [queueOptions, queueName])
const allowTicketMentions = useMemo(() => {
const normalized = (role ?? "").toLowerCase()
return normalized === "admin" || normalized === "agent" || normalized === "collaborator"
}, [role])
async function submit(event: React.FormEvent) { async function submit(event: React.FormEvent) {
event.preventDefault() event.preventDefault()
if (!convexUserId || loading) return if (!convexUserId || loading) return
@ -201,6 +206,7 @@ export default function NewTicketPage() {
} }
}} }}
placeholder="Detalhe o problema, passos para reproduzir, links, etc." placeholder="Detalhe o problema, passos para reproduzir, links, etc."
ticketMention={{ enabled: allowTicketMentions }}
/> />
{descriptionError ? <p className="text-xs font-medium text-red-500">{descriptionError}</p> : null} {descriptionError ? <p className="text-xs font-medium text-red-500">{descriptionError}</p> : null}
</div> </div>

View file

@ -198,6 +198,7 @@ export function PortalTicketForm() {
placeholder="Compartilhe passos para reproduzir, mensagens de erro ou informações adicionais." placeholder="Compartilhe passos para reproduzir, mensagens de erro ou informações adicionais."
className="rounded-2xl border border-slate-200 shadow-sm focus-within:border-neutral-900 focus-within:ring-neutral-900/20" className="rounded-2xl border border-slate-200 shadow-sm focus-within:border-neutral-900 focus-within:ring-neutral-900/20"
disabled={machineInactive || isSubmitting} disabled={machineInactive || isSubmitting}
ticketMention={{ enabled: allowTicketMentions }}
/> />
</div> </div>
</div> </div>
@ -252,3 +253,4 @@ export function PortalTicketForm() {
</Card> </Card>
) )
} }
const allowTicketMentions = true

View file

@ -57,7 +57,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
}, },
mode: "onTouched", mode: "onTouched",
}) })
const { convexUserId, isStaff } = useAuth() const { convexUserId, isStaff, role } = useAuth()
const queuesEnabled = Boolean(isStaff && convexUserId) const queuesEnabled = Boolean(isStaff && convexUserId)
const queueArgs = queuesEnabled const queueArgs = queuesEnabled
? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> }
@ -297,6 +297,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
}) })
} }
placeholder="Detalhe o problema, passos para reproduzir, links, etc." placeholder="Detalhe o problema, passos para reproduzir, links, etc."
ticketMention={{ enabled: allowTicketMentions }}
/> />
<FieldError <FieldError
errors={ errors={
@ -440,3 +441,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
</Dialog> </Dialog>
) )
} }
const allowTicketMentions = useMemo(() => {
const normalized = (role ?? "").toLowerCase()
return normalized === "admin" || normalized === "agent" || normalized === "collaborator"
}, [role])

View file

@ -16,6 +16,9 @@ import Placeholder from "@tiptap/extension-placeholder"
import Mention from "@tiptap/extension-mention" import Mention from "@tiptap/extension-mention"
import { ReactRenderer } from "@tiptap/react" import { ReactRenderer } from "@tiptap/react"
import tippy, { type Instance, type Props as TippyProps } from "tippy.js" import tippy, { type Instance, type Props as TippyProps } from "tippy.js"
// Nota: o CSS do Tippy não é obrigatório, mas melhora muito a renderização
// do popover de sugestões. O componente aplica um z-index alto e estratégia
// "fixed" para evitar problemas de sobreposição/scrolling mesmo sem CSS global.
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import sanitize from "sanitize-html" import sanitize from "sanitize-html"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@ -357,6 +360,8 @@ const TicketMentionExtension = Mention.extend({
interactive: true, interactive: true,
trigger: "manual", trigger: "manual",
placement: "bottom-start", placement: "bottom-start",
zIndex: 99999,
popperOptions: { strategy: "fixed" },
}) })
}, },
onUpdate(props) { onUpdate(props) {