Newer
Older
bugtrail / packages / extension / src / background / api.ts
import type { HttpClient, ReportCreateIn, ReportDetail, AnnotationShape } from "@ltt/shared";
import { getHttpClient, getSettings, type Settings } from "./settings";
import type { SubmitNotePayload } from "../lib/messages";

async function dataUrlToFile(dataUrl: string, filename: string): Promise<File> {
  const response = await fetch(dataUrl);
  const blob = await response.blob();
  return new File([blob], filename, { type: blob.type || "image/png" });
}

async function uploadFile(client: HttpClient, dataUrl: string, filename: string): Promise<string> {
  const file = await dataUrlToFile(dataUrl, filename);
  const result = await client.upload<{ file_id: string }>("/api/uploads", file);
  return result.file_id;
}

/** Prepares and submits an element-note report from the NoteComposer payload. */
export async function submitNoteReport(payload: SubmitNotePayload): Promise<{ report_token: string }> {
  const settings = await getSettings();

  if (!settings.token) throw new Error("Not signed in — open the extension settings");
  if (!settings.defaultProjectId) throw new Error("No default project selected — open the extension settings");

  const client = await getHttpClient();
  const attachmentIds: string[] = [];
  const shapesByFile: Record<string, AnnotationShape[]> = {};

  const screenshotId = await uploadFile(client, payload.screenshotDataUrl, "screenshot.png");
  attachmentIds.push(screenshotId);
  if (payload.annotationShapes?.length) {
    shapesByFile[screenshotId] = payload.annotationShapes;
  }

  for (const [index, file] of payload.files.entries()) {
    attachmentIds.push(await uploadFile(client, file.dataUrl, file.filename || `attachment-${index + 1}`));
  }

  const body: ReportCreateIn = {
    project_id: settings.defaultProjectId,
    type: "element_note",
    title: payload.title || "Element note",
    description: [payload.comment, ...payload.links.map((l) => l)].filter(Boolean).join("\n\n") || null,
    page_url: payload.pageUrl || null,
    page_title: payload.pageTitle || null,
    environment: payload.environment,
    element: payload.element,
    attachment_ids: attachmentIds,
    annotation_shapes: Object.keys(shapesByFile).length ? shapesByFile : null,
  };

  const report = await client.post<ReportDetail>("/api/reports", body);
  return { report_token: report.share_token };
}

/** Prepares and submits a recording report from buffered recorder steps. */
export async function submitRecordingReport(input: {
  payload: Omit<ReportCreateIn, "project_id">;
  settings?: Settings;
}): Promise<{ report_token: string }> {
  const settings = input.settings ?? (await getSettings());
  if (!settings.token) throw new Error("Not signed in — open the extension settings");
  if (!settings.defaultProjectId) throw new Error("No default project selected — open the extension settings");
  const client = await getHttpClient();
  const report = await client.post<ReportDetail>("/api/reports", {
    ...input.payload,
    project_id: settings.defaultProjectId,
  });
  return { report_token: report.share_token };
}

/** Composer Save: the report already exists — patch its title/description. */
export async function updateRecordingReport(token: string, title: string, description: string): Promise<ReportDetail> {
  const client = await getHttpClient();
  // empty fields are dropped so the server keeps the auto-title / no description
  return client.patch<ReportDetail>(`/api/reports/${token}`, {
    ...(title.trim() ? { title: title.trim() } : {}),
    ...(description.trim() ? { description: description.trim() } : {}),
  });
}