useCommandPalette
Panoramica
useCommandPalette espone lo state e le API della command palette. Lo state è un singleton a livello di modulo: tutte le chiamate condividono isOpen, searchQuery, il registro dei comandi, le fonti di risultati e il binding Ctrl+K (Cmd+K su macOS).
Il binding viene installato alla prima chiamata come scorciatoia di useShortcuts, in uno scope separato che sopravvive ai componenti: funziona anche con il focus in un campo di testo e rispetta la configurazione globale delle scorciatoie (con enabled: false si disattiva anche Ctrl+K).
I comandi passati in initialCommands e le fonti registrate con registerSource vengono rimossi automaticamente quando lo scope del chiamante viene distrutto (tipicamente allo smontaggio del componente).
Firma
function useCommandPalette(
initialCommands?: Command[],
options?: UseCommandPaletteOptions
): UseCommandPaletteReturn
Parametri
| Parametro | Tipo | Default | Descrizione |
|---|---|---|---|
initialCommands | Command[] | [] | Comandi da registrare per lo scope corrente. Vengono rimossi al dispose |
options | UseCommandPaletteOptions | {} | Callback di errore e tempi delle fonti asincrone |
Command
interface Command {
id: string
label: string
description?: string
category?: string
icon?: string
keywords?: string[]
shortcut?: string
handler: (context?: CommandHandlerContext) => void | Promise<void>
enabled?: boolean
}
interface CommandHandlerContext {
signal: AbortSignal
}
| Campo | Tipo | Descrizione |
|---|---|---|
id | string | Identificatore univoco. Una seconda registrazione con lo stesso id viene ignorata (avviso in console in sviluppo) |
label | string | Etichetta principale della riga |
description | string | Testo secondario sotto l'etichetta |
category | string | Gruppo del comando: header di sezione senza query, testo a destra della riga durante la ricerca |
icon | string | Classe di un icon font (pi pi-home) oppure un glifo testuale (emoji o carattere) |
keywords | string[] | Parole non visualizzate che partecipano alla ricerca |
shortcut | string | Combinazione mostrata come badge nella riga (es. Alt+H). Solo informativa: non registra la scorciatoia |
handler | (context?) => void | Promise<void> | Eseguito alla scelta. Riceve { signal }, abortito se la palette si chiude durante l'esecuzione: si può passare a fetch |
enabled | boolean | Se false, il comando è ignorato senza essere rimosso: non compare tra i risultati, con o senza query, ma resta in allCommands |
Un comando senza id, label o handler validi viene scartato.
UseCommandPaletteOptions
| Opzione | Tipo | Default | Descrizione |
|---|---|---|---|
onCommandError | (error, command) => void | — | Invocata quando l'handler di un comando lancia o rifiuta. Senza, l'errore va in console.error |
onRenderError | (error) => void | — | Invocata quando CommandPalette intercetta un errore di rendering di un comando malformato, per inoltrarlo al monitoring |
sourceTimeout | number | 500 | Millisecondi entro cui una fonte deve rispondere |
sourceDebounce | number | 150 | Millisecondi dall'ultima modifica della query prima di interrogare le fonti |
Le opzioni sono condivise come il resto dello state: vale quella dell'ultima chiamata che la passa, e il valore precedente viene ripristinato quando il suo scope viene distrutto.
Return
| Proprietà | Tipo | Descrizione |
|---|---|---|
isOpen | Ref<boolean> | Stato di apertura della palette |
searchQuery | Ref<string> | Testo della ricerca, collegato all'input di CommandPalette |
filteredCommands | ComputedRef<Command[]> | Comandi che corrispondono alla query, seguiti dai risultati delle fonti |
sourceResults | ComputedRef<Command[]> | Solo i risultati delle fonti per la query corrente, raggruppati per category |
isLoadingSources | ComputedRef<boolean> | true mentre ci sono fonti interrogate (o in attesa del debounce) che non hanno risposto |
allCommands | Readonly<Ref<Command[]>> | Registro completo dei comandi |
isExecuting | Readonly<Ref<boolean>> | true mentre l'handler di un comando è in esecuzione |
lastError | Ref<unknown> | Ultimo errore di esecuzione, null se nessuno |
toggle | () => void | Apre o chiude la palette |
open | () => void | Apre la palette, azzerando query ed errore |
close | () => void | Chiude la palette, azzera la query e abortisce ricerche e handler in corso |
clearSearch | () => void | Svuota la query senza chiudere |
executeCommand | (command: Command) => Promise<void> | Esegue l'handler del comando |
addCommand | (command: Command) => void | Aggiunge un comando. Ignorato se l'id esiste già |
removeCommand | (id: string) => void | Rimuove il comando con quell'id |
registerSource | (source: CommandSource) => void | Aggiunge una fonte di risultati. Un id già presente viene sostituito |
unregisterSource | (id: string) => void | Toglie la fonte con quell'id, con i suoi risultati |
Il return type è UseCommandPaletteReturn.
Ricerca
filteredCommands confronta la query, senza spazi iniziali e finali e senza distinzione tra maiuscole e minuscole, con:
command.labelcommand.description- ogni elemento di
command.keywords
Il match è per sottostringa (includes). Con la query vuota filteredCommands contiene tutti i comandi abilitati del registro: quelli con enabled: false sono sempre esclusi. Fino alla 1.2.0 lo erano solo durante una ricerca, e all'apertura della palette comparivano ed erano eseguibili.
Se la query comincia con il prefisso di una fonte (vedi Prefisso), i comandi registrati vengono nascosti.
Esecuzione
executeCommand è invocato da CommandPalette con Enter o con il click, ma si può chiamare anche dal codice:
- mentre un comando è in esecuzione (
isExecuting) le altre invocazioni sono ignorate, così un doppioEnternon lo lancia due volte - l'handler riceve
{ signal }, abortito daclose(): un handler che fa I/O può interrompersi se l'utente chiude la palette - se l'handler termina senza errori, la palette si chiude
- se l'handler lancia o rifiuta, la palette resta aperta: l'errore va in
lastError,CommandPalettemostra un messaggio, e l'errore è passato aonCommandError(o aconsole.errorse non c'è). Una nuova ricerca o la riapertura azzeranolastError
Fonti di risultati asincrone
Una fonte fornisce comandi calcolati sulla query, per ciò che non si può elencare in anticipo: articoli, clienti, documenti. Ha la forma di MFEPaletteSource di @pzeta/mfe-contracts, con Command (e quindi handler) al posto di MFECommand.
interface CommandSource {
id: string
prefix?: string
search(query: string, signal: AbortSignal): Promise<Command[]>
}
| Campo | Descrizione |
|---|---|
id | Identificatore univoco. Una registerSource con un id già registrato sostituisce la fonte, nella stessa posizione |
prefix | Se presente, la fonte risponde solo alle query che cominciano così e le riceve senza prefisso |
search | Restituisce i comandi per la query. signal viene abortito quando la query è superata o scade il tempo |
Ciclo di una ricerca
- L'utente scrive: i comandi registrati che corrispondono compaiono subito, perché restano sincroni
- Le fonti da interrogare passano «in attesa» (
isLoadingSources) eCommandPalettemostra «Ricerca in corso...» - Trascorso
sourceDebouncedall'ultimo tasto, ogni fonte ricevesearch(query, signal) - I risultati di una fonte compaiono appena arrivano, senza attendere le altre, dopo i comandi registrati e raggruppati per
category(prima quelli senza categoria)
Le fonti senza prefisso non vengono interrogate con la query vuota: la palette mostra già tutti i comandi.
Annullamento e scadenza
- Query superata: a ogni modifica della query e alla chiusura della palette, il
signaldella ricerca precedente viene abortito e i suoi risultati, anche se arrivano dopo, vengono ignorati. Passaresignalafetchinterrompe la richiesta HTTP - Scadenza: una fonte che non risponde entro
sourceTimeoutriceve l'abort e non contribuisce più a quella query - Errori: una fonte che rifiuta o lancia non contribuisce (avviso in console in sviluppo). In nessuno di questi casi vengono bloccate le altre fonti o i comandi registrati
I risultati vengono validati: un esito che non è un array, un comando malformato o con enabled: false sono scartati. Un risultato con lo stesso id di un comando già presente nella lista viene scartato, perché duplicherebbe la riga.
Prefisso
Con prefix: '@' la fonte risponde a @rossi e riceve rossi (spazi dopo il prefisso compresi: @ rossi dà lo stesso risultato). Il solo prefisso (@) interroga la fonte con la query vuota, per esempio per mostrare elementi recenti.
Con un prefisso attivo l'utente ha scelto dove cercare: vengono interrogate solo le fonti con quel prefisso, e i comandi registrati e le fonti senza prefisso sono nascosti. Se due fonti dichiarano prefissi annidati ($ e $$), vince il più lungo.
Esempio
<script setup lang="ts">
import { useCommandPalette } from '@pzeta/vue-scorciatoie'
const { registerSource } = useCommandPalette([], { sourceTimeout: 800 })
// Risponde a ogni query non vuota
registerSource({
id: 'knowledge',
search: async (query, signal) => {
const res = await fetch(`/api/articoli?q=${encodeURIComponent(query)}`, { signal })
const articoli: { id: number; titolo: string }[] = await res.json()
return articoli.map((a) => ({
id: `articolo-${a.id}`,
label: a.titolo,
category: 'Knowledge',
icon: 'pi pi-book',
handler: () => router.push(`/knowledge/${a.id}`),
}))
},
})
// Risponde solo a "@query"
registerSource({
id: 'clienti',
prefix: '@',
search: async (query, signal) => {
const res = await fetch(`/api/clienti?q=${encodeURIComponent(query)}`, { signal })
const clienti: { id: number; nome: string }[] = await res.json()
return clienti.map((c) => ({
id: `cliente-${c.id}`,
label: c.nome,
category: 'Clienti',
handler: () => router.push(`/clienti/${c.id}`),
}))
},
})
</script>
Gli id dei risultati devono essere unici in tutta la palette: conviene prefissarli con la fonte, come sopra.
Esempio: registrazione di base
<script setup lang="ts">
import { CommandPalette, useCommandPalette } from '@pzeta/vue-scorciatoie'
useCommandPalette([
{
id: 'nav-dashboard',
label: 'Dashboard',
icon: 'pi pi-home',
shortcut: 'Alt+H',
handler: () => router.push('/'),
},
{
id: 'nav-anagrafica',
label: 'Anagrafiche',
category: 'Navigazione',
keywords: ['clienti', 'fornitori'],
handler: () => router.push('/anagrafica'),
},
])
</script>
<template>
<CommandPalette />
</template>
Esempio: errori degli handler
import { useCommandPalette } from '@pzeta/vue-scorciatoie'
useCommandPalette(
[
{
id: 'esporta',
label: 'Esporta ordini',
handler: async (context) => {
const res = await fetch('/api/ordini/export', { method: 'POST', signal: context?.signal })
if (!res.ok) throw new Error(`Export non riuscito: ${res.status}`)
},
},
],
{
// La palette resta aperta con il messaggio; qui si notifica e si traccia
onCommandError: (error, command) => toast.error(`${command.label}: ${String(error)}`),
}
)
Esempio: apertura programmatica
import { watch } from 'vue'
import { useCommandPalette } from '@pzeta/vue-scorciatoie'
const { open, toggle, isOpen } = useCommandPalette()
// Da un pulsante o da un evento esterno
open()
toggle()
watch(isOpen, (aperta) => {
console.log(aperta ? 'palette aperta' : 'palette chiusa')
})
Esempio: comandi dinamici
const { addCommand, removeCommand } = useCommandPalette()
watch(permessi, (perms) => {
if (perms.includes('admin.users')) {
addCommand({
id: 'admin-users',
label: 'Gestione utenti',
category: 'Admin',
handler: () => router.push('/admin/users'),
})
} else {
removeCommand('admin-users')
}
})
I comandi aggiunti con addCommand vengono rimossi anch'essi al dispose dello scope che ha chiamato useCommandPalette.
Test
Lo state è a livello di modulo e le funzioni di reset usate dai test della libreria non sono esportate dal pacchetto. Nei test di un'applicazione si ottiene uno stato pulito smontando i componenti (comandi e fonti vengono rimossi al dispose dello scope) e chiudendo la palette con close().
Note
openetoggleazzerano sempre la query, e la chiusura la svuota: la ricerca non sopravvive alla palettesearchQueryè unRefscrivibile: si può impostare dall'esterno a palette aperta, e la ricerca sulle fonti riparte- In un microfrontend non si chiama
useCommandPalette, che legherebbe un secondoCtrl+Kin conflitto con la shell: il modulo registra comandi e fonti concontext.palettedi@pzeta/mfe-contracts(dalla 3.1.0), e la shell, proprietaria della palette, li porta in queste API