lib/command-palette/registry/ActionRegistry.ts
Rama main · réplica de lectura del repositorio original.
/**
* ActionRegistry - Central registry for contextual actions.
*
* The registry manages ActionProviders that supply actions for different
* entity types. When the user presses → on an item, the registry retrieves
* all applicable actions from registered providers.
*
* Each <CommandPalette> builds its own instance from the `providers` prop
* (plus the built-in item-actions provider) — no global singleton.
*/
import type { PaletteItem } from "../types";
import {
sortActions,
type ActionExecutionContext,
type ActionProvider,
type ActionResult,
type ContextualAction,
} from "./types";
// =============================================================================
// ACTION REGISTRY CLASS
// =============================================================================
export class ActionRegistry {
/** Registered providers by ID. */
private providers: Map<string, ActionProvider> = new Map();
/** Cache for provider lookup by entity type. */
private typeToProviders: Map<string, ActionProvider[]> = new Map();
// ===========================================================================
// PROVIDER MANAGEMENT
// ===========================================================================
/**
* Register an action provider.
* Providers are checked in priority order (highest first).
*/
register(provider: ActionProvider): void {
this.providers.set(provider.id, provider);
this.typeToProviders.clear();
}
/** Unregister a provider by ID. */
unregister(providerId: string): void {
if (this.providers.delete(providerId)) {
this.typeToProviders.clear();
}
}
hasProvider(providerId: string): boolean {
return this.providers.has(providerId);
}
getProviderIds(): string[] {
return Array.from(this.providers.keys());
}
/** Clear all registered providers (mainly for testing). */
clear(): void {
this.providers.clear();
this.typeToProviders.clear();
}
// ===========================================================================
// ACTION RETRIEVAL
// ===========================================================================
/**
* Get all actions available for a palette item.
* Actions are filtered by conditions and sorted by category/priority.
*/
getActionsForItem(
item: PaletteItem,
ctx: ActionExecutionContext
): ContextualAction[] {
const entityType = this.getEntityType(item);
const matchingProviders = this.getProvidersForType(entityType);
const allActions: ContextualAction[] = [];
for (const provider of matchingProviders) {
try {
const providerActions = provider.getActions(item, ctx);
const filteredActions = providerActions.filter((action) => {
if (!action.condition) return true;
try {
return action.condition(item, ctx);
} catch (error) {
console.error("[command-palette] action condition error", action.id, error);
return false;
}
});
allActions.push(...filteredActions);
} catch (error) {
console.error("[command-palette] provider getActions error", provider.id, error);
}
}
return sortActions(allActions);
}
/**
* Get the default action for an item (executed on Enter).
* Checks providers' getDefaultAction first, then falls back to the first
* primary action.
*/
getDefaultAction(
item: PaletteItem,
ctx: ActionExecutionContext
): ContextualAction | null {
const entityType = this.getEntityType(item);
const matchingProviders = this.getProvidersForType(entityType);
for (const provider of matchingProviders) {
if (provider.getDefaultAction) {
try {
const defaultAction = provider.getDefaultAction(item, ctx);
if (defaultAction) {
if (defaultAction.condition && !defaultAction.condition(item, ctx)) {
continue;
}
return defaultAction;
}
} catch (error) {
console.error("[command-palette] getDefaultAction error", provider.id, error);
}
}
}
const allActions = this.getActionsForItem(item, ctx);
const primaryAction = allActions.find((a) => a.category === "primary");
return primaryAction ?? allActions[0] ?? null;
}
/**
* Check whether an item deserves the actions submenu.
* Generic "basic" actions (open, open in a new tab…) are not
* enough: a single one of them, or a handful, duplicates what Enter already
* does — the submenu is reserved for items with real contextual actions.
*/
hasActionsForItem(item: PaletteItem, ctx: ActionExecutionContext): boolean {
return this.getActionsForItem(item, ctx).some((action) => !action.basic);
}
/**
* Execute the default action for an item.
*/
async executeDefaultAction(
item: PaletteItem,
ctx: ActionExecutionContext
): Promise<ActionResult> {
const defaultAction = this.getDefaultAction(item, ctx);
if (!defaultAction) {
return { success: false, message: "No action available" };
}
if (!defaultAction.execute) {
if (defaultAction.requiresForm) {
// Form actions are handled by the UI (opens the inline form)
return { success: false, message: "Action requires form input" };
}
return { success: false, message: "No execute function defined" };
}
try {
return await defaultAction.execute(item, ctx);
} catch (error) {
console.error("[command-palette] default action error", defaultAction.id, error);
return {
success: false,
message: error instanceof Error ? error.message : "Action failed",
};
}
}
// ===========================================================================
// ENTITY TYPE DETECTION
// ===========================================================================
/**
* Determine the entity type for a palette item.
* Explicit entityType wins; otherwise the filter category routes the item.
*/
private getEntityType(item: PaletteItem): string {
if (item.entityType) return item.entityType;
return item.filterCategory || "default";
}
// ===========================================================================
// PROVIDER LOOKUP CACHE
// ===========================================================================
/**
* Get providers that handle a specific entity type (including "*" catch-all
* providers), sorted by priority (highest first).
*/
private getProvidersForType(entityType: string): ActionProvider[] {
const cached = this.typeToProviders.get(entityType);
if (cached) return cached;
const matching = Array.from(this.providers.values())
.filter((p) => p.handles.includes(entityType) || p.handles.includes("*"))
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
this.typeToProviders.set(entityType, matching);
return matching;
}
}
/** Create a fresh registry (used internally by <CommandPalette>). */
export function createActionRegistry(): ActionRegistry {
return new ActionRegistry();
}