diff --git a/webui/README.md b/webui/README.md index 30ee7f40..fb63eb61 100644 --- a/webui/README.md +++ b/webui/README.md @@ -82,5 +82,12 @@ The recommended dev flow keeps the backend and frontend separate: ``` Vite hot reloads the React side when you change webui files. +For linting and formatting, use: + +```bash +npm run check +npm run fix +``` + If you want a convenience wrapper, the repo root also includes `./dev.sh`. It starts both halves together and is most useful on Linux, macOS, and WSL. diff --git a/webui/package.json b/webui/package.json index 856ca34c..e59e01f2 100644 --- a/webui/package.json +++ b/webui/package.json @@ -3,7 +3,9 @@ "private": true, "type": "module", "scripts": { + "check": "vp check src", "dev": "vp dev", + "fix": "vp check --fix src", "build": "vp build", "test": "vp test run", "test:watch": "vp test", diff --git a/webui/src/components/form/form.test.tsx b/webui/src/components/form/form.test.tsx index 423c0eac..3488a2f6 100644 --- a/webui/src/components/form/form.test.tsx +++ b/webui/src/components/form/form.test.tsx @@ -66,7 +66,11 @@ function FormDemo() { {(['low', 'normal', 'high'] as const).map((value) => ( - setPriority(value)} selected={priority === value}> + setPriority(value)} + selected={priority === value} + > {value[0].toUpperCase()} {value.slice(1)} diff --git a/webui/src/components/form/form.tsx b/webui/src/components/form/form.tsx index db06faa7..56e78726 100644 --- a/webui/src/components/form/form.tsx +++ b/webui/src/components/form/form.tsx @@ -85,8 +85,7 @@ export function OptionCardGroup({ return
{children}
; } -export interface OptionCardProps - extends Omit, 'title'> { +export interface OptionCardProps extends Omit, 'title'> { description?: ReactNode; icon?: ReactNode; selected?: boolean; @@ -163,7 +162,14 @@ export const Button = forwardRef(function Button { className, type = 'button', ...props }, ref, ) { - return - +
+
Admin Actions
+
+ + +
- )} {isAdmin && ( @@ -494,11 +496,7 @@ export function IssueDetailModal({
- {!isLoading && !error && issue && ( @@ -626,8 +624,10 @@ function getTrackMetaItems(snapshot: ReturnType) { const duration = formatDuration(snapshot.duration); if (duration) items.push({ icon: 'T', label: 'Duration', value: duration }); if (snapshot.format) items.push({ icon: 'F', label: 'Format', value: String(snapshot.format) }); - if (snapshot.bitrate) items.push({ icon: 'B', label: 'Bitrate', value: `${snapshot.bitrate} kbps` }); + if (snapshot.bitrate) + items.push({ icon: 'B', label: 'Bitrate', value: `${snapshot.bitrate} kbps` }); if (snapshot.bpm) items.push({ icon: 'M', label: 'BPM', value: String(snapshot.bpm) }); - if (snapshot.quality) items.push({ icon: 'Q', label: 'Quality', value: String(snapshot.quality) }); + if (snapshot.quality) + items.push({ icon: 'Q', label: 'Quality', value: String(snapshot.quality) }); return items; } diff --git a/webui/src/routes/issues/-ui/issue-domain-host.tsx b/webui/src/routes/issues/-ui/issue-domain-host.tsx index 62425f28..f69ffdc9 100644 --- a/webui/src/routes/issues/-ui/issue-domain-host.tsx +++ b/webui/src/routes/issues/-ui/issue-domain-host.tsx @@ -1,9 +1,8 @@ -import { useForm } from "@tanstack/react-form"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; +import { useForm } from '@tanstack/react-form'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; -import { getShellProfileContext } from "@/platform/shell/bridge"; import { Button, FormActions, @@ -15,10 +14,11 @@ import { OptionCardGroup, TextArea, TextInput, -} from "@/components/form"; -import { useShellBridge } from "@/platform/shell/route-controllers"; +} from '@/components/form'; +import { getShellProfileContext } from '@/platform/shell/bridge'; +import { useShellBridge } from '@/platform/shell/route-controllers'; -import type { IssuePriority, IssueReportPayload } from "../-issues.types"; +import type { IssuePriority, IssueReportPayload } from '../-issues.types'; import { REFRESH_EVENT, @@ -26,10 +26,10 @@ import { createIssue, getIssueCategoriesForEntity, issueCountsQueryOptions, -} from "../-issues.helpers"; -import styles from "./issue-detail-modal.module.css"; +} from '../-issues.helpers'; +import styles from './issue-detail-modal.module.css'; -const ISSUE_DOMAIN_QUERY_KEY = ["issues"] as const; +const ISSUE_DOMAIN_QUERY_KEY = ['issues'] as const; interface ReportIssueFormValues { category: string; @@ -39,10 +39,10 @@ interface ReportIssueFormValues { } const DEFAULT_REPORT_ISSUE_VALUES: ReportIssueFormValues = { - category: "", - description: "", - priority: "normal", - title: "", + category: '', + description: '', + priority: 'normal', + title: '', }; export function IssueDomainHost() { @@ -127,7 +127,7 @@ function ReportIssueModal({ [payload.entityType], ); const entityLabel = - payload.entityType === "track" ? "Track" : payload.entityType === "album" ? "Album" : "Artist"; + payload.entityType === 'track' ? 'Track' : payload.entityType === 'album' ? 'Album' : 'Artist'; const createMutation = useMutation({ mutationFn: async (values: ReportIssueFormValues) => { @@ -141,7 +141,7 @@ function ReportIssueModal({ }); }, onSuccess: () => { - notify("Issue reported successfully", "success"); + notify('Issue reported successfully', 'success'); onSubmitted(); }, }); @@ -161,9 +161,9 @@ function ReportIssueModal({ await createMutation.mutateAsync(normalizedValues); } catch (mutationError) { const message = - mutationError instanceof Error ? mutationError.message : "Failed to submit issue"; + mutationError instanceof Error ? mutationError.message : 'Failed to submit issue'; formApi.setErrorMap({ onSubmit: message }); - notify(message, "error"); + notify(message, 'error'); throw mutationError; } }, @@ -206,7 +206,7 @@ function ReportIssueModal({ {payload.artistName ? (
{payload.artistName} - {payload.albumTitle ? ` - ${payload.albumTitle}` : ""} + {payload.albumTitle ? ` - ${payload.albumTitle}` : ''}
) : null}
@@ -227,9 +227,9 @@ function ReportIssueModal({ field.handleChange(category); createMutation.reset(); form.setErrorMap({ onSubmit: undefined }); - if (!form.getFieldMeta("title")?.isTouched) { + if (!form.getFieldMeta('title')?.isTouched) { form.setFieldValue( - "title", + 'title', createDefaultIssueTitle(category, payload.entityName), { dontUpdateMeta: true }, ); @@ -298,7 +298,7 @@ function ReportIssueModal({ label="Priority" > - {(["low", "normal", "high"] as const).map((priority) => ( + {(['low', 'normal', 'high'] as const).map((priority) => ( field.handleChange(priority)} @@ -326,11 +326,7 @@ function ReportIssueModal({ - - {isSubmitting ? "Submitting..." : "Submit Issue"} + {isSubmitting ? 'Submitting...' : 'Submit Issue'} ); }} @@ -373,27 +369,27 @@ function validateReportIssueForm( values: ReportIssueFormValues, ): string | undefined { const normalizedValues = normalizeReportIssueFormValues(values); - if (!profileId) return "Profile is still loading"; - if (!normalizedValues.category) return "Please select an issue category"; - if (!normalizedValues.title) return "Please provide a title for the issue"; + if (!profileId) return 'Profile is still loading'; + if (!normalizedValues.category) return 'Please select an issue category'; + if (!normalizedValues.title) return 'Please provide a title for the issue'; return undefined; } function getReportIssueFormError(errors: Array): string { const error = errors.find(Boolean); - if (!error) return ""; - if (typeof error === "string") return error; + if (!error) return ''; + if (typeof error === 'string') return error; if (error instanceof Error) return error.message; return String(error); } -function notify(message: string, type: "success" | "error" | "warning" | "info" = "info") { +function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') { window.showToast?.(message, type); } function updateBadge(openCount: number) { - const badge = document.getElementById("issues-nav-badge"); + const badge = document.getElementById('issues-nav-badge'); if (!badge) return; badge.textContent = String(openCount || 0); - badge.classList.toggle("hidden", !openCount); + badge.classList.toggle('hidden', !openCount); } diff --git a/webui/src/routes/issues/-ui/issues-page.tsx b/webui/src/routes/issues/-ui/issues-page.tsx index 11fcf0a9..4a9c3e83 100644 --- a/webui/src/routes/issues/-ui/issues-page.tsx +++ b/webui/src/routes/issues/-ui/issues-page.tsx @@ -2,8 +2,8 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { useEffect, useState } from 'react'; -import { getShellProfileContext } from '@/platform/shell/bridge'; import { Select } from '@/components/form'; +import { getShellProfileContext } from '@/platform/shell/bridge'; import { useReactPageShell } from '@/platform/shell/route-controllers'; import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types'; diff --git a/webui/vite.config.ts b/webui/vite.config.ts index 6d05aaf0..b7bbb73a 100644 --- a/webui/vite.config.ts +++ b/webui/vite.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ ], }, sortPackageJson: true, + trailingComma: 'all', }, plugins: [ tanstackRouter({