fix(conversazioni): il menu dei tag mostra una riga per persona, non per alias
Con un cliente «Gian Luca Caruso / Caruso Speaker» il menu proponeva tre voci per un destinatario solo. Non era un difetto di resa: al menu era stata passata la stessa lista che il parser usa per RICONOSCERE un tag rileggendo il testo. Sono due domande diverse, e il codice le aveva confuse: - come lo si puo' scrivere -> tutti gli alias, invisibili, solo in lettura - chi si puo' scegliere -> una riga per destinatario `MentionTarget` le separa: `label` per il menu, `aliases` per la rilettura. Scrivere «@Gian» a mano continua a valere come tag e a far partire la mail -- cambia solo cosa viene offerto, non cosa viene accettato. Il filtro del menu ora passa da `normalizeForSearch`, la stessa normalizzazione del parser: con un toLowerCase() a parte, digitare «nicolo» non avrebbe trovato «Nicolò» nel menu mentre nel messaggio sarebbe stato riconosciuto -- due regole per la stessa domanda, e la seconda si scopre solo quando la mail non parte. Il difetto scalava peggio del valore: con tre contatti per cliente sarebbero diventate nove righe. `mentionTargets()` ritorna gia' un array per questo, ma il tag per singola persona resta impossibile finche' `client_emails` ha gli indirizzi e non i nomi: serve una migration, ed e' un lavoro a se'. Verificato: build e lint puliti, 23 test su parser e menu. Mai visto a schermo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -290,7 +290,7 @@ function ActiveThread({ thread }: { thread: ConversationThread }) {
|
||||
key={m.id}
|
||||
message={m}
|
||||
clientId={thread.clientId}
|
||||
mentionCandidates={thread.mentionCandidates}
|
||||
mentionCandidates={thread.mentionAliases}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -302,7 +302,7 @@ function ActiveThread({ thread }: { thread: ConversationThread }) {
|
||||
channelKey={activeChannel}
|
||||
channelLabel={activeLabel}
|
||||
isGeneral={isGeneral}
|
||||
mentionCandidates={thread.mentionCandidates}
|
||||
mentionTargets={thread.mentionTargets}
|
||||
notifyEmailCount={thread.notifyEmailCount}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AtSign } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { normalizeForSearch, type MentionTarget } from "@/lib/mentions";
|
||||
import { replyToConversation } from "@/app/admin/conversazioni/actions";
|
||||
|
||||
/**
|
||||
@@ -20,15 +21,19 @@ export function ReplyComposer({
|
||||
channelKey,
|
||||
channelLabel,
|
||||
isGeneral,
|
||||
mentionCandidates,
|
||||
mentionTargets,
|
||||
notifyEmailCount,
|
||||
}: {
|
||||
clientId: string;
|
||||
channelKey: string;
|
||||
channelLabel: string;
|
||||
isGeneral: boolean;
|
||||
/** I nomi con cui si può taggare questo cliente, dal più completo al più corto. */
|
||||
mentionCandidates: string[];
|
||||
/**
|
||||
* Chi si può taggare: **una voce per destinatario**, non una per modo di
|
||||
* scriverlo. Prima qui arrivava la lista piatta degli alias e il menu mostrava
|
||||
* tre righe — nome, brand, nome di battesimo — per una persona sola.
|
||||
*/
|
||||
mentionTargets: MentionTarget[];
|
||||
/** Quante mail partirebbero davvero con un tag. Zero = si avvisa, non si tace. */
|
||||
notifyEmailCount: number;
|
||||
}) {
|
||||
@@ -43,10 +48,14 @@ export function ReplyComposer({
|
||||
|
||||
const suggestions = useMemo(() => {
|
||||
if (!trigger) return [];
|
||||
const q = trigger.query.toLowerCase();
|
||||
// Il nome intero per primo: è quello che si vuole quasi sempre.
|
||||
return mentionCandidates.filter((c) => c.toLowerCase().startsWith(q)).slice(0, 4);
|
||||
}, [trigger, mentionCandidates]);
|
||||
const q = normalizeForSearch(trigger.query);
|
||||
// Si filtra sugli alias ma si tiene il TARGET: digitando «car» il cliente si
|
||||
// trova passando dal brand, e resta comunque una riga sola col suo nome.
|
||||
// La normalizzazione è quella del parser, così «nicolo» trova «Nicolò».
|
||||
return mentionTargets
|
||||
.filter((t) => t.aliases.some((a) => normalizeForSearch(a).startsWith(q)))
|
||||
.slice(0, 6);
|
||||
}, [trigger, mentionTargets]);
|
||||
|
||||
const open = suggestions.length > 0;
|
||||
|
||||
@@ -122,8 +131,8 @@ export function ReplyComposer({
|
||||
aria-label="Tagga il cliente"
|
||||
className="absolute bottom-full left-0 z-20 mb-2 w-64 overflow-hidden rounded-lg border border-border bg-card shadow-lg"
|
||||
>
|
||||
{suggestions.map((candidate, i) => (
|
||||
<li key={candidate}>
|
||||
{suggestions.map((target, i) => (
|
||||
<li key={target.label}>
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
@@ -133,7 +142,7 @@ export function ReplyComposer({
|
||||
// nel posto sbagliato.
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
insert(candidate);
|
||||
insert(target.label);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-muted",
|
||||
@@ -141,7 +150,7 @@ export function ReplyComposer({
|
||||
)}
|
||||
>
|
||||
<AtSign className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate font-medium text-foreground">{candidate}</span>
|
||||
<span className="truncate font-medium text-foreground">{target.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
@@ -162,7 +171,7 @@ export function ReplyComposer({
|
||||
onKeyDown={(e) => {
|
||||
if (open && (e.key === "Enter" || e.key === "Tab")) {
|
||||
e.preventDefault();
|
||||
insert(suggestions[0]);
|
||||
insert(suggestions[0].label);
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape" && open) {
|
||||
|
||||
Reference in New Issue
Block a user