Ctrl K
ComunidadGuías…

lib/command-palette/registry/providers/MinddyEntityActionsProvider.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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;
    },
  };
}