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