toasts all over

This commit is contained in:
Oliver Bryan
2026-01-12 12:41:03 +00:00
parent 2b0bf94134
commit 0a931ca47c
12 changed files with 297 additions and 63 deletions

View File

@@ -50,14 +50,18 @@ function AccountDialog({ trigger }: { trigger?: ReactNode }) {
setUser(data);
setPassword("");
setOpen(false);
},
onError: (errorMessage) => {
setError(errorMessage);
},
});
toast.success(`Account updated successfully`, {
dismissible: false,
toast.success(`Account updated successfully`, {
dismissible: false,
});
},
onError: (error) => {
setError(error);
toast.error(`Error updating account: ${error}`, {
dismissible: false,
});
},
});
};

View File

@@ -1,5 +1,6 @@
import type { UserRecord } from "@issue/shared";
import { type FormEvent, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -67,9 +68,13 @@ export function AddMemberDialog({
userData = data;
userId = data.id;
},
onError: (err) => {
setError(err || "user not found");
onError: (error) => {
setError(error || "user not found");
setSubmitting(false);
toast.error(`Error adding member: ${error}`, {
dismissible: false,
});
},
});
@@ -90,9 +95,13 @@ export function AddMemberDialog({
console.error(actionErr);
}
},
onError: (err) => {
setError(err || "failed to add member");
onError: (error) => {
setError(error || "failed to add member");
setSubmitting(false);
toast.error(`Error adding member: ${error}`, {
dismissible: false,
});
},
});
} catch (err) {

View File

@@ -6,6 +6,7 @@ import {
} from "@issue/shared";
import { type FormEvent, useState } from "react";
import { toast } from "sonner";
import { useAuthenticatedSession } from "@/components/session-provider";
import { StatusSelect } from "@/components/status-select";
import StatusTag from "@/components/status-tag";
@@ -33,6 +34,7 @@ export function CreateIssue({
statuses,
trigger,
completeAction,
errorAction,
}: {
projectId?: number;
sprints?: SprintRecord[];
@@ -40,6 +42,7 @@ export function CreateIssue({
statuses: Record<string, string>;
trigger?: React.ReactNode;
completeAction?: (issueNumber: number) => void | Promise<void>;
errorAction?: (errorMessage: string) => void | Promise<void>;
}) {
const { user } = useAuthenticatedSession();
@@ -113,9 +116,13 @@ export function CreateIssue({
console.error(actionErr);
}
},
onError: (message) => {
setError(message);
onError: (error) => {
setError(error);
setSubmitting(false);
toast.error(`Error creating issue: ${error}`, {
dismissible: false,
});
},
});
} catch (err) {

View File

@@ -31,9 +31,11 @@ const slugify = (value: string) =>
export function CreateOrganisation({
trigger,
completeAction,
errorAction,
}: {
trigger?: React.ReactNode;
completeAction?: (org: OrganisationRecord) => void | Promise<void>;
errorAction?: (errorMessage: string) => void | Promise<void>;
}) {
const { user } = useAuthenticatedSession();
@@ -93,9 +95,14 @@ export function CreateOrganisation({
console.error(actionErr);
}
},
onError: (err) => {
onError: async (err) => {
setError(err || "failed to create organisation");
setSubmitting(false);
try {
await errorAction?.(err || "failed to create organisation");
} catch (actionErr) {
console.error(actionErr);
}
},
});
} catch (err) {

View File

@@ -1,5 +1,6 @@
import { PROJECT_NAME_MAX_LENGTH, type ProjectRecord } from "@issue/shared";
import { type FormEvent, useState } from "react";
import { toast } from "sonner";
import { useAuthenticatedSession } from "@/components/session-provider";
import { Button } from "@/components/ui/button";
import {
@@ -98,9 +99,13 @@ export function CreateProject({
console.error(actionErr);
}
},
onError: (message) => {
setError(message);
onError: (error) => {
setError(error);
setSubmitting(false);
toast.error(`Error creating project: ${error}`, {
dismissible: false,
});
},
});
} catch (err) {

View File

@@ -1,5 +1,6 @@
import { DEFAULT_SPRINT_COLOUR, type SprintRecord } from "@issue/shared";
import { type FormEvent, useMemo, useState } from "react";
import { toast } from "sonner";
import { useAuthenticatedSession } from "@/components/session-provider";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
@@ -134,9 +135,13 @@ export function CreateSprint({
console.error(actionErr);
}
},
onError: (message) => {
setError(message);
onError: (error) => {
setError(error);
setSubmitting(false);
toast.error(`Error creating sprint: ${error}`, {
dismissible: false,
});
},
});
} catch (submitError) {

View File

@@ -14,6 +14,7 @@ import { SelectTrigger } from "@/components/ui/select";
import { UserSelect } from "@/components/user-select";
import { issue } from "@/lib/server";
import { issueID } from "@/lib/utils";
import SmallSprintDisplay from "./small-sprint-display";
import { SprintSelect } from "./sprint-select";
export function IssueDetailPane({
@@ -68,10 +69,40 @@ export function IssueDetailPane({
sprintId: newSprintId,
onSuccess: () => {
onIssueUpdate?.();
toast.success(
<>
Successfully updated sprint to{" "}
{value === "unassigned" ? (
"Unassigned"
) : (
<SmallSprintDisplay sprint={sprints.find((s) => s.id === newSprintId)} />
)}{" "}
for {issueID(project.Project.key, issueData.Issue.number)}
</>,
{
dismissible: false,
},
);
},
onError: (error) => {
console.error("error updating sprint:", error);
setSprintId(issueData.Issue.sprintId?.toString() ?? "unassigned");
toast.error(
<>
Error updating sprint to{" "}
{value === "unassigned" ? (
"Unassigned"
) : (
<SmallSprintDisplay sprint={sprints.find((s) => s.id === newSprintId)} />
)}{" "}
for {issueID(project.Project.key, issueData.Issue.number)}
</>,
{
dismissible: false,
},
);
},
});
};
@@ -96,6 +127,10 @@ export function IssueDetailPane({
onError: (error) => {
console.error("error updating assignee:", error);
setAssigneeId(issueData.Issue.assigneeId?.toString() ?? "unassigned");
toast.error(`Error updating assignee: ${error}`, {
dismissible: false,
});
},
});
};
@@ -119,6 +154,10 @@ export function IssueDetailPane({
onError: (error) => {
console.error("error updating status:", error);
setStatus(issueData.Issue.status);
toast.error(`Error updating status: ${error}`, {
dismissible: false,
});
},
});
};
@@ -148,9 +187,23 @@ export function IssueDetailPane({
issueId: issueData.Issue.id,
onSuccess: async () => {
await onIssueDelete?.(issueData.Issue.id);
toast.success(`Deleted issue ${issueID(project.Project.key, issueData.Issue.number)}`, {
dismissible: false,
});
},
onError: (error) => {
console.error("error deleting issue:", error);
console.error(
`error deleting issue ${issueID(project.Project.key, issueData.Issue.number)}`,
error,
);
toast.error(
`Error deleting issue ${issueID(project.Project.key, issueData.Issue.number)}: ${error}`,
{
dismissible: false,
},
);
},
});
setDeleteOpen(false);

View File

@@ -1,5 +1,6 @@
import type { OrganisationRecord, OrganisationResponse } from "@issue/shared";
import { useState } from "react";
import { toast } from "sonner";
import { CreateOrganisation } from "@/components/create-organisation";
import { Button } from "@/components/ui/button";
import {
@@ -86,6 +87,11 @@ export function OrganisationSelect({
console.error(err);
}
}}
errorAction={async (errorMessage) => {
toast.error(`Error creating organisation: ${errorMessage}`, {
dismissible: false,
});
}}
/>
</SelectContent>
</Select>

View File

@@ -98,6 +98,10 @@ function OrganisationsDialog({
onError: (error) => {
console.error(error);
setMembers([]);
toast.error(`Error fetching members: ${error}`, {
dismissible: false,
});
},
});
} catch (err) {
@@ -125,15 +129,23 @@ function OrganisationsDialog({
role: newRole,
onSuccess: () => {
closeConfirmDialog();
toast.success(`${capitalise(action)}d ${memberName} to ${newRole} successfully`, {
dismissible: false,
});
void refetchMembers();
},
onError: (error) => {
console.error(error);
},
});
toast.success(`${capitalise(action)}d ${memberName} to ${newRole} successfully`, {
dismissible: false,
toast.error(
`Error ${action.slice(0, -1)}ing ${memberName} to ${newRole}: ${error}`,
{
dismissible: false,
},
);
},
});
} catch (err) {
console.error(err);
@@ -162,19 +174,27 @@ function OrganisationsDialog({
userId: memberUserId,
onSuccess: () => {
closeConfirmDialog();
toast.success(
`Removed ${memberName} from ${selectedOrganisation.Organisation.name} successfully`,
{
dismissible: false,
},
);
void refetchMembers();
},
onError: (error) => {
console.error(error);
toast.error(
`Error removing member from ${selectedOrganisation.Organisation.name}: ${error}`,
{
dismissible: false,
},
);
},
});
toast.success(
`Removed ${memberName} from ${selectedOrganisation.Organisation.name} successfully`,
{
dismissible: false,
},
);
} catch (err) {
console.error(err);
}
@@ -191,6 +211,8 @@ function OrganisationsDialog({
const updateStatuses = async (
newStatuses: Record<string, string>,
statusRemoved?: { name: string; colour: string },
statusAdded?: { name: string; colour: string },
statusMoved?: { name: string; colour: string; currentIndex: number; nextIndex: number },
) => {
if (!selectedOrganisation) return;
@@ -200,7 +222,17 @@ function OrganisationsDialog({
statuses: newStatuses,
onSuccess: () => {
setStatuses(newStatuses);
if (statusRemoved) {
if (statusAdded) {
toast.success(
<>
Created <StatusTag status={statusAdded.name} colour={statusAdded.colour} />{" "}
status successfully
</>,
{
dismissible: false,
},
);
} else if (statusRemoved) {
toast.success(
<>
Removed{" "}
@@ -211,11 +243,58 @@ function OrganisationsDialog({
dismissible: false,
},
);
} else if (statusMoved) {
toast.success(
<>
Moved <StatusTag status={statusMoved.name} colour={statusMoved.colour} /> from
position {statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1}{" "}
successfully
</>,
{
dismissible: false,
},
);
}
void refetchOrganisations();
},
onError: (error) => {
console.error("error updating statuses:", error);
if (statusAdded) {
toast.error(
<>
Error adding{" "}
<StatusTag status={statusAdded.name} colour={statusAdded.colour} /> to{" "}
{selectedOrganisation.Organisation.name}: {error}
</>,
{
dismissible: false,
},
);
} else if (statusRemoved) {
toast.error(
<>
Error removing{" "}
<StatusTag status={statusRemoved.name} colour={statusRemoved.colour} /> from{" "}
{selectedOrganisation.Organisation.name}: {error}
</>,
{
dismissible: false,
},
);
} else if (statusMoved) {
toast.error(
<>
Error moving{" "}
<StatusTag status={statusMoved.name} colour={statusMoved.colour} />
from position {statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1}{" "}
{selectedOrganisation.Organisation.name}: {error}
</>,
{
dismissible: false,
},
);
}
},
});
} catch (err) {
@@ -240,17 +319,7 @@ function OrganisationsDialog({
}
const newStatuses = { ...statuses };
newStatuses[trimmed] = newStatusColour;
await updateStatuses(newStatuses);
toast.success(
<>
Created <StatusTag status={newStatusName.trim()} colour={newStatusColour} /> status
successfully
</>,
{
dismissible: false,
},
);
await updateStatuses(newStatuses, undefined, { name: trimmed, colour: newStatusColour });
setNewStatusName("");
setNewStatusColour(DEFAULT_STATUS_COLOUR);
@@ -282,6 +351,16 @@ function OrganisationsDialog({
},
onError: (error) => {
console.error("error checking status usage:", error);
toast.error(
<>
Error checking status usage for{" "}
<StatusTag status={status} colour={statuses[status]} />: {error}
</>,
{
dismissible: false,
},
);
},
});
} catch (err) {
@@ -301,15 +380,11 @@ function OrganisationsDialog({
nextStatuses[currentIndex],
];
await updateStatuses(Object.fromEntries(nextStatuses.map((status) => [status, statuses[status]])));
toast.success(
<>
Moved <StatusTag status={status} colour={statuses[status]} /> from position {currentIndex + 1}{" "}
to {nextIndex + 1} successfully
</>,
{
dismissible: false,
},
await updateStatuses(
Object.fromEntries(nextStatuses.map((status) => [status, statuses[status]])),
undefined,
undefined,
{ name: status, colour: statuses[status], currentIndex, nextIndex },
);
};
@@ -331,6 +406,17 @@ function OrganisationsDialog({
},
onError: (error) => {
console.error("error replacing status:", error);
toast.error(
<>
Error removing <StatusTag status={statusToRemove} colour={statuses[statusToRemove]} />{" "}
from
{selectedOrganisation.Organisation.name}: {error}{" "}
</>,
{
dismissible: false,
},
);
},
});
};
@@ -482,6 +568,7 @@ function OrganisationsDialog({
dismissible: false,
},
);
refetchMembers();
}}
trigger={

View File

@@ -1,5 +1,6 @@
import type { TimerState } from "@issue/shared";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { timer } from "@/lib/server";
import { formatTime } from "@/lib/utils";
@@ -24,9 +25,13 @@ export function TimerDisplay({ issueId }: { issueId: number }) {
setWorkTimeMs(data?.workTimeMs ?? 0);
setError(null);
},
onError: (message) => {
onError: (error) => {
if (!isMounted) return;
setError(message);
setError(error);
toast.error(`Error fetching timer data: ${error}`, {
dismissible: false,
});
},
});
@@ -42,9 +47,13 @@ export function TimerDisplay({ issueId }: { issueId: number }) {
setInactiveWorkTimeMs(totalWorkTime);
setError(null);
},
onError: (message) => {
onError: (error) => {
if (!isMounted) return;
setError(message);
setError(error);
toast.error(`Error fetching timer data: ${error}`, {
dismissible: false,
});
},
});
};

View File

@@ -38,15 +38,25 @@ export function UploadAvatar({
onSuccess: (url) => {
onAvatarUploaded(url);
setUploading(false);
},
onError: (msg) => {
setError(msg);
setUploading(false);
},
});
toast.success(`Avatar uploaded successfully`, {
dismissible: false,
toast.success(
<div className="flex flex-col items-center gap-4">
<img src={url} alt="Avatar" className="w-32 h-32" />
Avatar uploaded successfully
</div>,
{
dismissible: false,
},
);
},
onError: (error) => {
setError(error);
setUploading(false);
toast.error(`Error uploading avatar: ${error}`, {
dismissible: false,
});
},
});
};