mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
full sonner implementation
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -53,12 +53,14 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.561.0",
|
"lucide-react": "^0.561.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-day-picker": "^9.13.0",
|
"react-day-picker": "^9.13.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-resizable-panels": "^4.0.15",
|
"react-resizable-panels": "^4.0.15",
|
||||||
"react-router-dom": "^7.10.1",
|
"react-router-dom": "^7.10.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
},
|
},
|
||||||
@@ -617,6 +619,8 @@
|
|||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
|
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
|
||||||
|
|
||||||
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||||
@@ -695,6 +699,8 @@
|
|||||||
|
|
||||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||||
|
|
||||||
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|||||||
@@ -25,12 +25,14 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.561.0",
|
"lucide-react": "^0.561.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-day-picker": "^9.13.0",
|
"react-day-picker": "^9.13.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-resizable-panels": "^4.0.15",
|
"react-resizable-panels": "^4.0.15",
|
||||||
"react-router-dom": "^7.10.1",
|
"react-router-dom": "^7.10.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.1.18"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -214,3 +214,13 @@
|
|||||||
height: 16px !important;
|
height: 16px !important;
|
||||||
border: 1px solid white !important;
|
border: 1px solid white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-sonner-toast] {
|
||||||
|
transition: none !important;
|
||||||
|
padding: 10px 15px 10px 15px !important;
|
||||||
|
width: max-content !important;
|
||||||
|
max-width: 90vw !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
@@ -54,6 +55,10 @@ function AccountDialog({ trigger }: { trigger?: ReactNode }) {
|
|||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toast.success(`Account updated successfully`, {
|
||||||
|
dismissible: false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { UserRecord } from "@issue/shared";
|
||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +21,7 @@ export function AddMemberDialog({
|
|||||||
organisationId: number;
|
organisationId: number;
|
||||||
existingMembers: string[];
|
existingMembers: string[];
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
onSuccess?: () => void | Promise<void>;
|
onSuccess?: (user: UserRecord) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
@@ -59,10 +60,12 @@ export function AddMemberDialog({
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
let userId: number | null = null;
|
let userId: number | null = null;
|
||||||
|
let userData: UserRecord;
|
||||||
await user.byUsername({
|
await user.byUsername({
|
||||||
username,
|
username,
|
||||||
onSuccess: (userData) => {
|
onSuccess: (data: UserRecord) => {
|
||||||
userId = userData.id;
|
userData = data;
|
||||||
|
userId = data.id;
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
setError(err || "user not found");
|
setError(err || "user not found");
|
||||||
@@ -82,7 +85,7 @@ export function AddMemberDialog({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
reset();
|
reset();
|
||||||
try {
|
try {
|
||||||
await onSuccess?.();
|
await onSuccess?.(userData);
|
||||||
} catch (actionErr) {
|
} catch (actionErr) {
|
||||||
console.error(actionErr);
|
console.error(actionErr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function CreateIssue({
|
|||||||
members?: UserRecord[];
|
members?: UserRecord[];
|
||||||
statuses: Record<string, string>;
|
statuses: Record<string, string>;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
completeAction?: (issueId: number) => void | Promise<void>;
|
completeAction?: (issueNumber: number) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { user } = useAuthenticatedSession();
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ export function CreateIssue({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
reset();
|
reset();
|
||||||
try {
|
try {
|
||||||
await completeAction?.(data.id);
|
await completeAction?.(data.number);
|
||||||
} catch (actionErr) {
|
} catch (actionErr) {
|
||||||
console.error(actionErr);
|
console.error(actionErr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { ORG_DESCRIPTION_MAX_LENGTH, ORG_NAME_MAX_LENGTH, ORG_SLUG_MAX_LENGTH } from "@issue/shared";
|
import {
|
||||||
|
ORG_DESCRIPTION_MAX_LENGTH,
|
||||||
|
ORG_NAME_MAX_LENGTH,
|
||||||
|
ORG_SLUG_MAX_LENGTH,
|
||||||
|
type OrganisationRecord,
|
||||||
|
} from "@issue/shared";
|
||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useState } from "react";
|
||||||
import { useAuthenticatedSession } from "@/components/session-provider";
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -28,7 +33,7 @@ export function CreateOrganisation({
|
|||||||
completeAction,
|
completeAction,
|
||||||
}: {
|
}: {
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
completeAction?: (organisationId: number) => void | Promise<void>;
|
completeAction?: (org: OrganisationRecord) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { user } = useAuthenticatedSession();
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
@@ -83,7 +88,7 @@ export function CreateOrganisation({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
reset();
|
reset();
|
||||||
try {
|
try {
|
||||||
await completeAction?.(data.id);
|
await completeAction?.(data);
|
||||||
} catch (actionErr) {
|
} catch (actionErr) {
|
||||||
console.error(actionErr);
|
console.error(actionErr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function CreateProject({
|
|||||||
}: {
|
}: {
|
||||||
organisationId?: number;
|
organisationId?: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
completeAction?: (projectId: number) => void | Promise<void>;
|
completeAction?: (project: ProjectRecord) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { user } = useAuthenticatedSession();
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ export function CreateProject({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
reset();
|
reset();
|
||||||
try {
|
try {
|
||||||
await completeAction?.(project.id);
|
await completeAction?.(project);
|
||||||
} catch (actionErr) {
|
} catch (actionErr) {
|
||||||
console.error(actionErr);
|
console.error(actionErr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_SPRINT_COLOUR } 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 { useAuthenticatedSession } from "@/components/session-provider";
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -53,7 +53,7 @@ export function CreateSprint({
|
|||||||
}: {
|
}: {
|
||||||
projectId?: number;
|
projectId?: number;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
completeAction?: () => void | Promise<void>;
|
completeAction?: (sprint: SprintRecord) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { user } = useAuthenticatedSession();
|
const { user } = useAuthenticatedSession();
|
||||||
|
|
||||||
@@ -122,14 +122,14 @@ export function CreateSprint({
|
|||||||
await sprint.create({
|
await sprint.create({
|
||||||
projectId,
|
projectId,
|
||||||
name,
|
name,
|
||||||
color: colour, // hm - always unsure which i should use
|
color: colour, // hm - always unsure which i should use
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
onSuccess: async () => {
|
onSuccess: async (data) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
reset();
|
reset();
|
||||||
try {
|
try {
|
||||||
await completeAction?.();
|
await completeAction?.(data);
|
||||||
} catch (actionErr) {
|
} catch (actionErr) {
|
||||||
console.error(actionErr);
|
console.error(actionErr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { IssueResponse, ProjectResponse, SprintRecord, UserRecord } from "@issue/shared";
|
import type { IssueResponse, ProjectResponse, SprintRecord, UserRecord } from "@issue/shared";
|
||||||
import { Check, Link, Trash, X } from "lucide-react";
|
import { Check, Link, Trash, X } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { useSession } from "@/components/session-provider";
|
import { useSession } from "@/components/session-provider";
|
||||||
import SmallUserDisplay from "@/components/small-user-display";
|
import SmallUserDisplay from "@/components/small-user-display";
|
||||||
import { StatusSelect } from "@/components/status-select";
|
import { StatusSelect } from "@/components/status-select";
|
||||||
@@ -83,6 +84,13 @@ export function IssueDetailPane({
|
|||||||
issueId: issueData.Issue.id,
|
issueId: issueData.Issue.id,
|
||||||
assigneeId: newAssigneeId,
|
assigneeId: newAssigneeId,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
const user = members.find((member) => member.id === newAssigneeId);
|
||||||
|
toast.success(
|
||||||
|
`Assigned ${user?.name} to ${issueID(project.Project.key, issueData.Issue.number)}`,
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
onIssueUpdate?.();
|
onIssueUpdate?.();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -99,6 +107,13 @@ export function IssueDetailPane({
|
|||||||
issueId: issueData.Issue.id,
|
issueId: issueData.Issue.id,
|
||||||
status: value,
|
status: value,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success(
|
||||||
|
<>
|
||||||
|
{issueID(project.Project.key, issueData.Issue.number)}'s status updated to{" "}
|
||||||
|
<StatusTag status={value} colour={statuses[value]} />
|
||||||
|
</>,
|
||||||
|
{ dismissible: false },
|
||||||
|
);
|
||||||
onIssueUpdate?.();
|
onIssueUpdate?.();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { OrganisationResponse } from "@issue/shared";
|
import type { OrganisationRecord, OrganisationResponse } from "@issue/shared";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { CreateOrganisation } from "@/components/create-organisation";
|
import { CreateOrganisation } from "@/components/create-organisation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -27,7 +27,7 @@ export function OrganisationSelect({
|
|||||||
organisations: OrganisationResponse[];
|
organisations: OrganisationResponse[];
|
||||||
selectedOrganisation: OrganisationResponse | null;
|
selectedOrganisation: OrganisationResponse | null;
|
||||||
onSelectedOrganisationChange: (organisation: OrganisationResponse | null) => void;
|
onSelectedOrganisationChange: (organisation: OrganisationResponse | null) => void;
|
||||||
onCreateOrganisation?: (organisationId: number) => void | Promise<void>;
|
onCreateOrganisation?: (org: OrganisationRecord) => void | Promise<void>;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
contentClass?: string;
|
contentClass?: string;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
@@ -79,9 +79,9 @@ export function OrganisationSelect({
|
|||||||
Create Organisation
|
Create Organisation
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
completeAction={async (organisationId) => {
|
completeAction={async (org) => {
|
||||||
try {
|
try {
|
||||||
await onCreateOrganisation?.(organisationId);
|
await onCreateOrganisation?.(org);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { ChevronDown, ChevronUp, EllipsisVertical, Plus, X } from "lucide-react";
|
import { ChevronDown, ChevronUp, EllipsisVertical, Plus, X } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { AddMemberDialog } from "@/components/add-member-dialog";
|
import { AddMemberDialog } from "@/components/add-member-dialog";
|
||||||
import { OrganisationSelect } from "@/components/organisation-select";
|
import { OrganisationSelect } from "@/components/organisation-select";
|
||||||
import { useAuthenticatedSession } from "@/components/session-provider";
|
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||||
@@ -26,6 +27,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { issue, organisation } from "@/lib/server";
|
import { issue, organisation } from "@/lib/server";
|
||||||
|
import { capitalise } from "@/lib/utils";
|
||||||
|
|
||||||
function OrganisationsDialog({
|
function OrganisationsDialog({
|
||||||
trigger,
|
trigger,
|
||||||
@@ -129,6 +131,10 @@ function OrganisationsDialog({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toast.success(`${capitalise(action)}d ${memberName} to ${newRole} successfully`, {
|
||||||
|
dismissible: false,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
@@ -162,6 +168,13 @@ function OrganisationsDialog({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`Removed ${memberName} from ${selectedOrganisation.Organisation.name} successfully`,
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
@@ -175,7 +188,10 @@ function OrganisationsDialog({
|
|||||||
}
|
}
|
||||||
}, [selectedOrganisation]);
|
}, [selectedOrganisation]);
|
||||||
|
|
||||||
const updateStatuses = async (newStatuses: Record<string, string>) => {
|
const updateStatuses = async (
|
||||||
|
newStatuses: Record<string, string>,
|
||||||
|
statusRemoved?: { name: string; colour: string },
|
||||||
|
) => {
|
||||||
if (!selectedOrganisation) return;
|
if (!selectedOrganisation) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -184,6 +200,18 @@ function OrganisationsDialog({
|
|||||||
statuses: newStatuses,
|
statuses: newStatuses,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setStatuses(newStatuses);
|
setStatuses(newStatuses);
|
||||||
|
if (statusRemoved) {
|
||||||
|
toast.success(
|
||||||
|
<>
|
||||||
|
Removed{" "}
|
||||||
|
<StatusTag status={statusRemoved.name} colour={statusRemoved.colour} /> status
|
||||||
|
successfully
|
||||||
|
</>,
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
void refetchOrganisations();
|
void refetchOrganisations();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -213,6 +241,17 @@ function OrganisationsDialog({
|
|||||||
const newStatuses = { ...statuses };
|
const newStatuses = { ...statuses };
|
||||||
newStatuses[trimmed] = newStatusColour;
|
newStatuses[trimmed] = newStatusColour;
|
||||||
await updateStatuses(newStatuses);
|
await updateStatuses(newStatuses);
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
<>
|
||||||
|
Created <StatusTag status={newStatusName.trim()} colour={newStatusColour} /> status
|
||||||
|
successfully
|
||||||
|
</>,
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
setNewStatusName("");
|
setNewStatusName("");
|
||||||
setNewStatusColour(DEFAULT_STATUS_COLOUR);
|
setNewStatusColour(DEFAULT_STATUS_COLOUR);
|
||||||
setIsCreatingStatus(false);
|
setIsCreatingStatus(false);
|
||||||
@@ -238,6 +277,7 @@ function OrganisationsDialog({
|
|||||||
const nextStatuses = Object.keys(statuses).filter((s) => s !== status);
|
const nextStatuses = Object.keys(statuses).filter((s) => s !== status);
|
||||||
void updateStatuses(
|
void updateStatuses(
|
||||||
Object.fromEntries(nextStatuses.map((statusKey) => [statusKey, statuses[statusKey]])),
|
Object.fromEntries(nextStatuses.map((statusKey) => [statusKey, statuses[statusKey]])),
|
||||||
|
{ name: status, colour: statuses[status] },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -262,6 +302,15 @@ function OrganisationsDialog({
|
|||||||
];
|
];
|
||||||
|
|
||||||
await updateStatuses(Object.fromEntries(nextStatuses.map((status) => [status, statuses[status]])));
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmRemoveStatus = async () => {
|
const confirmRemoveStatus = async () => {
|
||||||
@@ -275,6 +324,7 @@ function OrganisationsDialog({
|
|||||||
const newStatuses = Object.keys(statuses).filter((s) => s !== statusToRemove);
|
const newStatuses = Object.keys(statuses).filter((s) => s !== statusToRemove);
|
||||||
await updateStatuses(
|
await updateStatuses(
|
||||||
Object.fromEntries(newStatuses.map((status) => [status, statuses[status]])),
|
Object.fromEntries(newStatuses.map((status) => [status, statuses[status]])),
|
||||||
|
{ name: statusToRemove, colour: statuses[statusToRemove] },
|
||||||
);
|
);
|
||||||
setStatusToRemove(null);
|
setStatusToRemove(null);
|
||||||
setReassignToStatus("");
|
setReassignToStatus("");
|
||||||
@@ -319,8 +369,11 @@ function OrganisationsDialog({
|
|||||||
`${org?.Organisation.id}`,
|
`${org?.Organisation.id}`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onCreateOrganisation={async (organisationId) => {
|
onCreateOrganisation={async (org) => {
|
||||||
await refetchOrganisations({ selectOrganisationId: organisationId });
|
toast.success(`Created Organisation ${org.name}`, {
|
||||||
|
dismissible: false,
|
||||||
|
});
|
||||||
|
await refetchOrganisations({ selectOrganisationId: org.id });
|
||||||
}}
|
}}
|
||||||
contentClass={
|
contentClass={
|
||||||
"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"
|
"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"
|
||||||
@@ -422,7 +475,15 @@ function OrganisationsDialog({
|
|||||||
<AddMemberDialog
|
<AddMemberDialog
|
||||||
organisationId={selectedOrganisation.Organisation.id}
|
organisationId={selectedOrganisation.Organisation.id}
|
||||||
existingMembers={members.map((m) => m.User.username)}
|
existingMembers={members.map((m) => m.User.username)}
|
||||||
onSuccess={refetchMembers}
|
onSuccess={(user) => {
|
||||||
|
toast.success(
|
||||||
|
`${user.name} added to ${selectedOrganisation.Organisation.name} successfully`,
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
refetchMembers();
|
||||||
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
Add user <Plus className="size-4" />
|
Add user <Plus className="size-4" />
|
||||||
@@ -579,8 +640,11 @@ function OrganisationsDialog({
|
|||||||
setSelectedOrganisation(org);
|
setSelectedOrganisation(org);
|
||||||
localStorage.setItem("selectedOrganisationId", `${org?.Organisation.id}`);
|
localStorage.setItem("selectedOrganisationId", `${org?.Organisation.id}`);
|
||||||
}}
|
}}
|
||||||
onCreateOrganisation={async (organisationId) => {
|
onCreateOrganisation={async (org) => {
|
||||||
await refetchOrganisations({ selectOrganisationId: organisationId });
|
toast.success(`Created Organisation ${org.name}`, {
|
||||||
|
dismissible: false,
|
||||||
|
});
|
||||||
|
await refetchOrganisations({ selectOrganisationId: org.id });
|
||||||
}}
|
}}
|
||||||
contentClass={
|
contentClass={
|
||||||
"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"
|
"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ProjectResponse } from "@issue/shared";
|
import type { ProjectRecord, ProjectResponse } from "@issue/shared";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { CreateProject } from "@/components/create-project";
|
import { CreateProject } from "@/components/create-project";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -28,7 +28,7 @@ export function ProjectSelect({
|
|||||||
selectedProject: ProjectResponse | null;
|
selectedProject: ProjectResponse | null;
|
||||||
organisationId: number | undefined;
|
organisationId: number | undefined;
|
||||||
onSelectedProjectChange: (project: ProjectResponse | null) => void;
|
onSelectedProjectChange: (project: ProjectResponse | null) => void;
|
||||||
onCreateProject?: (projectId: number) => void | Promise<void>;
|
onCreateProject?: (project: ProjectRecord) => void | Promise<void>;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -75,9 +75,9 @@ export function ProjectSelect({
|
|||||||
Create Project
|
Create Project
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
completeAction={async (projectId) => {
|
completeAction={async (project) => {
|
||||||
try {
|
try {
|
||||||
await onCreateProject?.(projectId);
|
await onCreateProject?.(project);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|||||||
32
packages/frontend/src/components/ui/sonner.tsx
Normal file
32
packages/frontend/src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon } from "lucide-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
icons={{
|
||||||
|
success: <CircleCheckIcon className="size-4" />,
|
||||||
|
info: <InfoIcon className="size-4" />,
|
||||||
|
warning: <TriangleAlertIcon className="size-4" />,
|
||||||
|
error: <OctagonXIcon className="size-4" />,
|
||||||
|
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
"--border-radius": "0",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Toaster };
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Edit } from "lucide-react";
|
import { Edit } from "lucide-react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -43,6 +44,10 @@ export function UploadAvatar({
|
|||||||
setUploading(false);
|
setUploading(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toast.success(`Avatar uploaded successfully`, {
|
||||||
|
dismissible: false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ReactDOM from "react-dom/client";
|
|||||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
import { RequireAuth, SessionProvider } from "@/components/session-provider";
|
import { RequireAuth, SessionProvider } from "@/components/session-provider";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import App from "@/pages/App";
|
import App from "@/pages/App";
|
||||||
import Font from "@/pages/Font";
|
import Font from "@/pages/Font";
|
||||||
import Landing from "@/pages/Landing";
|
import Landing from "@/pages/Landing";
|
||||||
@@ -44,6 +45,7 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
</Routes>
|
</Routes>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
<Toaster visibleToasts={1} duration={2000} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
UserRecord,
|
UserRecord,
|
||||||
} from "@issue/shared";
|
} from "@issue/shared";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import AccountDialog from "@/components/account-dialog";
|
import AccountDialog from "@/components/account-dialog";
|
||||||
import { CreateIssue } from "@/components/create-issue";
|
import { CreateIssue } from "@/components/create-issue";
|
||||||
import { CreateSprint } from "@/components/create-sprint";
|
import { CreateSprint } from "@/components/create-sprint";
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ResizablePanel, ResizablePanelGroup, ResizableSeparator } from "@/components/ui/resizable";
|
import { ResizablePanel, ResizablePanelGroup, ResizableSeparator } from "@/components/ui/resizable";
|
||||||
import { issue, organisation, project, sprint } from "@/lib/server";
|
import { issue, organisation, project, sprint } from "@/lib/server";
|
||||||
|
import { issueID } from "@/lib/utils";
|
||||||
|
|
||||||
const BREATHING_ROOM = 1;
|
const BREATHING_ROOM = 1;
|
||||||
|
|
||||||
@@ -385,8 +387,11 @@ export default function App() {
|
|||||||
issueNumber: null,
|
issueNumber: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onCreateOrganisation={async (organisationId) => {
|
onCreateOrganisation={async (org) => {
|
||||||
await refetchOrganisations({ selectOrganisationId: organisationId });
|
toast.success(`Created Organisation ${org.name}`, {
|
||||||
|
dismissible: false,
|
||||||
|
});
|
||||||
|
await refetchOrganisations({ selectOrganisationId: org.id });
|
||||||
}}
|
}}
|
||||||
showLabel
|
showLabel
|
||||||
/>
|
/>
|
||||||
@@ -406,10 +411,13 @@ export default function App() {
|
|||||||
issueNumber: null,
|
issueNumber: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onCreateProject={async (projectId) => {
|
onCreateProject={async (project) => {
|
||||||
if (!selectedOrganisation) return;
|
if (!selectedOrganisation) return;
|
||||||
|
toast.success(`Created Project ${project.name}`, {
|
||||||
|
dismissible: false,
|
||||||
|
});
|
||||||
await refetchProjects(selectedOrganisation.Organisation.id, {
|
await refetchProjects(selectedOrganisation.Organisation.id, {
|
||||||
selectProjectId: projectId,
|
selectProjectId: project.id,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
showLabel
|
showLabel
|
||||||
@@ -422,16 +430,31 @@ export default function App() {
|
|||||||
sprints={sprints}
|
sprints={sprints}
|
||||||
members={members}
|
members={members}
|
||||||
statuses={selectedOrganisation.Organisation.statuses}
|
statuses={selectedOrganisation.Organisation.statuses}
|
||||||
completeAction={async () => {
|
completeAction={async (issueNumber) => {
|
||||||
if (!selectedProject) return;
|
if (!selectedProject) return;
|
||||||
|
toast.success(
|
||||||
|
`Created ${issueID(selectedProject.Project.key, issueNumber)}`,
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
await refetchIssues();
|
await refetchIssues();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<CreateSprint
|
<CreateSprint
|
||||||
projectId={selectedProject?.Project.id}
|
projectId={selectedProject?.Project.id}
|
||||||
completeAction={async () => {
|
completeAction={async (sprint) => {
|
||||||
if (!selectedProject) return;
|
if (!selectedProject) return;
|
||||||
|
toast.success(
|
||||||
|
<>
|
||||||
|
Created sprint{" "}
|
||||||
|
<span style={{ color: sprint.color }}>{sprint.name}</span>
|
||||||
|
</>,
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
await refetchSprints(selectedProject?.Project.id);
|
await refetchSprints(selectedProject?.Project.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
4
todo.md
4
todo.md
@@ -1,13 +1,11 @@
|
|||||||
# HIGH PRIORITY
|
# HIGH PRIORITY
|
||||||
|
|
||||||
|
- fix colour picker alignment in new status form
|
||||||
- projects
|
- projects
|
||||||
- project management menu (will this be accessed from the organisations-dialog? or will it be a separate menu in the user select)
|
- project management menu (will this be accessed from the organisations-dialog? or will it be a separate menu in the user select)
|
||||||
- sprints
|
- sprints
|
||||||
- timeline display
|
- timeline display
|
||||||
- display sprints
|
- display sprints
|
||||||
- add toasts app-wide
|
|
||||||
- for almost every network interaction that is user prompted
|
|
||||||
- the interface feels snappy but sometimes it's hard to tell if your changes are volatile or saved
|
|
||||||
- issues
|
- issues
|
||||||
- sprint
|
- sprint
|
||||||
- edit title & description
|
- edit title & description
|
||||||
|
|||||||
Reference in New Issue
Block a user