fix(web): show change-password validation messages and failure reason

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-05 22:27:30 -04:00
1 parent a6d642616b
commit 436c463cc8
5 files changed
+24 -21

No files matched your search

@@ -47,8 +47,9 @@ import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useResetUserState } from "@/hooks/use-reset-user-state"; import { useResetUserState } from "@/hooks/use-reset-user-state";
import { authClient, signOut } from "@/lib/auth/client"; import { authClient, signOut } from "@/lib/auth/client";
import { getErrorMessage } from "@/lib/error-messages"; import { getAuthErrorMessage, getErrorMessage } from "@/lib/error-messages";
import { useAppForm } from "@/lib/form"; import { useAppForm } from "@/lib/form";
import { formatFieldErrors } from "@/lib/form/fields";
import { orpc } from "@/lib/orpc/client"; import { orpc } from "@/lib/orpc/client";
import type { NormalizedImport } from "@sofa/api/schemas"; import type { NormalizedImport } from "@sofa/api/schemas";
import { formatDate } from "@sofa/i18n/format"; import { formatDate } from "@sofa/i18n/format";
@@ -611,7 +612,7 @@ function ChangePasswordDialog() {
revokeOtherSessions: value.revokeOtherSessions, revokeOtherSessions: value.revokeOtherSessions,
}); });
if (result.error) { if (result.error) {
setError(t`Failed to change password`); setError(getAuthErrorMessage(result.error, t`Failed to change password`));
return; return;
} }
toast.success(t`Password updated`); toast.success(t`Password updated`);
@@ -677,9 +678,7 @@ function ChangePasswordDialog() {
/> />
{field.state.meta.errors.length > 0 && ( {field.state.meta.errors.length > 0 && (
<p className="text-destructive text-xs"> <p className="text-destructive text-xs">
{field.state.meta.errors {formatFieldErrors(field.state.meta.errors)}
.map((e) => (typeof e === "string" ? e : ""))
.join(", ")}
</p> </p>
)} )}
</div> </div>
@@ -703,9 +702,7 @@ function ChangePasswordDialog() {
/> />
{field.state.meta.errors.length > 0 && ( {field.state.meta.errors.length > 0 && (
<p className="text-destructive text-xs"> <p className="text-destructive text-xs">
{field.state.meta.errors {formatFieldErrors(field.state.meta.errors)}
.map((e) => (typeof e === "string" ? e : ""))
.join(", ")}
</p> </p>
)} )}
</div> </div>
@@ -729,9 +726,7 @@ function ChangePasswordDialog() {
/> />
{field.state.meta.errors.length > 0 && ( {field.state.meta.errors.length > 0 && (
<p className="text-destructive text-xs"> <p className="text-destructive text-xs">
{field.state.meta.errors {formatFieldErrors(field.state.meta.errors)}
.map((e) => (typeof e === "string" ? e : ""))
.join(", ")}
</p> </p>
)} )}
</div> </div>
@@ -1,7 +1,7 @@
import { Trans, useLingui } from "@lingui/react/macro"; import { Trans, useLingui } from "@lingui/react/macro";
import { IconDoorEnter } from "@tabler/icons-react"; import { IconDoorEnter } from "@tabler/icons-react";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useOptimistic, useState, useTransition } from "react"; import { useOptimistic, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card"; import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
@@ -12,9 +12,9 @@ import { orpc } from "@/lib/orpc/client";
export function RegistrationSection() { export function RegistrationSection() {
const { t } = useLingui(); const { t } = useLingui();
const { data, isPending: isLoading } = useQuery(orpc.admin.settings.get.queryOptions()); const { data, isPending: isLoading } = useQuery(orpc.admin.settings.get.queryOptions());
const [registrationOpen, setRegistrationOpen] = useState<boolean | null>(null); const currentOpen = data?.registration.open ?? false;
const currentOpen = registrationOpen ?? data?.registration.open ?? false;
const [optimisticOpen, setOptimisticOpen] = useOptimistic(currentOpen); const [optimisticOpen, setOptimisticOpen] = useOptimistic(currentOpen);
const queryClient = useQueryClient();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const toggleMutation = useMutation(orpc.admin.settings.update.mutationOptions()); const toggleMutation = useMutation(orpc.admin.settings.update.mutationOptions());
@@ -31,7 +31,7 @@ export function RegistrationSection() {
setOptimisticOpen(checked); setOptimisticOpen(checked);
try { try {
await toggleMutation.mutateAsync({ registration: { open: checked } }); await toggleMutation.mutateAsync({ registration: { open: checked } });
setRegistrationOpen(checked); await queryClient.invalidateQueries({ queryKey: orpc.admin.settings.get.key() });
toast.success(checked ? t`Registration opened` : t`Registration closed`); toast.success(checked ? t`Registration opened` : t`Registration closed`);
} catch { } catch {
toast.error(t`Failed to update registration setting`); toast.error(t`Failed to update registration setting`);
@@ -1,7 +1,7 @@
import { Trans, useLingui } from "@lingui/react/macro"; import { Trans, useLingui } from "@lingui/react/macro";
import { IconWorldUpload } from "@tabler/icons-react"; import { IconWorldUpload } from "@tabler/icons-react";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useOptimistic, useState, useTransition } from "react"; import { useOptimistic, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { CardContent, CardDescription, CardTitle } from "@/components/ui/card"; import { CardContent, CardDescription, CardTitle } from "@/components/ui/card";
@@ -12,9 +12,9 @@ import { orpc } from "@/lib/orpc/client";
export function UpdateCheckSection() { export function UpdateCheckSection() {
const { t } = useLingui(); const { t } = useLingui();
const { data, isPending: isLoading } = useQuery(orpc.admin.settings.get.queryOptions()); const { data, isPending: isLoading } = useQuery(orpc.admin.settings.get.queryOptions());
const [localEnabled, setLocalEnabled] = useState<boolean | null>(null); const currentEnabled = data?.updateCheck.enabled ?? true;
const currentEnabled = localEnabled ?? data?.updateCheck.enabled ?? true;
const [optimisticEnabled, setOptimisticEnabled] = useOptimistic(currentEnabled); const [optimisticEnabled, setOptimisticEnabled] = useOptimistic(currentEnabled);
const queryClient = useQueryClient();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const toggleMutation = useMutation(orpc.admin.settings.update.mutationOptions()); const toggleMutation = useMutation(orpc.admin.settings.update.mutationOptions());
@@ -31,7 +31,7 @@ export function UpdateCheckSection() {
setOptimisticEnabled(checked); setOptimisticEnabled(checked);
try { try {
await toggleMutation.mutateAsync({ updateCheck: { enabled: checked } }); await toggleMutation.mutateAsync({ updateCheck: { enabled: checked } });
setLocalEnabled(checked); await queryClient.invalidateQueries({ queryKey: orpc.admin.settings.get.key() });
toast.success(checked ? t`Update checks enabled` : t`Update checks disabled`); toast.success(checked ? t`Update checks enabled` : t`Update checks disabled`);
} catch { } catch {
toast.error(t`Failed to update setting`); toast.error(t`Failed to update setting`);
+1
View File
@@ -4,4 +4,5 @@ export const updateToastDismissedVersionAtom = atomWithStorage<string | null>(
"sofa:update-toast-dismissed-version", "sofa:update-toast-dismissed-version",
null, null,
createJSONStorage(() => sessionStorage), createJSONStorage(() => sessionStorage),
{ getOnInit: true },
); );
+7
View File
@@ -15,6 +15,13 @@ function normalizeErrors(errors: unknown[]): Array<{ message?: string }> {
return errors.map((e) => (typeof e === "string" ? { message: e } : (e as { message?: string }))); return errors.map((e) => (typeof e === "string" ? { message: e } : (e as { message?: string })));
} }
export function formatFieldErrors(errors: unknown[]): string {
return normalizeErrors(errors)
.map((e) => e.message ?? "")
.filter(Boolean)
.join(", ");
}
function TextField({ function TextField({
label, label,
description, description,