lib/command-palette/registry/providers/MinddyEntityActionsProvider.ts
Rama main · réplica de lectura del repositorio original.
import { projectPath, recordRef } from "@/lib/app-urls";
import type {
Project,
SearchIndexIssue,
SearchIndexObjective,
SearchIndexPage,
} from "@/lib/types";
import { CircleDashedIcon, Copy, ExternalLinkIcon, FileTextIcon as FileText, FolderKanbanIcon, Link02Icon as Link2, Settings, Target01Icon as Target, Task01Icon as ListTodo } from "@hugeicons/core-free-icons";
import type { PaletteItem } from "../../types";
import type { ActionProvider, ContextualAction } from "../types";
export interface MinddyEntityActionLabels {
copied: string;
linkCopied: string;
openInNewTab: string;
copyLink: string;
newIssue: string;
newObjective: string;
copyProjectKey: string;
openObjectives: string;
openPages: string;
openTriage: string;
openFeedback: string;
openProjectSettings: string;
viewObjectiveIssues: string;
newIssueForObjective: string;
copyObjectiveName: string;
openProjectBoard: string;
newIssueInProject: string;
copyPageTitle: string;
copyIssueTitle: string;
openLinkedObjective: string;
}
export interface MinddyEntityActionsDependencies {
labels: MinddyEntityActionLabels;
navigate: (href: string) => void;
openInNewTab: (href: string) => void;
copyText: (
value: string,
confirmation: string,
resolveHref?: boolean
) => Promise<void>;
openCreateIssue: (projectId: string, objectiveId?: string) => void;
openCreateObjective: (projectId: string) => void;
}
/**
* Build the exhaustive, read-safe action catalog for each searchable entity.
* Mutating field actions for issues remain in the issue provider; this catalog
* owns navigation, creation and clipboard actions shared across entity types.
*/
export function createMinddyEntityActionsProvider(
dependencies: MinddyEntityActionsDependencies
): ActionProvider {
const { labels } = dependencies;
const go = (
id: string,
label: string,
icon: typeof FolderKanbanIcon,
href: string,
priority = 0
): ContextualAction => ({
id,
label,
icon,
category: "navigation",
priority,
execute: async () => {
dependencies.navigate(href);
// The destination replaces the palette: staying open would hover over
// the page it just opened.
return { success: true, closeMenu: true };
},
});
const create = (
id: string,
label: string,
icon: typeof ListTodo,
run: () => void,
priority = 0
): ContextualAction => ({
id,
label,
icon,
category: "secondary",
priority,
execute: async () => {
run();
return { success: true };
},
});
const copy = (
id: string,
label: string,
value: string,
priority = 0
): ContextualAction => ({
id,
label,
icon: Copy,
category: "secondary",
priority,
execute: async () => {
await dependencies.copyText(value, labels.copied);
return { success: true, closeMenu: false };
},
});
// Basic only when the menu already has real counterparts (entities): pure
// navigation rows must still earn the ⌘; actions menu — the sole guest there
// is exactly these two link gestures (see ActionRegistry.hasActionsForItem).
const linkActions = (item: PaletteItem, basic = true): ContextualAction[] => {
if (!item.href) return [];
return [
{
id: "entity.open-new-tab",
label: labels.openInNewTab,
icon: ExternalLinkIcon,
category: "navigation",
priority: -90,
basic,
execute: async () => {
dependencies.openInNewTab(item.href as string);
// A new tab opened: the palette has served its purpose.
return { success: true, closeMenu: true };
},
},
{
id: "entity.copy-link",
label: labels.copyLink,
icon: Link2,
category: "secondary",
priority: -90,
basic,
execute: async () => {
await dependencies.copyText(item.href as string, labels.linkCopied, true);
return { success: true, closeMenu: false };
},
},
];
};
return {
id: "minddy-entity-actions",
handles: ["navigation", "project", "objective", "page", "issue"],
priority: 40,
getActions: (item): ContextualAction[] => {
const actions = linkActions(item, item.entityType !== "navigation");
if (item.entityType === "navigation") {
return actions;
}
if (item.entityType === "project") {
const project = item.data as Project | undefined;
if (!project) return actions;
const base = projectPath(project.id);
actions.push(
create("project.new-issue", labels.newIssue, ListTodo, () => dependencies.openCreateIssue(project.id), 50),
create("project.new-objective", labels.newObjective, Target, () => dependencies.openCreateObjective(project.id), 40),
copy("project.copy-key", labels.copyProjectKey, project.key, 20),
go("project.objectives", labels.openObjectives, Target, `${base}/objectives`, 50),
go("project.pages", labels.openPages, FileText, `${base}/pages`, 40),
go("project.triage", labels.openTriage, CircleDashedIcon, `${base}/triage`, 30),
go("project.feedback", labels.openFeedback, Link2, `${base}/feedback`, 20),
go("project.settings", labels.openProjectSettings, Settings, `${base}/settings`, 10)
);
return actions;
}
if (item.entityType === "objective") {
const objective = item.data as SearchIndexObjective | undefined;
if (!objective) return actions;
const base = projectPath(objective.project_id);
actions.push(
go("objective.issues", labels.viewObjectiveIssues, ListTodo, `${base}?objective=${recordRef(objective.id)}`, 100),
create(
"objective.new-issue",
labels.newIssueForObjective,
ListTodo,
() => dependencies.openCreateIssue(objective.project_id, objective.id),
50
),
create("objective.new-objective", labels.newObjective, Target, () => dependencies.openCreateObjective(objective.project_id), 20),
copy("objective.copy-name", labels.copyObjectiveName, item.title, 10),
go("objective.list", labels.openObjectives, Target, `${base}/objectives`, 50),
go("objective.project", labels.openProjectBoard, FolderKanbanIcon, base, 40)
);
return actions;
}
if (item.entityType === "page") {
const page = item.data as SearchIndexPage | undefined;
if (!page) return actions;
const base = projectPath(page.project_id);
actions.push(
create("page.new-issue", labels.newIssueInProject, ListTodo, () => dependencies.openCreateIssue(page.project_id), 40),
copy("page.copy-title", labels.copyPageTitle, item.title, 20),
go("page.index", labels.openPages, FileText, `${base}/pages`, 50),
go("page.project", labels.openProjectBoard, FolderKanbanIcon, base, 40)
);
return actions;
}
const issue = item.data as SearchIndexIssue | undefined;
if (!issue) return actions;
const base = projectPath(issue.project_id);
actions.push(
create("issue.new-issue", labels.newIssueInProject, ListTodo, () => dependencies.openCreateIssue(issue.project_id), 20),
copy("issue.copy-title", labels.copyIssueTitle, item.title, 10),
go("issue.project", labels.openProjectBoard, FolderKanbanIcon, base, 30)
);
if (issue.objective_id) {
actions.push(
go("issue.objective-issues", labels.viewObjectiveIssues, ListTodo, `${base}?objective=${recordRef(issue.objective_id)}`, 60),
go("issue.open-objective", labels.openLinkedObjective, Target, `${base}/objectives?open=${recordRef(issue.objective_id)}`, 70)
);
}
return actions;
},
};
}