mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 01:45:52 +02:00
* fix(views): surface backend error messages on mutation failures (MUL-2317)
Mutation toasts across the views package were swallowing the backend
`error` string and showing only a generic i18n fallback. This made it
impossible for users to see why an operation failed (most visibly:
creating an issue with a duplicate title produced a vague "Failed to
create issue" toast).
The fix has three pieces:
1. Create-issue duplicate branch (A段)
- New schema `DuplicateIssueErrorBodySchema` in core/api/schemas.ts.
- `create-issue.tsx` parses `ApiError.body` via `parseWithFallback`
and renders a dedicated amber-toned toast with a "view existing"
link when the server returns `{ code: "active_duplicate_issue",
issue: {...} }`. Schema drift downgrades to the normal error toast.
- Schema intentionally omits `issue.status` so the toast does not
depend on `StatusIcon`, which has no fallback for unknown enums.
2. User-facing mutation failure toasts (B段)
- 47 sites converted to `err instanceof Error && err.message ?
err.message : <existing fallback>` — preserves all existing
code-specific branches (slug conflict, agent_unavailable,
daemon_version_unsupported) and i18n keys.
- Covers Type 1 (onError) and Type 2 (catch block) patterns across
issues, projects, autopilots, inbox, runtimes, squads, comments,
batch actions, workspace create, and agent config tabs.
3. Autopilot partial-success (Type 3)
- New i18n keys `toast_create_partial_with_reason` /
`toast_update_partial_with_reason` (double-brace `{{reason}}`).
- `autopilot-dialog.tsx` captures `err.message` in the schedule
`catch` and routes to the `_with_reason` variant when present,
preserving the partial-success semantic (autopilot saved, schedule
failed) while exposing the actual reason.
Explicitly out of scope:
- `packages/core/` mutation hooks (no global onError, no UI dependency)
- No `toastApiError` helper (matches existing 14+ correct sites)
- Sub-issue link aggregate `Promise.allSettled` keeps count-based toast
(N independent requests cannot collapse to one err.message); only
added a dev-side `console.error` per rejection.
- Clipboard catches and `useUpdateChatSession` (not API mutation toasts)
Tests:
- `packages/core/api/schemas.test.ts` — schema contract (valid body,
forward-compat fields, rename rejection, missing issue, wrong types).
- `packages/views/modals/create-issue.test.tsx` — duplicate toast +
view link, schema-drift fallback, err.message surfacing, non-Error
fallback (4 new cases).
- `packages/views/autopilots/components/autopilot-dialog-i18n.test.ts`
— real i18next, asserts rendered text contains the reason verbatim
(guards against `{reason}` vs `{{reason}}` regression).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilots): unify rotate-token catch + cover dialog partial-success render
Address reviewer feedback on PR #2772:
1. webhook-token rotate (`autopilot-detail-page.tsx`) now follows the
`err.message ?? fallback` ternary used by the sibling trigger
delete/add paths, instead of swallowing the error.
2. Extract `formatSchedulePartialFailureToast` so the dialog's
partial-success branches and the i18n test exercise the same
helper. The test now drives the actual format function, so a
variable-name typo at the call site (e.g. `{ msg }` instead of
`{ reason }`) fails the substring assertion.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(modals): drop user.type for title in success path to dodge CI 5s timeout
The success-path test typed the 42-character title via userEvent which
triggers a controlled re-render per keystroke. On the slower CI runner
the whole test crept up to ~5s and intermittently tripped the default
vitest timeout. Setting the value in one shot via fireEvent.change cuts
the cost while leaving the submit + toast interactions on userEvent.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
117 lines
4.0 KiB
TypeScript
117 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useWorkspaceId } from "@multica/core/hooks";
|
|
import { notificationPreferenceOptions } from "@multica/core/notification-preferences/queries";
|
|
import { useUpdateNotificationPreferences } from "@multica/core/notification-preferences/mutations";
|
|
import type { NotificationGroupKey, NotificationPreferences } from "@multica/core/types";
|
|
import { Card, CardContent } from "@multica/ui/components/ui/card";
|
|
import { Switch } from "@multica/ui/components/ui/switch";
|
|
import { toast } from "sonner";
|
|
import { useT } from "../../i18n";
|
|
|
|
// Inbox event groups rendered in the per-event toggle list. `system_notifications`
|
|
// is a sibling preference key but lives in its own section below.
|
|
const INBOX_GROUP_KEYS = [
|
|
"assignments",
|
|
"status_changes",
|
|
"comments",
|
|
"updates",
|
|
"agent_activity",
|
|
] as const;
|
|
type InboxGroupKey = (typeof INBOX_GROUP_KEYS)[number];
|
|
|
|
export function NotificationsTab() {
|
|
const { t } = useT("settings");
|
|
const wsId = useWorkspaceId();
|
|
const { data } = useQuery(notificationPreferenceOptions(wsId));
|
|
const mutation = useUpdateNotificationPreferences();
|
|
|
|
const preferences = data?.preferences ?? {};
|
|
|
|
const handleToggle = (key: NotificationGroupKey, enabled: boolean) => {
|
|
const updated: NotificationPreferences = {
|
|
...preferences,
|
|
[key]: enabled ? "all" : "muted",
|
|
};
|
|
// Remove keys set to "all" (default) to keep the object clean
|
|
if (enabled) {
|
|
delete updated[key];
|
|
}
|
|
mutation.mutate(updated, {
|
|
onError: (err) =>
|
|
toast.error(
|
|
err instanceof Error && err.message
|
|
? err.message
|
|
: t(($) => $.notifications.toast_failed),
|
|
),
|
|
});
|
|
};
|
|
|
|
const systemEnabled = preferences.system_notifications !== "muted";
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<section className="space-y-4">
|
|
<div>
|
|
<h2 className="text-sm font-semibold">{t(($) => $.notifications.title)}</h2>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{t(($) => $.notifications.description)}
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardContent className="divide-y">
|
|
{INBOX_GROUP_KEYS.map((key: InboxGroupKey) => {
|
|
const enabled = preferences[key] !== "muted";
|
|
return (
|
|
<div
|
|
key={key}
|
|
className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
|
|
>
|
|
<div className="space-y-0.5 pr-4">
|
|
<p className="text-sm font-medium">{t(($) => $.notifications.groups[key].label)}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t(($) => $.notifications.groups[key].description)}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={enabled}
|
|
onCheckedChange={(checked) => handleToggle(key, checked)}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
|
|
<section className="space-y-4">
|
|
<div>
|
|
<h2 className="text-sm font-semibold">{t(($) => $.notifications.system.title)}</h2>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{t(($) => $.notifications.system.description)}
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5 pr-4">
|
|
<p className="text-sm font-medium">{t(($) => $.notifications.system.label)}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t(($) => $.notifications.system.hint)}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={systemEnabled}
|
|
onCheckedChange={(checked) => handleToggle("system_notifications", checked)}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|