mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
migrated modals to mutations
This commit is contained in:
@@ -10,10 +10,12 @@ import Icon from "@/components/ui/icon";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { UploadAvatar } from "@/components/upload-avatar";
|
||||
import { parseError, user } from "@/lib/server";
|
||||
import { useUpdateUser } from "@/lib/query/hooks";
|
||||
import { parseError } from "@/lib/server";
|
||||
|
||||
function AccountDialog({ trigger }: { trigger?: ReactNode }) {
|
||||
const { user: currentUser, setUser } = useAuthenticatedSession();
|
||||
const updateUser = useUpdateUser();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
@@ -45,30 +47,29 @@ function AccountDialog({ trigger }: { trigger?: ReactNode }) {
|
||||
return;
|
||||
}
|
||||
|
||||
await user.update({
|
||||
try {
|
||||
const data = await updateUser.mutateAsync({
|
||||
name: name.trim(),
|
||||
password: password.trim() || undefined,
|
||||
avatarURL,
|
||||
iconPreference,
|
||||
onSuccess: (data) => {
|
||||
});
|
||||
setError("");
|
||||
setUser(data);
|
||||
setPassword("");
|
||||
setOpen(false);
|
||||
|
||||
toast.success(`Account updated successfully`, {
|
||||
toast.success("Account updated successfully", {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = parseError(err);
|
||||
} catch (err) {
|
||||
const message = parseError(err as Error);
|
||||
setError(message);
|
||||
|
||||
toast.error(`Error updating account: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Field } from "@/components/ui/field";
|
||||
import { organisation, parseError, user } from "@/lib/server";
|
||||
import { useAddOrganisationMember } from "@/lib/query/hooks";
|
||||
import { parseError, user } from "@/lib/server";
|
||||
|
||||
export function AddMemberDialog({
|
||||
organisationId,
|
||||
@@ -29,6 +30,7 @@ export function AddMemberDialog({
|
||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const addMember = useAddOrganisationMember();
|
||||
|
||||
const reset = () => {
|
||||
setUsername("");
|
||||
@@ -60,34 +62,9 @@ export function AddMemberDialog({
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
let userId: number | null = null;
|
||||
let userData: UserRecord;
|
||||
await user.byUsername({
|
||||
username,
|
||||
onSuccess: (data: UserRecord) => {
|
||||
userData = data;
|
||||
userId = data.id;
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = parseError(err);
|
||||
setError(message || "user not found");
|
||||
setSubmitting(false);
|
||||
|
||||
toast.error(`Error adding member: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await organisation.addMember({
|
||||
organisationId,
|
||||
userId,
|
||||
role: "member",
|
||||
onSuccess: async () => {
|
||||
const userData: UserRecord = await user.byUsername(username);
|
||||
const userId = userData.id;
|
||||
await addMember.mutateAsync({ organisationId, userId, role: "member" });
|
||||
setOpen(false);
|
||||
reset();
|
||||
try {
|
||||
@@ -95,21 +72,15 @@ export function AddMemberDialog({
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = parseError(err);
|
||||
} catch (err) {
|
||||
const message = parseError(err as Error);
|
||||
console.error(err);
|
||||
setError(message || "failed to add member");
|
||||
setSubmitting(false);
|
||||
|
||||
toast.error(`Error adding member: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("failed to add member");
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Field } from "@/components/ui/field";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { organisation, parseError } from "@/lib/server";
|
||||
import { useCreateOrganisation, useUpdateOrganisation } from "@/lib/query/hooks";
|
||||
import { parseError } from "@/lib/server";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const slugify = (value: string) =>
|
||||
@@ -47,6 +48,8 @@ export function OrganisationModal({
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const { user } = useAuthenticatedSession();
|
||||
const createOrganisation = useCreateOrganisation();
|
||||
const updateOrganisation = useUpdateOrganisation();
|
||||
|
||||
const isControlled = controlledOpen !== undefined;
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
@@ -106,12 +109,12 @@ export function OrganisationModal({
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (isEdit && existingOrganisation) {
|
||||
await organisation.update({
|
||||
organisationId: existingOrganisation.id,
|
||||
const data = await updateOrganisation.mutateAsync({
|
||||
id: existingOrganisation.id,
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
onSuccess: async (data) => {
|
||||
});
|
||||
setOpen(false);
|
||||
reset();
|
||||
toast.success("Organisation updated");
|
||||
@@ -120,48 +123,33 @@ export function OrganisationModal({
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
onError: async (err) => {
|
||||
const message = parseError(err);
|
||||
setError(message || "failed to update organisation");
|
||||
setSubmitting(false);
|
||||
try {
|
||||
await errorAction?.(message || "failed to update organisation");
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await organisation.create({
|
||||
const data = await createOrganisation.mutateAsync({
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
onSuccess: async (data) => {
|
||||
});
|
||||
setOpen(false);
|
||||
reset();
|
||||
toast.success(`Created Organisation ${data.name}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
try {
|
||||
await completeAction?.(data);
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
onError: async (err) => {
|
||||
const message = parseError(err);
|
||||
setError(message || "failed to create organisation");
|
||||
}
|
||||
} catch (err) {
|
||||
const message = parseError(err as Error);
|
||||
console.error(err);
|
||||
setError(message || `failed to ${isEdit ? "update" : "create"} organisation`);
|
||||
setSubmitting(false);
|
||||
try {
|
||||
await errorAction?.(message || "failed to create organisation");
|
||||
await errorAction?.(message || `failed to ${isEdit ? "update" : "create"} organisation`);
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(`failed to ${isEdit ? "update" : "create"} organisation`);
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import {
|
||||
DEFAULT_STATUS_COLOUR,
|
||||
ISSUE_STATUS_MAX_LENGTH,
|
||||
type OrganisationMemberResponse,
|
||||
type OrganisationResponse,
|
||||
type ProjectRecord,
|
||||
type ProjectResponse,
|
||||
type SprintRecord,
|
||||
} from "@sprint/shared";
|
||||
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
||||
import { DEFAULT_STATUS_COLOUR, ISSUE_STATUS_MAX_LENGTH, type SprintRecord } from "@sprint/shared";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AddMemberDialog } from "@/components/add-member-dialog";
|
||||
import { OrganisationModal } from "@/components/organisation-modal";
|
||||
import { OrganisationSelect } from "@/components/organisation-select";
|
||||
import { ProjectModal } from "@/components/project-modal";
|
||||
import { ProjectSelect } from "@/components/project-select";
|
||||
import { useSelection } from "@/components/selection-provider";
|
||||
import { useAuthenticatedSession } from "@/components/session-provider";
|
||||
import SmallSprintDisplay from "@/components/small-sprint-display";
|
||||
import SmallUserDisplay from "@/components/small-user-display";
|
||||
@@ -34,39 +28,83 @@ import { IconButton } from "@/components/ui/icon-button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { issue, organisation, project, sprint } from "@/lib/server";
|
||||
import {
|
||||
useDeleteOrganisation,
|
||||
useDeleteProject,
|
||||
useDeleteSprint,
|
||||
useOrganisationMembers,
|
||||
useOrganisations,
|
||||
useProjects,
|
||||
useRemoveOrganisationMember,
|
||||
useReplaceIssueStatus,
|
||||
useSprints,
|
||||
useUpdateOrganisation,
|
||||
useUpdateOrganisationMemberRole,
|
||||
} from "@/lib/query/hooks";
|
||||
import { queryKeys } from "@/lib/query/keys";
|
||||
import { issue } from "@/lib/server";
|
||||
import { capitalise } from "@/lib/utils";
|
||||
|
||||
function OrganisationsDialog({
|
||||
trigger,
|
||||
organisations,
|
||||
selectedOrganisation,
|
||||
setSelectedOrganisation,
|
||||
refetchOrganisations,
|
||||
projects,
|
||||
selectedProject,
|
||||
sprints,
|
||||
onSelectedProjectChange,
|
||||
onCreateProject,
|
||||
onCreateSprint,
|
||||
}: {
|
||||
trigger?: ReactNode;
|
||||
organisations: OrganisationResponse[];
|
||||
selectedOrganisation: OrganisationResponse | null;
|
||||
setSelectedOrganisation: (organisation: OrganisationResponse | null) => void;
|
||||
refetchOrganisations: (options?: { selectOrganisationId?: number }) => Promise<void>;
|
||||
projects: ProjectResponse[];
|
||||
selectedProject: ProjectResponse | null;
|
||||
sprints: SprintRecord[];
|
||||
onSelectedProjectChange: (project: ProjectResponse | null) => void;
|
||||
onCreateProject: (project: ProjectRecord) => void | Promise<void>;
|
||||
onCreateSprint: (sprint: SprintRecord) => void | Promise<void>;
|
||||
}) {
|
||||
function OrganisationsDialog({ trigger }: { trigger?: ReactNode }) {
|
||||
const { user } = useAuthenticatedSession();
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedOrganisationId, selectedProjectId } = useSelection();
|
||||
const { data: organisationsData = [] } = useOrganisations();
|
||||
const { data: projectsData = [] } = useProjects(selectedOrganisationId);
|
||||
const { data: sprints = [] } = useSprints(selectedProjectId);
|
||||
const { data: membersData = [] } = useOrganisationMembers(selectedOrganisationId);
|
||||
const updateOrganisation = useUpdateOrganisation();
|
||||
const updateMemberRole = useUpdateOrganisationMemberRole();
|
||||
const removeMember = useRemoveOrganisationMember();
|
||||
const deleteOrganisation = useDeleteOrganisation();
|
||||
const deleteProject = useDeleteProject();
|
||||
const deleteSprint = useDeleteSprint();
|
||||
const replaceIssueStatus = useReplaceIssueStatus();
|
||||
|
||||
const organisations = useMemo(
|
||||
() => [...organisationsData].sort((a, b) => a.Organisation.name.localeCompare(b.Organisation.name)),
|
||||
[organisationsData],
|
||||
);
|
||||
const projects = useMemo(
|
||||
() => [...projectsData].sort((a, b) => a.Project.name.localeCompare(b.Project.name)),
|
||||
[projectsData],
|
||||
);
|
||||
const selectedOrganisation = useMemo(
|
||||
() => organisations.find((org) => org.Organisation.id === selectedOrganisationId) ?? null,
|
||||
[organisations, selectedOrganisationId],
|
||||
);
|
||||
const selectedProject = useMemo(
|
||||
() => projects.find((proj) => proj.Project.id === selectedProjectId) ?? null,
|
||||
[projects, selectedProjectId],
|
||||
);
|
||||
|
||||
const invalidateOrganisations = () =>
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.organisations.byUser() });
|
||||
const invalidateProjects = () =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.projects.byOrganisation(selectedOrganisationId ?? 0),
|
||||
});
|
||||
const invalidateMembers = useCallback(
|
||||
() =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.organisations.members(selectedOrganisationId ?? 0),
|
||||
}),
|
||||
[queryClient, selectedOrganisationId],
|
||||
);
|
||||
const invalidateSprints = () =>
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.sprints.byProject(selectedProjectId ?? 0) });
|
||||
const members = useMemo(() => {
|
||||
const roleOrder: Record<string, number> = { owner: 0, admin: 1, member: 2 };
|
||||
return [...membersData].sort((a, b) => {
|
||||
const roleA = roleOrder[a.OrganisationMember.role] ?? 3;
|
||||
const roleB = roleOrder[b.OrganisationMember.role] ?? 3;
|
||||
if (roleA !== roleB) return roleA - roleB;
|
||||
return a.User.name.localeCompare(b.User.name);
|
||||
});
|
||||
}, [membersData]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("info");
|
||||
const [members, setMembers] = useState<OrganisationMemberResponse[]>([]);
|
||||
|
||||
const [statuses, setStatuses] = useState<Record<string, string>>({});
|
||||
const [isCreatingStatus, setIsCreatingStatus] = useState(false);
|
||||
@@ -123,37 +161,6 @@ function OrganisationsDialog({
|
||||
return start <= today && today <= end;
|
||||
};
|
||||
|
||||
const refetchMembers = useCallback(async () => {
|
||||
if (!selectedOrganisation) return;
|
||||
try {
|
||||
await organisation.members({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
onSuccess: (data) => {
|
||||
const members = data as OrganisationMemberResponse[];
|
||||
const roleOrder: Record<string, number> = { owner: 0, admin: 1, member: 2 };
|
||||
members.sort((a, b) => {
|
||||
const roleA = roleOrder[a.OrganisationMember.role] ?? 3;
|
||||
const roleB = roleOrder[b.OrganisationMember.role] ?? 3;
|
||||
if (roleA !== roleB) return roleA - roleB;
|
||||
return a.User.name.localeCompare(b.User.name);
|
||||
});
|
||||
setMembers(members);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
setMembers([]);
|
||||
|
||||
toast.error(`Error fetching members: ${error}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("error fetching members:", err);
|
||||
setMembers([]);
|
||||
}
|
||||
}, [selectedOrganisation]);
|
||||
|
||||
const handleRoleChange = (memberUserId: number, memberName: string, currentRole: string) => {
|
||||
if (!selectedOrganisation) return;
|
||||
const action = currentRole === "admin" ? "demote" : "promote";
|
||||
@@ -167,32 +174,23 @@ function OrganisationsDialog({
|
||||
variant: action === "demote" ? "destructive" : "default",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await organisation.updateMemberRole({
|
||||
await updateMemberRole.mutateAsync({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
userId: memberUserId,
|
||||
role: newRole,
|
||||
onSuccess: () => {
|
||||
});
|
||||
closeConfirmDialog();
|
||||
|
||||
toast.success(`${capitalise(action)}d ${memberName} to ${newRole} successfully`, {
|
||||
dismissible: false,
|
||||
});
|
||||
|
||||
void refetchMembers();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error(
|
||||
`Error ${action.slice(0, -1)}ing ${memberName} to ${newRole}: ${error}`,
|
||||
`Error ${action.slice(0, -1)}ing ${memberName} to ${newRole}: ${String(err)}`,
|
||||
{
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -213,34 +211,25 @@ function OrganisationsDialog({
|
||||
variant: "destructive",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await organisation.removeMember({
|
||||
await removeMember.mutateAsync({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
userId: memberUserId,
|
||||
onSuccess: () => {
|
||||
});
|
||||
closeConfirmDialog();
|
||||
|
||||
toast.success(
|
||||
`Removed ${memberName} from ${selectedOrganisation.Organisation.name} successfully`,
|
||||
{
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
|
||||
void refetchMembers();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error(
|
||||
`Error removing member from ${selectedOrganisation.Organisation.name}: ${error}`,
|
||||
`Error removing member from ${selectedOrganisation.Organisation.name}: ${String(err)}`,
|
||||
{
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -261,16 +250,16 @@ function OrganisationsDialog({
|
||||
if (!selectedOrganisation) return;
|
||||
|
||||
try {
|
||||
await organisation.update({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
await updateOrganisation.mutateAsync({
|
||||
id: selectedOrganisation.Organisation.id,
|
||||
statuses: newStatuses,
|
||||
onSuccess: () => {
|
||||
});
|
||||
setStatuses(newStatuses);
|
||||
if (statusAdded) {
|
||||
toast.success(
|
||||
<>
|
||||
Created <StatusTag status={statusAdded.name} colour={statusAdded.colour} />{" "}
|
||||
status successfully
|
||||
Created <StatusTag status={statusAdded.name} colour={statusAdded.colour} /> status
|
||||
successfully
|
||||
</>,
|
||||
{
|
||||
dismissible: false,
|
||||
@@ -279,8 +268,7 @@ function OrganisationsDialog({
|
||||
} else if (statusRemoved) {
|
||||
toast.success(
|
||||
<>
|
||||
Removed{" "}
|
||||
<StatusTag status={statusRemoved.name} colour={statusRemoved.colour} /> status
|
||||
Removed <StatusTag status={statusRemoved.name} colour={statusRemoved.colour} /> status
|
||||
successfully
|
||||
</>,
|
||||
{
|
||||
@@ -291,25 +279,22 @@ function OrganisationsDialog({
|
||||
toast.success(
|
||||
<>
|
||||
Moved <StatusTag status={statusMoved.name} colour={statusMoved.colour} /> from
|
||||
position {statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1}{" "}
|
||||
successfully
|
||||
position
|
||||
{statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1} successfully
|
||||
</>,
|
||||
{
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
void refetchOrganisations();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("error updating statuses:", error);
|
||||
|
||||
await invalidateOrganisations();
|
||||
} catch (err) {
|
||||
console.error("error updating statuses:", err);
|
||||
if (statusAdded) {
|
||||
toast.error(
|
||||
<>
|
||||
Error adding{" "}
|
||||
<StatusTag status={statusAdded.name} colour={statusAdded.colour} /> to{" "}
|
||||
{selectedOrganisation.Organisation.name}: {error}
|
||||
Error adding <StatusTag status={statusAdded.name} colour={statusAdded.colour} /> to{" "}
|
||||
{selectedOrganisation.Organisation.name}: {String(err)}
|
||||
</>,
|
||||
{
|
||||
dismissible: false,
|
||||
@@ -318,9 +303,8 @@ function OrganisationsDialog({
|
||||
} else if (statusRemoved) {
|
||||
toast.error(
|
||||
<>
|
||||
Error removing{" "}
|
||||
<StatusTag status={statusRemoved.name} colour={statusRemoved.colour} /> from{" "}
|
||||
{selectedOrganisation.Organisation.name}: {error}
|
||||
Error removing <StatusTag status={statusRemoved.name} colour={statusRemoved.colour} />{" "}
|
||||
from {selectedOrganisation.Organisation.name}: {String(err)}
|
||||
</>,
|
||||
{
|
||||
dismissible: false,
|
||||
@@ -329,20 +313,16 @@ function OrganisationsDialog({
|
||||
} 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}
|
||||
Error moving <StatusTag status={statusMoved.name} colour={statusMoved.colour} /> from
|
||||
position
|
||||
{statusMoved.currentIndex + 1} to {statusMoved.nextIndex + 1}{" "}
|
||||
{selectedOrganisation.Organisation.name}: {String(err)}
|
||||
</>,
|
||||
{
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("error updating statuses:", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -374,41 +354,32 @@ function OrganisationsDialog({
|
||||
const handleRemoveStatusClick = async (status: string) => {
|
||||
if (Object.keys(statuses).length <= 1 || !selectedOrganisation) return;
|
||||
try {
|
||||
await issue.statusCount({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
status,
|
||||
onSuccess: (data) => {
|
||||
const count = (data as { count?: number }).count ?? 0;
|
||||
const data = await issue.statusCount(selectedOrganisation.Organisation.id, status);
|
||||
const count = data.find((item) => item.status === status)?.count ?? 0;
|
||||
if (count > 0) {
|
||||
setStatusToRemove(status);
|
||||
setIssuesUsingStatus(count);
|
||||
const remaining = Object.keys(statuses).filter((s) => s !== status);
|
||||
const remaining = Object.keys(statuses).filter((item) => item !== status);
|
||||
setReassignToStatus(remaining[0] || "");
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStatuses = Object.keys(statuses).filter((s) => s !== status);
|
||||
void updateStatuses(
|
||||
const nextStatuses = Object.keys(statuses).filter((item) => item !== status);
|
||||
await updateStatuses(
|
||||
Object.fromEntries(nextStatuses.map((statusKey) => [statusKey, statuses[statusKey]])),
|
||||
{ name: status, colour: statuses[status] },
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("error checking status usage:", error);
|
||||
|
||||
} catch (err) {
|
||||
console.error("error checking status usage:", err);
|
||||
toast.error(
|
||||
<>
|
||||
Error checking status usage for{" "}
|
||||
<StatusTag status={status} colour={statuses[status]} />: {error}
|
||||
Error checking status usage for <StatusTag status={status} colour={statuses[status]} />:{" "}
|
||||
{String(err)}
|
||||
</>,
|
||||
{
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("error checking status usage:", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -435,40 +406,38 @@ function OrganisationsDialog({
|
||||
const confirmRemoveStatus = async () => {
|
||||
if (!statusToRemove || !reassignToStatus || !selectedOrganisation) return;
|
||||
|
||||
await issue.replaceStatus({
|
||||
try {
|
||||
await replaceIssueStatus.mutateAsync({
|
||||
organisationId: selectedOrganisation.Organisation.id,
|
||||
oldStatus: statusToRemove,
|
||||
newStatus: reassignToStatus,
|
||||
onSuccess: async () => {
|
||||
const newStatuses = Object.keys(statuses).filter((s) => s !== statusToRemove);
|
||||
});
|
||||
const newStatuses = Object.keys(statuses).filter((item) => item !== statusToRemove);
|
||||
await updateStatuses(
|
||||
Object.fromEntries(newStatuses.map((status) => [status, statuses[status]])),
|
||||
{ name: statusToRemove, colour: statuses[statusToRemove] },
|
||||
);
|
||||
setStatusToRemove(null);
|
||||
setReassignToStatus("");
|
||||
},
|
||||
onError: (error) => {
|
||||
} catch (error) {
|
||||
console.error("error replacing status:", error);
|
||||
|
||||
toast.error(
|
||||
<>
|
||||
Error removing <StatusTag status={statusToRemove} colour={statuses[statusToRemove]} />{" "}
|
||||
from
|
||||
{selectedOrganisation.Organisation.name}: {error}{" "}
|
||||
{selectedOrganisation.Organisation.name}: {String(error)}
|
||||
</>,
|
||||
{
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
void refetchMembers();
|
||||
}, [open, refetchMembers]);
|
||||
if (!open || !selectedOrganisationId) return;
|
||||
void invalidateMembers();
|
||||
}, [open, invalidateMembers, selectedOrganisationId]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -490,21 +459,6 @@ function OrganisationsDialog({
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full min-w-0">
|
||||
<div className="flex flex-wrap gap-2 items-center w-full min-w-0">
|
||||
<OrganisationSelect
|
||||
organisations={organisations}
|
||||
selectedOrganisation={selectedOrganisation}
|
||||
onSelectedOrganisationChange={(org) => {
|
||||
setSelectedOrganisation(org);
|
||||
localStorage.setItem(
|
||||
"selectedOrganisationId",
|
||||
`${org?.Organisation.id}`,
|
||||
);
|
||||
}}
|
||||
onCreateOrganisation={async (org) => {
|
||||
toast.success(`Created Organisation ${org.name}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
await refetchOrganisations({ selectOrganisationId: org.id });
|
||||
}}
|
||||
contentClass={
|
||||
"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"
|
||||
}
|
||||
@@ -562,21 +516,18 @@ function OrganisationsDialog({
|
||||
processingText: "Deleting...",
|
||||
variant: "destructive",
|
||||
onConfirm: async () => {
|
||||
await organisation.remove({
|
||||
organisationId:
|
||||
try {
|
||||
await deleteOrganisation.mutateAsync(
|
||||
selectedOrganisation.Organisation.id,
|
||||
onSuccess: async () => {
|
||||
);
|
||||
closeConfirmDialog();
|
||||
toast.success(
|
||||
`Deleted organisation "${selectedOrganisation.Organisation.name}"`,
|
||||
);
|
||||
setSelectedOrganisation(null);
|
||||
await refetchOrganisations();
|
||||
},
|
||||
onError: (error) => {
|
||||
await invalidateOrganisations();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -595,9 +546,7 @@ function OrganisationsDialog({
|
||||
open={editOrgOpen}
|
||||
onOpenChange={setEditOrgOpen}
|
||||
completeAction={async () => {
|
||||
await refetchOrganisations({
|
||||
selectOrganisationId: selectedOrganisation.Organisation.id,
|
||||
});
|
||||
await invalidateOrganisations();
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
@@ -683,7 +632,7 @@ function OrganisationsDialog({
|
||||
},
|
||||
);
|
||||
|
||||
refetchMembers();
|
||||
void invalidateMembers();
|
||||
}}
|
||||
trigger={
|
||||
<Button variant="outline">
|
||||
@@ -699,14 +648,7 @@ function OrganisationsDialog({
|
||||
<TabsContent value="projects">
|
||||
<div className="border p-2 min-w-0 overflow-hidden">
|
||||
<div className="flex flex-col gap-3">
|
||||
<ProjectSelect
|
||||
projects={projects}
|
||||
selectedProject={selectedProject}
|
||||
organisationId={selectedOrganisation?.Organisation.id}
|
||||
onSelectedProjectChange={onSelectedProjectChange}
|
||||
onCreateProject={onCreateProject}
|
||||
showLabel
|
||||
/>
|
||||
<ProjectSelect showLabel />
|
||||
<div className="flex gap-3 flex-col">
|
||||
<div className="border p-2 min-w-0 overflow-hidden">
|
||||
{selectedProject ? (
|
||||
@@ -745,27 +687,20 @@ function OrganisationsDialog({
|
||||
processingText: "Deleting...",
|
||||
variant: "destructive",
|
||||
onConfirm: async () => {
|
||||
await project.remove({
|
||||
projectId:
|
||||
try {
|
||||
await deleteProject.mutateAsync(
|
||||
selectedProject
|
||||
.Project.id,
|
||||
onSuccess:
|
||||
async () => {
|
||||
);
|
||||
closeConfirmDialog();
|
||||
toast.success(
|
||||
`Deleted project "${selectedProject.Project.name}"`,
|
||||
);
|
||||
onSelectedProjectChange(
|
||||
null,
|
||||
);
|
||||
await refetchOrganisations();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(
|
||||
error,
|
||||
);
|
||||
},
|
||||
});
|
||||
await invalidateProjects();
|
||||
await invalidateSprints();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -859,28 +794,20 @@ function OrganisationsDialog({
|
||||
"destructive",
|
||||
onConfirm:
|
||||
async () => {
|
||||
await sprint.remove(
|
||||
{
|
||||
sprintId:
|
||||
try {
|
||||
await deleteSprint.mutateAsync(
|
||||
sprintItem.id,
|
||||
onSuccess:
|
||||
async () => {
|
||||
);
|
||||
closeConfirmDialog();
|
||||
toast.success(
|
||||
`Deleted sprint "${sprintItem.name}"`,
|
||||
);
|
||||
await refetchOrganisations();
|
||||
},
|
||||
onError:
|
||||
(
|
||||
error,
|
||||
) => {
|
||||
await invalidateSprints();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
error,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -902,7 +829,6 @@ function OrganisationsDialog({
|
||||
{isAdmin && (
|
||||
<SprintModal
|
||||
projectId={selectedProject?.Project.id}
|
||||
completeAction={onCreateSprint}
|
||||
trigger={
|
||||
<Button variant="outline" size="sm">
|
||||
Create sprint{" "}
|
||||
@@ -934,7 +860,7 @@ function OrganisationsDialog({
|
||||
open={editProjectOpen}
|
||||
onOpenChange={setEditProjectOpen}
|
||||
completeAction={async () => {
|
||||
await refetchOrganisations();
|
||||
await invalidateProjects();
|
||||
}}
|
||||
/>
|
||||
<SprintModal
|
||||
@@ -947,7 +873,7 @@ function OrganisationsDialog({
|
||||
if (!open) setEditingSprint(null);
|
||||
}}
|
||||
completeAction={async () => {
|
||||
await refetchOrganisations();
|
||||
await invalidateSprints();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -1105,18 +1031,6 @@ function OrganisationsDialog({
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 w-full min-w-0">
|
||||
<OrganisationSelect
|
||||
organisations={organisations}
|
||||
selectedOrganisation={selectedOrganisation}
|
||||
onSelectedOrganisationChange={(org) => {
|
||||
setSelectedOrganisation(org);
|
||||
localStorage.setItem("selectedOrganisationId", `${org?.Organisation.id}`);
|
||||
}}
|
||||
onCreateOrganisation={async (org) => {
|
||||
toast.success(`Created Organisation ${org.name}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
await refetchOrganisations({ selectOrganisationId: org.id });
|
||||
}}
|
||||
contentClass={
|
||||
"data-[side=bottom]:translate-y-2 data-[side=bottom]:translate-x-0.25"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Field } from "@/components/ui/field";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { parseError, project } from "@/lib/server";
|
||||
import { useCreateProject, useUpdateProject } from "@/lib/query/hooks";
|
||||
import { parseError } from "@/lib/server";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const keyify = (value: string) =>
|
||||
@@ -40,6 +41,8 @@ export function ProjectModal({
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const { user } = useAuthenticatedSession();
|
||||
const createProject = useCreateProject();
|
||||
const updateProject = useUpdateProject();
|
||||
|
||||
const isControlled = controlledOpen !== undefined;
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
@@ -106,12 +109,11 @@ export function ProjectModal({
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (isEdit && existingProject) {
|
||||
await project.update({
|
||||
projectId: existingProject.id,
|
||||
const proj = await updateProject.mutateAsync({
|
||||
id: existingProject.id,
|
||||
key,
|
||||
name,
|
||||
onSuccess: async (data) => {
|
||||
const proj = data as ProjectRecord;
|
||||
});
|
||||
setOpen(false);
|
||||
reset();
|
||||
toast.success("Project updated");
|
||||
@@ -120,52 +122,35 @@ export function ProjectModal({
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = parseError(err);
|
||||
setError(message);
|
||||
setSubmitting(false);
|
||||
|
||||
toast.error(`Error updating project: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (!organisationId) {
|
||||
setError("select an organisation first");
|
||||
return;
|
||||
}
|
||||
await project.create({
|
||||
const proj = await createProject.mutateAsync({
|
||||
key,
|
||||
name,
|
||||
organisationId: organisationId,
|
||||
onSuccess: async (data) => {
|
||||
const proj = data as ProjectRecord;
|
||||
|
||||
organisationId,
|
||||
});
|
||||
setOpen(false);
|
||||
reset();
|
||||
toast.success(`Created Project ${proj.name}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
try {
|
||||
await completeAction?.(proj);
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = parseError(err);
|
||||
setError(message);
|
||||
setSubmitting(false);
|
||||
|
||||
toast.error(`Error creating project: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const message = parseError(err as Error);
|
||||
console.error(err);
|
||||
setError(`failed to ${isEdit ? "update" : "create"} project`);
|
||||
setError(message || `failed to ${isEdit ? "update" : "create"} project`);
|
||||
setSubmitting(false);
|
||||
toast.error(`Error ${isEdit ? "updating" : "creating"} project: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
import { Field } from "@/components/ui/field";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { parseError, sprint } from "@/lib/server";
|
||||
import { useCreateSprint, useUpdateSprint } from "@/lib/query/hooks";
|
||||
import { parseError } from "@/lib/server";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SPRINT_NAME_MAX_LENGTH = 64;
|
||||
@@ -67,6 +68,8 @@ export function SprintModal({
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const { user } = useAuthenticatedSession();
|
||||
const createSprint = useCreateSprint();
|
||||
const updateSprint = useUpdateSprint();
|
||||
|
||||
const isControlled = controlledOpen !== undefined;
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
@@ -146,13 +149,13 @@ export function SprintModal({
|
||||
|
||||
try {
|
||||
if (isEdit && existingSprint) {
|
||||
await sprint.update({
|
||||
sprintId: existingSprint.id,
|
||||
const data = await updateSprint.mutateAsync({
|
||||
id: existingSprint.id,
|
||||
name,
|
||||
color: colour,
|
||||
startDate,
|
||||
endDate,
|
||||
onSuccess: async (data) => {
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
});
|
||||
setOpen(false);
|
||||
reset();
|
||||
toast.success("Sprint updated");
|
||||
@@ -161,52 +164,42 @@ export function SprintModal({
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = parseError(err);
|
||||
setError(message);
|
||||
setSubmitting(false);
|
||||
|
||||
toast.error(`Error updating sprint: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (!projectId) {
|
||||
setError("select a project first");
|
||||
return;
|
||||
}
|
||||
await sprint.create({
|
||||
projectId: projectId,
|
||||
const data = await createSprint.mutateAsync({
|
||||
projectId,
|
||||
name,
|
||||
color: colour,
|
||||
startDate,
|
||||
endDate,
|
||||
onSuccess: async (data) => {
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
});
|
||||
setOpen(false);
|
||||
reset();
|
||||
toast.success(
|
||||
<>
|
||||
Created sprint <span style={{ color: data.color }}>{data.name}</span>
|
||||
</>,
|
||||
{
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await completeAction?.(data);
|
||||
} catch (actionErr) {
|
||||
console.error(actionErr);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = parseError(err);
|
||||
setError(message);
|
||||
setSubmitting(false);
|
||||
|
||||
toast.error(`Error creating sprint: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (submitError) {
|
||||
const message = parseError(submitError as Error);
|
||||
console.error(submitError);
|
||||
setError(`failed to ${isEdit ? "update" : "create"} sprint`);
|
||||
setError(message || `failed to ${isEdit ? "update" : "create"} sprint`);
|
||||
setSubmitting(false);
|
||||
toast.error(`Error ${isEdit ? "updating" : "creating"} sprint: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import Avatar from "@/components/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Icon from "@/components/ui/icon";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { parseError, user } from "@/lib/server";
|
||||
import { useUploadAvatar } from "@/lib/query/hooks";
|
||||
import { parseError } from "@/lib/server";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function UploadAvatar({
|
||||
@@ -25,6 +26,7 @@ export function UploadAvatar({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const uploadAvatar = useUploadAvatar();
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -33,9 +35,8 @@ export function UploadAvatar({
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
|
||||
await user.uploadAvatar({
|
||||
file,
|
||||
onSuccess: (url) => {
|
||||
try {
|
||||
const url = await uploadAvatar.mutateAsync(file);
|
||||
onAvatarUploaded(url);
|
||||
setUploading(false);
|
||||
|
||||
@@ -48,17 +49,15 @@ export function UploadAvatar({
|
||||
dismissible: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = parseError(err);
|
||||
} catch (err) {
|
||||
const message = parseError(err as Error);
|
||||
setError(message);
|
||||
setUploading(false);
|
||||
|
||||
toast.error(`Error uploading avatar: ${message}`, {
|
||||
dismissible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user