Normalize issues schema naming

Use camelCase for the Zod schema objects while keeping shared enum value arrays in CONSTANT_CASE.

Also adds search validation coverage for invalid statuses so the new route schema behavior stays tested.
This commit is contained in:
Antti Kettunen 2026-05-03 19:15:14 +03:00
parent 59eb8b75b0
commit 8bc686afec
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
4 changed files with 23 additions and 15 deletions

View file

@ -1,11 +1,11 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { ISSUE_CATEGORY_META } from './-issues.helpers'; import { ISSUE_CATEGORY_META } from './-issues.helpers';
import { ISSUE_SEARCH_SCHEMA } from './-issues.types'; import { issueSearchSchema } from './-issues.types';
describe('ISSUE_SEARCH_SCHEMA', () => { describe('issueSearchSchema', () => {
it('falls back to all for unknown categories', () => { it('falls back to all for unknown categories', () => {
expect(ISSUE_SEARCH_SCHEMA.parse({ category: 'not_real' })).toEqual({ expect(issueSearchSchema.parse({ category: 'not_real' })).toEqual({
status: 'open', status: 'open',
category: 'all', category: 'all',
issueId: undefined, issueId: undefined,
@ -13,15 +13,23 @@ describe('ISSUE_SEARCH_SCHEMA', () => {
}); });
it('preserves known categories', () => { it('preserves known categories', () => {
expect(ISSUE_SEARCH_SCHEMA.parse({ category: 'wrong_metadata' })).toEqual({ expect(issueSearchSchema.parse({ category: 'wrong_metadata' })).toEqual({
status: 'open', status: 'open',
category: 'wrong_metadata', category: 'wrong_metadata',
issueId: undefined, issueId: undefined,
}); });
}); });
it('falls back to open for unknown statuses', () => {
expect(issueSearchSchema.parse({ status: 'not_real' })).toEqual({
status: 'open',
category: 'all',
issueId: undefined,
});
});
it('drops invalid issue ids', () => { it('drops invalid issue ids', () => {
expect(ISSUE_SEARCH_SCHEMA.parse({ issueId: 'abc123' })).toEqual({ expect(issueSearchSchema.parse({ issueId: 'abc123' })).toEqual({
status: 'open', status: 'open',
category: 'all', category: 'all',
issueId: undefined, issueId: undefined,
@ -29,7 +37,7 @@ describe('ISSUE_SEARCH_SCHEMA', () => {
}); });
it('normalizes numeric issue ids', () => { it('normalizes numeric issue ids', () => {
expect(ISSUE_SEARCH_SCHEMA.parse({ issueId: '7' })).toEqual({ expect(issueSearchSchema.parse({ issueId: '7' })).toEqual({
status: 'open', status: 'open',
category: 'all', category: 'all',
issueId: 7, issueId: 7,

View file

@ -33,13 +33,13 @@ export const ISSUE_SEARCH_STATUS_VALUES = [
] as const; ] as const;
export const ISSUE_SEARCH_CATEGORY_VALUES = ['all', ...ISSUE_CATEGORY_VALUES] as const; export const ISSUE_SEARCH_CATEGORY_VALUES = ['all', ...ISSUE_CATEGORY_VALUES] as const;
export const ISSUE_SEARCH_SCHEMA = z.object({ export const issueSearchSchema = z.object({
status: z.enum(ISSUE_SEARCH_STATUS_VALUES).default('open').catch('open'), status: z.enum(ISSUE_SEARCH_STATUS_VALUES).default('open').catch('open'),
category: z.enum(ISSUE_SEARCH_CATEGORY_VALUES).default('all').catch('all'), category: z.enum(ISSUE_SEARCH_CATEGORY_VALUES).default('all').catch('all'),
issueId: z.coerce.number().int().positive().optional().catch(undefined), issueId: z.coerce.number().int().positive().optional().catch(undefined),
}); });
export type IssuesSearch = z.infer<typeof ISSUE_SEARCH_SCHEMA>; export type IssuesSearch = z.infer<typeof issueSearchSchema>;
export interface IssueTrackRow extends Record<string, unknown> { export interface IssueTrackRow extends Record<string, unknown> {
bitrate?: string | number; bitrate?: string | number;

View file

@ -326,7 +326,7 @@ const DEFAULT_REPORT_ISSUE_VALUES: ReportIssueFormValues = {
title: '', title: '',
}; };
const reportIssueFormBaseSchema = z.object({ const reportIssueFormSchema = z.object({
category: z category: z
.string() .string()
.trim() .trim()
@ -336,8 +336,8 @@ const reportIssueFormBaseSchema = z.object({
title: z.string().trim().min(1, 'Please provide a title for the issue'), title: z.string().trim().min(1, 'Please provide a title for the issue'),
}); });
type ReportIssueFormValues = z.input<typeof reportIssueFormBaseSchema>; type ReportIssueFormValues = z.input<typeof reportIssueFormSchema>;
type NormalizedReportIssueFormValues = z.output<typeof reportIssueFormBaseSchema>; type NormalizedReportIssueFormValues = z.output<typeof reportIssueFormSchema>;
function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') { function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') {
window.showToast?.(message, type); window.showToast?.(message, type);
@ -353,7 +353,7 @@ function updateBadge(openCount: number) {
function normalizeReportIssueFormValues( function normalizeReportIssueFormValues(
values: ReportIssueFormValues, values: ReportIssueFormValues,
): NormalizedReportIssueFormValues { ): NormalizedReportIssueFormValues {
return reportIssueFormBaseSchema.parse(values); return reportIssueFormSchema.parse(values);
} }
function validateReportIssueForm( function validateReportIssueForm(
@ -361,7 +361,7 @@ function validateReportIssueForm(
values: ReportIssueFormValues, values: ReportIssueFormValues,
): string | undefined { ): string | undefined {
if (!profileId) return 'Profile is still loading'; if (!profileId) return 'Profile is still loading';
const result = reportIssueFormBaseSchema.safeParse(values); const result = reportIssueFormSchema.safeParse(values);
if (result.success) return undefined; if (result.success) return undefined;
return result.error.issues[0]?.message || 'Unable to submit this issue'; return result.error.issues[0]?.message || 'Unable to submit this issue';
} }

View file

@ -7,11 +7,11 @@ import {
issueDetailQueryOptions, issueDetailQueryOptions,
issueListQueryOptions, issueListQueryOptions,
} from './-issues.api'; } from './-issues.api';
import { ISSUE_SEARCH_SCHEMA } from './-issues.types'; import { issueSearchSchema } from './-issues.types';
import { IssuesPage } from './-ui/issues-page'; import { IssuesPage } from './-ui/issues-page';
export const Route = createFileRoute('/issues')({ export const Route = createFileRoute('/issues')({
validateSearch: ISSUE_SEARCH_SCHEMA, validateSearch: issueSearchSchema,
beforeLoad: ({ context }) => { beforeLoad: ({ context }) => {
const { bridge } = context.shell; const { bridge } = context.shell;