220 lines
7.1 KiB
TypeScript
220 lines
7.1 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { toast } from "@/lib/toast/sonner";
|
|
import { TaskCreateDialog } from "@/components/task-create-dialog";
|
|
import { useAppStore } from "@/components/state-provider";
|
|
import { cacheAzureDevOpsTaskPullRequest } from "@/hooks/domains/azure-devops/use-azure-devops-task-pull-requests";
|
|
import { cacheAzureDevOpsTaskWorkItem } from "@/hooks/domains/azure-devops/use-azure-devops-task-work-items";
|
|
import { useRouter } from "@/lib/routing/client-router";
|
|
import { linkToTask } from "@/lib/links";
|
|
import {
|
|
associateAzureDevOpsPullRequest,
|
|
associateAzureDevOpsWorkItem,
|
|
} from "@/lib/api/domains/azure-devops-api";
|
|
import type {
|
|
AzureDevOpsActionPreset,
|
|
AzureDevOpsPullRequest,
|
|
AzureDevOpsWorkItem,
|
|
} from "@/lib/types/azure-devops";
|
|
import type { Repository, Task, Workflow, WorkflowStep } from "@/lib/types/http";
|
|
import { truncateRemoteTaskTitle } from "@/lib/task-title";
|
|
import { t } from "@/lib/i18n";
|
|
|
|
export type AzureDevOpsLaunchPayload =
|
|
| { kind: "work-item"; item: AzureDevOpsWorkItem; action?: AzureDevOpsActionPreset }
|
|
| { kind: "pull-request"; pullRequest: AzureDevOpsPullRequest; action?: AzureDevOpsActionPreset };
|
|
|
|
function plainText(value: string | undefined): string {
|
|
// Resolved per call, not at module load, so the fallback follows the active
|
|
// locale. `launchText` runs inside the launcher's `useMemo`, i.e. at render.
|
|
const fallback = t("azuredevops:descriptionFallback");
|
|
if (!value) return fallback;
|
|
const document = new DOMParser().parseFromString(value, "text/html");
|
|
return document.body.textContent?.trim() || fallback;
|
|
}
|
|
|
|
function launchText(payload: AzureDevOpsLaunchPayload) {
|
|
if (payload.kind === "work-item") {
|
|
const item = payload.item;
|
|
const description = [
|
|
`Azure DevOps work item: ${item.id}`,
|
|
`URL: ${item.webUrl ?? item.apiUrl ?? ""}`,
|
|
"",
|
|
item.title,
|
|
"",
|
|
plainText(item.description),
|
|
].join("\n");
|
|
return {
|
|
title: truncateRemoteTaskTitle(
|
|
payload.action
|
|
? `${payload.action.label}: ${item.title}`
|
|
: `${item.type} ${item.id}: ${item.title}`,
|
|
),
|
|
description: actionDescription(
|
|
payload.action,
|
|
item.webUrl ?? item.apiUrl ?? "",
|
|
item.title,
|
|
description,
|
|
),
|
|
};
|
|
}
|
|
const pullRequest = payload.pullRequest;
|
|
const description = [
|
|
`Azure DevOps pull request: ${pullRequest.webUrl}`,
|
|
"",
|
|
pullRequest.title,
|
|
"",
|
|
plainText(pullRequest.description),
|
|
].join("\n");
|
|
// i18n-exempt: persisted as the created task's title.
|
|
return {
|
|
title: truncateRemoteTaskTitle(
|
|
payload.action
|
|
? `${payload.action.label}: ${pullRequest.title}`
|
|
: `Review PR ${pullRequest.id}: ${pullRequest.title}`,
|
|
),
|
|
description: actionDescription(
|
|
payload.action,
|
|
pullRequest.webUrl,
|
|
pullRequest.title,
|
|
description,
|
|
),
|
|
};
|
|
}
|
|
|
|
function actionDescription(
|
|
action: AzureDevOpsActionPreset | undefined,
|
|
url: string,
|
|
title: string,
|
|
fallback: string,
|
|
): string {
|
|
if (!action?.promptTemplate.trim()) return fallback;
|
|
return action.promptTemplate.replaceAll("{{url}}", url).replaceAll("{{title}}", title);
|
|
}
|
|
|
|
function matchingRepository(
|
|
payload: AzureDevOpsLaunchPayload,
|
|
repositories: Repository[],
|
|
): Repository | undefined {
|
|
if (payload.kind !== "pull-request") return undefined;
|
|
return repositories.find(
|
|
(repository) =>
|
|
repository.provider === "azure_devops" &&
|
|
repository.provider_repo_id === payload.pullRequest.repositoryId &&
|
|
repository.provider_owner === payload.pullRequest.projectId,
|
|
);
|
|
}
|
|
|
|
function findLaunchWorkflow(workflows: Workflow[], steps: WorkflowStep[]) {
|
|
const workflow = workflows.find((candidate) =>
|
|
steps.some((step) => step.workflow_id === candidate.id),
|
|
);
|
|
const workflowSteps = steps
|
|
.filter((step) => step.workflow_id === workflow?.id)
|
|
.sort((left, right) => left.position - right.position);
|
|
return { workflow, workflowSteps };
|
|
}
|
|
|
|
export function AzureDevOpsTaskLauncher({
|
|
workspaceId,
|
|
workflows,
|
|
steps,
|
|
repositories,
|
|
payload,
|
|
onClose,
|
|
}: {
|
|
workspaceId?: string;
|
|
workflows: Workflow[];
|
|
steps: WorkflowStep[];
|
|
repositories: Repository[];
|
|
payload: AzureDevOpsLaunchPayload | null;
|
|
onClose: () => void;
|
|
}) {
|
|
const router = useRouter();
|
|
const setTaskPullRequest = useAppStore((state) => state.setAzureDevOpsTaskPullRequest);
|
|
const setTaskWorkItem = useAppStore((state) => state.setAzureDevOpsTaskWorkItem);
|
|
const launch = useMemo(() => {
|
|
if (!payload) return null;
|
|
const { workflow, workflowSteps } = findLaunchWorkflow(workflows, steps);
|
|
return {
|
|
workflow,
|
|
workflowSteps,
|
|
repository: matchingRepository(payload, repositories),
|
|
text: launchText(payload),
|
|
};
|
|
}, [payload, repositories, steps, workflows]);
|
|
|
|
const linkCreatedTask = async (task: Task) => {
|
|
if (payload?.kind === "work-item" && workspaceId) {
|
|
if (!payload.item.project) {
|
|
toast.error(t("azuredevops:failedToLinkWorkItemNoProject"));
|
|
} else {
|
|
try {
|
|
const linked = await associateAzureDevOpsWorkItem(workspaceId, task.id, {
|
|
projectId: payload.item.project,
|
|
workItemId: payload.item.id,
|
|
});
|
|
cacheAzureDevOpsTaskWorkItem(workspaceId, task.id, linked);
|
|
setTaskWorkItem(task.id, linked);
|
|
} catch (error: unknown) {
|
|
toast.error(
|
|
error instanceof Error ? error.message : t("azuredevops:failedToLinkWorkItem"),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (payload?.kind === "pull-request" && workspaceId && launch?.repository) {
|
|
try {
|
|
const linked = await associateAzureDevOpsPullRequest(workspaceId, task.id, {
|
|
repositoryId: launch.repository.id,
|
|
pullRequestId: payload.pullRequest.id,
|
|
});
|
|
cacheAzureDevOpsTaskPullRequest(workspaceId, task.id, linked);
|
|
setTaskPullRequest(task.id, linked);
|
|
} catch (error: unknown) {
|
|
toast.error(
|
|
error instanceof Error ? error.message : t("azuredevops:failedToLinkPullRequest"),
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
const onSuccess = async (
|
|
task: Task,
|
|
_mode?: "create" | "edit",
|
|
meta?: { autoFocus?: boolean },
|
|
) => {
|
|
await linkCreatedTask(task);
|
|
onClose();
|
|
if (meta?.autoFocus !== false) router.push(linkToTask(task.id));
|
|
};
|
|
|
|
if (!workspaceId || !payload || !launch?.workflow || !launch.workflowSteps[0]) return null;
|
|
return (
|
|
<TaskCreateDialog
|
|
open
|
|
onOpenChange={(open) => !open && onClose()}
|
|
mode="create"
|
|
workspaceId={workspaceId}
|
|
workflowId={launch.workflow.id}
|
|
defaultStepId={launch.workflowSteps[0].id}
|
|
steps={launch.workflowSteps.map((step) => ({
|
|
id: step.id,
|
|
title: step.name,
|
|
events: step.events,
|
|
}))}
|
|
initialValues={{
|
|
title: launch.text.title,
|
|
description: launch.text.description,
|
|
repositoryId: launch.repository?.id,
|
|
branch:
|
|
payload.kind === "pull-request"
|
|
? payload.pullRequest.sourceBranch.replace(/^refs\/heads\//, "")
|
|
: undefined,
|
|
}}
|
|
onSuccess={onSuccess}
|
|
/>
|
|
);
|
|
}
|