OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import type { Opc, OpcNote, OpcArtifact, OpcType, OpcStatus, OpcPriority } from '../types/opc';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL ?? '';
|
||||
|
||||
// ── OPC CRUD ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getNextNumber(): Promise<string> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/next-number`);
|
||||
if (!res.ok) throw new Error('Failed to fetch next OPC number');
|
||||
const data = await res.json();
|
||||
return data.number as string;
|
||||
}
|
||||
|
||||
export async function listOpcs(type?: string, status?: string): Promise<Opc[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (type && type !== 'all') params.set('type', type);
|
||||
if (status && status !== 'all') params.set('status', status);
|
||||
const res = await fetch(`${BASE_URL}/api/opc?${params}`);
|
||||
if (!res.ok) throw new Error(`Failed to load OPCs: ${res.statusText}`);
|
||||
// API returns OpcRecord (camelCase from .NET JsonSerializerDefaults.Web)
|
||||
return (await res.json()).map(mapRecord);
|
||||
}
|
||||
|
||||
export async function getOpc(id: string): Promise<Opc> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${id}`);
|
||||
if (!res.ok) throw new Error(`Failed to load OPC: ${res.statusText}`);
|
||||
return mapRecord(await res.json());
|
||||
}
|
||||
|
||||
export async function createOpc(req: {
|
||||
title: string; type: OpcType; priority: OpcPriority;
|
||||
assignee: string; description: string;
|
||||
}): Promise<Opc> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to create OPC: ${res.statusText}`);
|
||||
return mapRecord(await res.json());
|
||||
}
|
||||
|
||||
export async function updateOpc(id: string, req: {
|
||||
title?: string; description?: string; type?: OpcType;
|
||||
status?: OpcStatus; priority?: OpcPriority; assignee?: string;
|
||||
}): Promise<Opc> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to update OPC: ${res.statusText}`);
|
||||
return mapRecord(await res.json());
|
||||
}
|
||||
|
||||
// ── Notes ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function listNotes(opcId: string): Promise<OpcNote[]> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${opcId}/notes`);
|
||||
if (!res.ok) throw new Error(`Failed to load notes: ${res.statusText}`);
|
||||
return (await res.json()).map(mapNote);
|
||||
}
|
||||
|
||||
export async function addNote(opcId: string, author: string, content: string): Promise<OpcNote> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${opcId}/notes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ author, content }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to add note: ${res.statusText}`);
|
||||
return mapNote(await res.json());
|
||||
}
|
||||
|
||||
// ── Artifacts ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function listArtifacts(opcId: string, type?: string): Promise<OpcArtifact[]> {
|
||||
const params = type ? `?type=${type}` : '';
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${opcId}/artifacts${params}`);
|
||||
if (!res.ok) throw new Error(`Failed to load artifacts: ${res.statusText}`);
|
||||
return (await res.json()).map(mapArtifact);
|
||||
}
|
||||
|
||||
export async function createArtifact(opcId: string, req: {
|
||||
artifactType: string; title: string; content: string;
|
||||
}): Promise<OpcArtifact> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${opcId}/artifacts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to create artifact: ${res.statusText}`);
|
||||
return mapArtifact(await res.json());
|
||||
}
|
||||
|
||||
export async function updateArtifact(artifactId: string, req: {
|
||||
artifactType: string; title: string; content: string;
|
||||
}): Promise<OpcArtifact> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/artifacts/${artifactId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to update artifact: ${res.statusText}`);
|
||||
return mapArtifact(await res.json());
|
||||
}
|
||||
|
||||
export async function deleteArtifact(artifactId: string): Promise<void> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/artifacts/${artifactId}`, { method: 'DELETE' });
|
||||
if (!res.ok && res.status !== 404) throw new Error(`Failed to delete artifact: ${res.statusText}`);
|
||||
}
|
||||
|
||||
// ── Git commit linkage ────────────────────────────────────────────────────────
|
||||
// Commits are linked by convention: developers include "OPC # XXXX" in their commit message.
|
||||
// The git log endpoint supports ?grep=OPC+%230001 to filter.
|
||||
|
||||
export interface LinkedCommit {
|
||||
hash: string;
|
||||
shortHash: string;
|
||||
author: string;
|
||||
date: string;
|
||||
subject: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
export async function getLinkedCommits(opcNumber: string): Promise<LinkedCommit[]> {
|
||||
const res = await fetch(`${BASE_URL}/api/git/log?grep=${encodeURIComponent(opcNumber)}&limit=50`);
|
||||
if (!res.ok) throw new Error(`Failed to load commits: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── Pinned commits ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface PinnedCommit {
|
||||
opcId: string;
|
||||
hash: string;
|
||||
shortHash: string;
|
||||
subject: string;
|
||||
author: string;
|
||||
pinnedAt: string;
|
||||
pinnedBy: string;
|
||||
}
|
||||
|
||||
function mapPinnedCommit(d: Record<string, unknown>): PinnedCommit {
|
||||
return {
|
||||
opcId: d.opcId as string,
|
||||
hash: d.hash as string,
|
||||
shortHash: d.shortHash as string,
|
||||
subject: d.subject as string,
|
||||
author: d.author as string,
|
||||
pinnedAt: d.pinnedAt as string,
|
||||
pinnedBy: d.pinnedBy as string,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPinnedCommits(opcId: string): Promise<PinnedCommit[]> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${opcId}/pinned-commits`);
|
||||
if (!res.ok) throw new Error(`Failed to load pinned commits: ${res.statusText}`);
|
||||
return (await res.json()).map(mapPinnedCommit);
|
||||
}
|
||||
|
||||
export async function pinCommit(opcId: string, hash: string, pinnedBy: string): Promise<PinnedCommit> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${opcId}/pinned-commits`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hash, pinnedBy }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => res.statusText);
|
||||
throw new Error(body || res.statusText);
|
||||
}
|
||||
return mapPinnedCommit(await res.json());
|
||||
}
|
||||
|
||||
export async function unpinCommit(opcId: string, hash: string): Promise<void> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/${opcId}/pinned-commits/${hash}`, { method: 'DELETE' });
|
||||
if (!res.ok && res.status !== 404) throw new Error(`Failed to unpin commit: ${res.statusText}`);
|
||||
}
|
||||
|
||||
// ── Branch coverage ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface BranchCoverage {
|
||||
branch: string;
|
||||
contains: boolean;
|
||||
tipHash: string;
|
||||
isHead: boolean;
|
||||
}
|
||||
|
||||
export async function getBranchCoverage(hashes: string[]): Promise<BranchCoverage[]> {
|
||||
if (hashes.length === 0) return [];
|
||||
const res = await fetch(`${BASE_URL}/api/git/branch-coverage?commits=${hashes.join(',')}`);
|
||||
if (!res.ok) throw new Error(`Failed to get branch coverage: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── Commit detail (full diff) ─────────────────────────────────────────────────
|
||||
|
||||
export interface CommitFile {
|
||||
path: string;
|
||||
oldPath: string;
|
||||
status: string;
|
||||
additions: number;
|
||||
deletions: number;
|
||||
patch: string;
|
||||
}
|
||||
|
||||
export interface CommitDetail {
|
||||
hash: string;
|
||||
shortHash: string;
|
||||
author: string;
|
||||
email: string;
|
||||
date: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
files: CommitFile[];
|
||||
}
|
||||
|
||||
export async function getCommitDetail(hash: string): Promise<CommitDetail> {
|
||||
const res = await fetch(`${BASE_URL}/api/git/commits/${hash}`);
|
||||
if (!res.ok) throw new Error(`Failed to load commit ${hash}: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── AI assist ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function aiAssist(prompt: string, context?: string): Promise<string> {
|
||||
const res = await fetch(`${BASE_URL}/api/opc/ai-assist`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt, context }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`AI assist failed: ${err}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data.text as string;
|
||||
}
|
||||
|
||||
// ── Field mappers (snake_case/PascalCase → camelCase) ─────────────────────────
|
||||
// .NET JsonSerializerDefaults.Web produces camelCase already, so these are
|
||||
// lightweight guards in case of null/missing fields.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function mapRecord(r: any): Opc {
|
||||
return {
|
||||
id: r.id,
|
||||
number: r.number,
|
||||
title: r.title,
|
||||
description: r.description ?? '',
|
||||
type: r.type as OpcType,
|
||||
status: r.status as OpcStatus,
|
||||
priority: r.priority as OpcPriority,
|
||||
assignee: r.assignee ?? '',
|
||||
createdAt: r.createdAt,
|
||||
updatedAt: r.updatedAt,
|
||||
notes: [], // loaded separately on drawer open
|
||||
commits: [], // loaded separately on drawer open
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function mapNote(r: any): OpcNote {
|
||||
return {
|
||||
id: r.id,
|
||||
author: r.author,
|
||||
timestamp: r.createdAt,
|
||||
content: r.content,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function mapArtifact(r: any): OpcArtifact {
|
||||
return {
|
||||
id: r.id,
|
||||
opcId: r.opcId,
|
||||
artifactType: r.artifactType,
|
||||
title: r.title,
|
||||
content: r.content,
|
||||
createdAt: r.createdAt,
|
||||
updatedAt: r.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Gitea branch integration ───────────────────────────────────────────────────
|
||||
|
||||
export interface GiteaBranch {
|
||||
name: string;
|
||||
commitSha: string;
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
export async function listGiteaBranches(): Promise<GiteaBranch[]> {
|
||||
const res = await fetch(`${BASE_URL}/api/gitea/branches`);
|
||||
if (!res.ok) throw new Error(`Failed to load Gitea branches: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createGiteaBranch(
|
||||
opcNumber: string,
|
||||
opcTitle: string,
|
||||
from = 'master',
|
||||
): Promise<GiteaBranch> {
|
||||
const res = await fetch(`${BASE_URL}/api/gitea/branches`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ opcNumber, opcTitle, from }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => res.statusText);
|
||||
throw new Error(body || res.statusText);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
Reference in New Issue
Block a user