From ffd989d0f3d74bea413863bb309a2dc1774de845 Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sat, 2 May 2026 17:53:22 +0300 Subject: [PATCH] Split issue request code into api module - Move HTTP and query-option helpers out of -issues.helpers.ts. - Keep -issues.helpers.ts focused on pure normalization and formatting helpers. - Update issue route and modal callers to import request code from -issues.api.ts. --- webui/src/routes/issues/-issues.api.ts | 150 ++++++++++++++++++ webui/src/routes/issues/-issues.helpers.ts | 145 ----------------- .../routes/issues/-ui/issue-detail-modal.tsx | 3 +- .../routes/issues/-ui/issue-domain-host.tsx | 6 +- webui/src/routes/issues/-ui/issues-page.tsx | 10 +- webui/src/routes/issues/route.tsx | 4 +- 6 files changed, 163 insertions(+), 155 deletions(-) create mode 100644 webui/src/routes/issues/-issues.api.ts diff --git a/webui/src/routes/issues/-issues.api.ts b/webui/src/routes/issues/-issues.api.ts new file mode 100644 index 00000000..894eebe0 --- /dev/null +++ b/webui/src/routes/issues/-issues.api.ts @@ -0,0 +1,150 @@ +import { queryOptions } from '@tanstack/react-query'; + +import { apiClient, readJson } from '@/app/api-client'; + +import type { + CreateIssuePayload, + IssueCounts, + IssueCountsResponse, + IssueDetailResponse, + IssueListResponse, + IssueRecord, +} from './-issues.types'; + +import type { NormalizedIssuesSearch } from './-issues.helpers'; + +const DEFAULT_LIMIT = 100; + +function createIssueHeaders(profileId: number, extra?: HeadersInit): Headers { + const headers = new Headers(extra); + headers.set('X-Profile-Id', String(profileId || 1)); + return headers; +} + +export async function fetchIssueCounts(profileId: number): Promise { + const payload = await readJson( + apiClient.get('issues/counts', { + headers: createIssueHeaders(profileId), + }), + ); + if (!payload.success) { + throw new Error(payload.error || 'Failed to load issue counts'); + } + return payload.counts; +} + +export async function fetchIssueList( + profileId: number, + search: Pick, +): Promise { + const params = new URLSearchParams(); + params.set('limit', String(DEFAULT_LIMIT)); + if (search.status !== 'all') { + params.set('status', search.status); + } + if (search.category !== 'all') { + params.set('category', search.category); + } + + const payload = await readJson( + apiClient.get('issues', { + headers: createIssueHeaders(profileId), + searchParams: params, + }), + ); + if (!payload.success) { + throw new Error(payload.error || 'Failed to load issues'); + } + return payload; +} + +export async function fetchIssue(profileId: number, issueId: number): Promise { + const payload = await readJson( + apiClient.get(`issues/${issueId}`, { + headers: createIssueHeaders(profileId), + }), + ); + if (!payload.success || !payload.issue) { + throw new Error(payload.error || 'Issue not found'); + } + return payload.issue; +} + +export async function updateIssue( + profileId: number, + issueId: number, + updates: { status?: string; admin_response?: string }, +): Promise { + const payload = await readJson<{ success: boolean; error?: string }>( + apiClient.put(`issues/${issueId}`, { + headers: createIssueHeaders(profileId), + json: updates, + }), + ); + if (!payload.success) { + throw new Error(payload.error || 'Failed to update issue'); + } +} + +export async function createIssue( + profileId: number, + payload: CreateIssuePayload, +): Promise { + const response = await readJson<{ + success: boolean; + issue?: IssueRecord; + error?: string; + }>( + apiClient.post('issues', { + headers: createIssueHeaders(profileId, { 'Content-Type': 'application/json' }), + json: { + entity_type: payload.entity_type, + entity_id: String(payload.entity_id), + category: payload.category, + title: payload.title, + description: payload.description || '', + priority: payload.priority || 'normal', + }, + }), + ); + if (!response.success) { + throw new Error(response.error || 'Failed to submit issue'); + } + return response.issue ?? null; +} + +export async function deleteIssue(profileId: number, issueId: number): Promise { + const payload = await readJson<{ success: boolean; error?: string }>( + apiClient.delete(`issues/${issueId}`, { + headers: createIssueHeaders(profileId), + }), + ); + if (!payload.success) { + throw new Error(payload.error || 'Failed to delete issue'); + } +} + +export function issueCountsQueryOptions(profileId: number) { + return queryOptions({ + queryKey: ['issues', 'counts', profileId], + queryFn: () => fetchIssueCounts(profileId), + }); +} + +export function issueListQueryOptions( + profileId: number, + search: Pick, +) { + return queryOptions({ + queryKey: ['issues', 'list', profileId, search.status, search.category], + queryFn: () => fetchIssueList(profileId, search), + }); +} + +export function issueDetailQueryOptions(profileId: number, issueId: number) { + return queryOptions({ + queryKey: ['issues', 'detail', profileId, issueId], + queryFn: () => fetchIssue(profileId, issueId), + enabled: issueId > 0, + }); +} diff --git a/webui/src/routes/issues/-issues.helpers.ts b/webui/src/routes/issues/-issues.helpers.ts index 02993bd3..01683a45 100644 --- a/webui/src/routes/issues/-issues.helpers.ts +++ b/webui/src/routes/issues/-issues.helpers.ts @@ -1,20 +1,9 @@ -import { queryOptions } from '@tanstack/react-query'; - -import { apiClient, readJson } from '@/app/api-client'; - import type { - IssueCounts, - IssueCountsResponse, - IssueDetailResponse, - IssueListResponse, IssueRecord, - CreateIssuePayload, IssuesSearch, IssueSnapshot, } from './-issues.types'; -const DEFAULT_LIMIT = 100; - export const REFRESH_EVENT = 'ss:issues-refresh'; export const CLOSE_EVENT = 'ss:issues-close-detail'; @@ -102,12 +91,6 @@ export const ISSUE_STATUS_META: Record category.applies.includes(entityType), @@ -145,134 +128,6 @@ export function normalizeIssuesSearch(search: IssuesSearch | undefined): Normali }; } -export async function fetchIssueCounts(profileId: number): Promise { - const payload = await readJson( - apiClient.get('issues/counts', { - headers: createIssueHeaders(profileId), - }), - ); - if (!payload.success) { - throw new Error(payload.error || 'Failed to load issue counts'); - } - return payload.counts; -} - -export async function fetchIssueList( - profileId: number, - search: Pick, -): Promise { - const params = new URLSearchParams(); - params.set('limit', String(DEFAULT_LIMIT)); - if (search.status !== 'all') { - params.set('status', search.status); - } - if (search.category !== 'all') { - params.set('category', search.category); - } - - const payload = await readJson( - apiClient.get('issues', { - headers: createIssueHeaders(profileId), - searchParams: params, - }), - ); - if (!payload.success) { - throw new Error(payload.error || 'Failed to load issues'); - } - return payload; -} - -export async function fetchIssue(profileId: number, issueId: number): Promise { - const payload = await readJson( - apiClient.get(`issues/${issueId}`, { - headers: createIssueHeaders(profileId), - }), - ); - if (!payload.success || !payload.issue) { - throw new Error(payload.error || 'Issue not found'); - } - return payload.issue; -} - -export async function updateIssue( - profileId: number, - issueId: number, - updates: { status?: string; admin_response?: string }, -): Promise { - const payload = await readJson<{ success: boolean; error?: string }>( - apiClient.put(`issues/${issueId}`, { - headers: createIssueHeaders(profileId), - json: updates, - }), - ); - if (!payload.success) { - throw new Error(payload.error || 'Failed to update issue'); - } -} - -export async function createIssue( - profileId: number, - payload: CreateIssuePayload, -): Promise { - const response = await readJson<{ - success: boolean; - issue?: IssueRecord; - error?: string; - }>( - apiClient.post('issues', { - headers: createIssueHeaders(profileId, { 'Content-Type': 'application/json' }), - json: { - entity_type: payload.entity_type, - entity_id: String(payload.entity_id), - category: payload.category, - title: payload.title, - description: payload.description || '', - priority: payload.priority || 'normal', - }, - }), - ); - if (!response.success) { - throw new Error(response.error || 'Failed to submit issue'); - } - return response.issue ?? null; -} - -export async function deleteIssue(profileId: number, issueId: number): Promise { - const payload = await readJson<{ success: boolean; error?: string }>( - apiClient.delete(`issues/${issueId}`, { - headers: createIssueHeaders(profileId), - }), - ); - if (!payload.success) { - throw new Error(payload.error || 'Failed to delete issue'); - } -} - -export function issueCountsQueryOptions(profileId: number) { - return queryOptions({ - queryKey: ['issues', 'counts', profileId], - queryFn: () => fetchIssueCounts(profileId), - }); -} - -export function issueListQueryOptions( - profileId: number, - search: Pick, -) { - return queryOptions({ - queryKey: ['issues', 'list', profileId, search.status, search.category], - queryFn: () => fetchIssueList(profileId, search), - }); -} - -export function issueDetailQueryOptions(profileId: number, issueId: number) { - return queryOptions({ - queryKey: ['issues', 'detail', profileId, issueId], - queryFn: () => fetchIssue(profileId, issueId), - enabled: issueId > 0, - }); -} - export function dispatchIssuesRefreshEvent() { window.dispatchEvent(new CustomEvent(REFRESH_EVENT)); } diff --git a/webui/src/routes/issues/-ui/issue-detail-modal.tsx b/webui/src/routes/issues/-ui/issue-detail-modal.tsx index fb571d4a..79bb88e3 100644 --- a/webui/src/routes/issues/-ui/issue-detail-modal.tsx +++ b/webui/src/routes/issues/-ui/issue-detail-modal.tsx @@ -10,15 +10,14 @@ import { import type { IssueRecord } from '../-issues.types'; import { - deleteIssue, formatIssueDate, formatStatusLabel, getIssueArtwork, getPriorityClassName, ISSUE_CATEGORY_META, parseSnapshot, - updateIssue, } from '../-issues.helpers'; +import { deleteIssue, updateIssue } from '../-issues.api'; import styles from './issue-detail-modal.module.css'; export function IssueDetailModal({ diff --git a/webui/src/routes/issues/-ui/issue-domain-host.tsx b/webui/src/routes/issues/-ui/issue-domain-host.tsx index f69ffdc9..97505f51 100644 --- a/webui/src/routes/issues/-ui/issue-domain-host.tsx +++ b/webui/src/routes/issues/-ui/issue-domain-host.tsx @@ -23,10 +23,12 @@ import type { IssuePriority, IssueReportPayload } from '../-issues.types'; import { REFRESH_EVENT, createDefaultIssueTitle, - createIssue, getIssueCategoriesForEntity, - issueCountsQueryOptions, } from '../-issues.helpers'; +import { + createIssue, + issueCountsQueryOptions, +} from '../-issues.api'; import styles from './issue-detail-modal.module.css'; const ISSUE_DOMAIN_QUERY_KEY = ['issues'] as const; diff --git a/webui/src/routes/issues/-ui/issues-page.tsx b/webui/src/routes/issues/-ui/issues-page.tsx index 4dbe29e8..9563614e 100644 --- a/webui/src/routes/issues/-ui/issues-page.tsx +++ b/webui/src/routes/issues/-ui/issues-page.tsx @@ -17,15 +17,17 @@ import { getEntityName, getIssueArtwork, getPriorityClassName, - issueCountsQueryOptions, - issueDetailQueryOptions, - issueListQueryOptions, + formatIssueDate, ISSUE_CATEGORY_META, ISSUE_STATUS_META, normalizeIssuesSearch, parseSnapshot, - formatIssueDate, } from '../-issues.helpers'; +import { + issueCountsQueryOptions, + issueDetailQueryOptions, + issueListQueryOptions, +} from '../-issues.api'; import { Route } from '../route'; import { IssueDetailModal } from './issue-detail-modal'; import styles from './issues-page.module.css'; diff --git a/webui/src/routes/issues/route.tsx b/webui/src/routes/issues/route.tsx index 735805f4..709021f5 100644 --- a/webui/src/routes/issues/route.tsx +++ b/webui/src/routes/issues/route.tsx @@ -6,8 +6,8 @@ import { issueCountsQueryOptions, issueDetailQueryOptions, issueListQueryOptions, - normalizeIssuesSearch, -} from './-issues.helpers'; +} from './-issues.api'; +import { normalizeIssuesSearch } from './-issues.helpers'; import { IssuesPage } from './-ui/issues-page'; export const Route = createFileRoute('/issues')({