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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import type { TimerState } from "@issue/shared"; import type { TimerState } from "@issue/shared";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
import { timer } from "@/lib/server"; import { timer } from "@/lib/server";
import { formatTime } from "@/lib/utils"; import { formatTime } from "@/lib/utils";
@@ -24,9 +25,13 @@ export function TimerDisplay({ issueId }: { issueId: number }) {
setWorkTimeMs(data?.workTimeMs ?? 0); setWorkTimeMs(data?.workTimeMs ?? 0);
setError(null); setError(null);
}, },
onError: (message) => { onError: (error) => {
if (!isMounted) return; 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); setInactiveWorkTimeMs(totalWorkTime);
setError(null); setError(null);
}, },
onError: (message) => { onError: (error) => {
if (!isMounted) return; 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) => { onSuccess: (url) => {
onAvatarUploaded(url); onAvatarUploaded(url);
setUploading(false); setUploading(false);
},
onError: (msg) => {
setError(msg);
setUploading(false);
},
});
toast.success(`Avatar uploaded successfully`, { toast.success(
dismissible: false, <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,
});
},
}); });
}; };

View File

@@ -168,6 +168,12 @@ export default function App() {
}, },
onError: (error) => { onError: (error) => {
console.error("error fetching organisations:", error); console.error("error fetching organisations:", error);
setOrganisations([]);
setSelectedOrganisation(null);
toast.error(`Error fetching organisations: ${error}`, {
dismissible: false,
});
}, },
}); });
} catch (err) { } catch (err) {
@@ -240,6 +246,12 @@ export default function App() {
}, },
onError: (error) => { onError: (error) => {
console.error("error fetching projects:", error); console.error("error fetching projects:", error);
setProjects([]);
setSelectedProject(null);
toast.error(`Error fetching projects: ${error}`, {
dismissible: false,
});
}, },
}); });
} catch (err) { } catch (err) {
@@ -258,6 +270,10 @@ export default function App() {
onError: (error) => { onError: (error) => {
console.error("error fetching members:", error); console.error("error fetching members:", error);
setMembers([]); setMembers([]);
toast.error(`Error fetching members: ${error}`, {
dismissible: false,
});
}, },
}); });
} catch (err) { } catch (err) {
@@ -276,6 +292,10 @@ export default function App() {
onError: (error) => { onError: (error) => {
console.error("error fetching sprints:", error); console.error("error fetching sprints:", error);
setSprints([]); setSprints([]);
toast.error(`Error fetching sprints: ${error}`, {
dismissible: false,
});
}, },
}); });
} catch (err) { } catch (err) {
@@ -324,6 +344,11 @@ export default function App() {
onError: (error) => { onError: (error) => {
console.error("error fetching issues:", error); console.error("error fetching issues:", error);
setIssues([]); setIssues([]);
setSelectedIssue(null);
toast.error(`Error fetching issues: ${error}`, {
dismissible: false,
});
}, },
}); });
} catch (err) { } catch (err) {
@@ -413,9 +438,11 @@ export default function App() {
}} }}
onCreateProject={async (project) => { onCreateProject={async (project) => {
if (!selectedOrganisation) return; if (!selectedOrganisation) return;
toast.success(`Created Project ${project.name}`, { toast.success(`Created Project ${project.name}`, {
dismissible: false, dismissible: false,
}); });
await refetchProjects(selectedOrganisation.Organisation.id, { await refetchProjects(selectedOrganisation.Organisation.id, {
selectProjectId: project.id, selectProjectId: project.id,
}); });
@@ -440,6 +467,11 @@ export default function App() {
); );
await refetchIssues(); await refetchIssues();
}} }}
errorAction={async (errorMessage) => {
toast.error(`Error creating issue: ${errorMessage}`, {
dismissible: false,
});
}}
/> />
{isAdmin && ( {isAdmin && (
<CreateSprint <CreateSprint