frontend server utility improvement

This commit is contained in:
Oliver Bryan
2025-12-31 17:57:55 +00:00
parent c7d261048b
commit 70ef02f790
19 changed files with 381 additions and 182 deletions

View File

@@ -17,8 +17,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ResizablePanel, ResizablePanelGroup, ResizableSeparator } from "@/components/ui/resizable";
import { issue, organisation, project } from "@/lib/server";
import { getAuthHeaders, getServerURL } from "@/lib/utils";
import { ResizablePanel, ResizablePanelGroup, ResizableSeparator } from "./components/ui/resizable";
function Index() {
const user = JSON.parse(localStorage.getItem("user") || "{}") as UserRecord;
@@ -35,27 +36,35 @@ function Index() {
const refetchOrganisations = async (options?: { selectOrganisationId?: number }) => {
try {
const res = await fetch(`${getServerURL()}/organisation/by-user?userId=${user.id}`, {
headers: getAuthHeaders(),
});
const data = (await res.json()) as Array<OrganisationResponse>;
await organisation.byUser({
userId: user.id,
onSuccess: (data) => {
const organisations = data as OrganisationResponse[];
setOrganisations(organisations);
setOrganisations(data);
// select newly created organisation
if (options?.selectOrganisationId) {
const created = organisations.find(
(o) => o.Organisation.id === options.selectOrganisationId,
);
if (created) {
setSelectedOrganisation(created);
return;
}
}
// select newly created organisation
if (options?.selectOrganisationId) {
const created = data.find((o) => o.Organisation.id === options.selectOrganisationId);
if (created) {
setSelectedOrganisation(created);
return;
}
}
// preserve previously selected organisation
setSelectedOrganisation((prev) => {
if (!prev) return data[0] || null;
const stillExists = data.find((o) => o.Organisation.id === prev.Organisation.id);
return stillExists || data[0] || null;
// preserve previously selected organisation
setSelectedOrganisation((prev) => {
if (!prev) return organisations[0] || null;
const stillExists = organisations.find(
(o) => o.Organisation.id === prev.Organisation.id,
);
return stillExists || organisations[0] || null;
});
},
onError: (error) => {
console.error("error fetching organisations:", error);
},
});
} catch (err) {
console.error("error fetching organisations:", err);
@@ -74,30 +83,31 @@ function Index() {
const refetchProjects = async (organisationId: number, options?: { selectProjectId?: number }) => {
try {
const res = await fetch(
`${getServerURL()}/projects/by-organisation?organisationId=${organisationId}`,
{
headers: getAuthHeaders(),
await project.byOrganisation({
organisationId,
onSuccess: (data) => {
const projects = data as ProjectResponse[];
setProjects(projects);
// select newly created project
if (options?.selectProjectId) {
const created = projects.find((p) => p.Project.id === options.selectProjectId);
if (created) {
setSelectedProject(created);
return;
}
}
// preserve previously selected project
setSelectedProject((prev) => {
if (!prev) return projects[0] || null;
const stillExists = projects.find((p) => p.Project.id === prev.Project.id);
return stillExists || projects[0] || null;
});
},
onError: (error) => {
console.error("error fetching projects:", error);
},
);
const data = (await res.json()) as ProjectResponse[];
setProjects(data);
// select newly created project
if (options?.selectProjectId) {
const created = data.find((p) => p.Project.id === options.selectProjectId);
if (created) {
setSelectedProject(created);
return;
}
}
// preserve previously selected project
setSelectedProject((prev) => {
if (!prev) return data[0] || null;
const stillExists = data.find((p) => p.Project.id === prev.Project.id);
return stillExists || data[0] || null;
});
} catch (err) {
console.error("error fetching projects:", err);
@@ -124,11 +134,17 @@ function Index() {
const refetchIssues = async (projectKey: string) => {
try {
const res = await fetch(`${getServerURL()}/issues/${projectKey}`, {
headers: getAuthHeaders(),
await issue.byProject({
projectId: selectedProject?.Project.id || 0,
onSuccess: (data) => {
const issues = data as IssueResponse[];
setIssues(issues);
},
onError: (error) => {
console.error("error fetching issues:", error);
setIssues([]);
},
});
const data = (await res.json()) as IssueResponse[];
setIssues(data);
} catch (err) {
console.error("error fetching issues:", err);
setIssues([]);

View File

@@ -2,6 +2,7 @@ import type { OrganisationResponse, UserRecord } from "@issue/shared";
import { useCallback, useEffect, useState } from "react";
import { OrganisationSelect } from "@/components/organisation-select";
import { SettingsPageLayout } from "@/components/settings-page-layout";
import { organisation } from "@/lib/server";
import { getAuthHeaders, getServerURL } from "@/lib/utils";
function Organisations() {
@@ -13,32 +14,35 @@ function Organisations() {
const refetchOrganisations = useCallback(
async (options?: { selectOrganisationId?: number }) => {
try {
const res = await fetch(`${getServerURL()}/organisation/by-user?userId=${user.id}`, {
headers: getAuthHeaders(),
});
await organisation.byUser({
userId: user.id,
onSuccess: (data) => {
const organisations = data as OrganisationResponse[];
setOrganisations(organisations);
if (!res.ok) {
console.error(await res.text());
setOrganisations([]);
setSelectedOrganisation(null);
return;
}
if (options?.selectOrganisationId) {
const created = organisations.find(
(o) => o.Organisation.id === options.selectOrganisationId,
);
if (created) {
setSelectedOrganisation(created);
return;
}
}
const data = (await res.json()) as Array<OrganisationResponse>;
setOrganisations(data);
if (options?.selectOrganisationId) {
const created = data.find((o) => o.Organisation.id === options.selectOrganisationId);
if (created) {
setSelectedOrganisation(created);
return;
}
}
setSelectedOrganisation((prev) => {
if (!prev) return data[0] || null;
const stillExists = data.find((o) => o.Organisation.id === prev.Organisation.id);
return stillExists || data[0] || null;
setSelectedOrganisation((prev) => {
if (!prev) return organisations[0] || null;
const stillExists = organisations.find(
(o) => o.Organisation.id === prev.Organisation.id,
);
return stillExists || organisations[0] || null;
});
},
onError: (error) => {
console.error(error);
setOrganisations([]);
setSelectedOrganisation(null);
},
});
} catch (err) {
console.error("error fetching organisations:", err);

View File

@@ -10,7 +10,8 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn, getAuthHeaders, getServerURL } from "@/lib/utils";
import { issue } from "@/lib/server";
import { cn } from "@/lib/utils";
export function CreateIssue({
projectId,
@@ -88,36 +89,24 @@ export function CreateIssue({
setSubmitting(true);
try {
const url = new URL(`${getServerURL()}/issue/create`);
url.searchParams.set("projectId", `${projectId}`);
url.searchParams.set("title", title.trim());
url.searchParams.set("description", description.trim());
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
await issue.create({
projectId,
title,
description,
onSuccess: async (data) => {
setOpen(false);
reset();
try {
await completeAction?.(data.id);
} catch (actionErr) {
console.error(actionErr);
}
},
onError: (message) => {
setError(message);
setSubmitting(false);
},
});
if (!res.ok) {
const message = await res.text();
setError(message || `failed to create issue (${res.status})`);
setSubmitting(false);
return;
}
const issue = (await res.json()) as { id?: number };
if (!issue.id) {
setError("failed to create issue");
setSubmitting(false);
return;
}
setOpen(false);
reset();
try {
await completeAction?.(issue.id);
} catch (actionErr) {
console.error(actionErr);
}
} catch (err) {
console.error(err);
setError("failed to create issue");

View File

@@ -10,7 +10,8 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn, getAuthHeaders, getServerURL } from "@/lib/utils";
import { organisation } from "@/lib/server";
import { cn } from "@/lib/utils";
const slugify = (value: string) =>
value
@@ -88,39 +89,25 @@ export function CreateOrganisation({
setSubmitting(true);
try {
const url = new URL(`${getServerURL()}/organisation/create`);
url.searchParams.set("name", name.trim());
url.searchParams.set("slug", slug.trim());
url.searchParams.set("userId", `${userId}`);
if (description.trim() !== "") {
url.searchParams.set("description", description.trim());
}
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
await organisation.create({
name,
slug,
description,
userId,
onSuccess: async (data) => {
setOpen(false);
reset();
try {
await completeAction?.(data.id);
} catch (actionErr) {
console.error(actionErr);
}
},
onError: (err) => {
setError(err || "failed to create organisation");
setSubmitting(false);
},
});
if (!res.ok) {
const message = await res.text();
setError(message || `failed to create organisation (${res.status})`);
setSubmitting(false);
return;
}
const organisation = (await res.json()) as { id?: number };
if (!organisation.id) {
setError("failed to create organisation");
setSubmitting(false);
return;
}
setOpen(false);
reset();
try {
await completeAction?.(organisation.id);
} catch (actionErr) {
console.error(actionErr);
}
} catch (err) {
console.error(err);
setError("failed to create organisation");

View File

@@ -1,3 +1,4 @@
import type { ProjectRecord } from "@issue/shared";
import { type FormEvent, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import {
@@ -10,7 +11,8 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn, getAuthHeaders, getServerURL } from "@/lib/utils";
import { project } from "@/lib/server";
import { cn } from "@/lib/utils";
const keyify = (value: string) =>
value
@@ -94,37 +96,27 @@ export function CreateProject({
setSubmitting(true);
try {
const url = new URL(`${getServerURL()}/project/create`);
url.searchParams.set("key", key);
url.searchParams.set("name", name.trim());
url.searchParams.set("creatorId", `${userId}`);
url.searchParams.set("organisationId", `${organisationId}`);
await project.create({
key,
name,
creatorId: userId,
organisationId,
onSuccess: async (data) => {
const project = data as ProjectRecord;
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
setOpen(false);
reset();
try {
await completeAction?.(project.id);
} catch (actionErr) {
console.error(actionErr);
}
},
onError: (message) => {
setError(message);
setSubmitting(false);
},
});
if (!res.ok) {
const message = await res.text();
setError(message || `failed to create project (${res.status})`);
setSubmitting(false);
return;
}
const project = (await res.json()) as { id?: number };
if (!project.id) {
setError("failed to create project");
setSubmitting(false);
return;
}
setOpen(false);
reset();
try {
await completeAction?.(project.id);
} catch (actionErr) {
console.error(actionErr);
}
} catch (err) {
console.error(err);
setError("failed to create project");

View File

@@ -0,0 +1,8 @@
export * as issue from "./issue";
export * as organisation from "./organisation";
export * as project from "./project";
export type ServerQueryInput = {
onSuccess?: (data: any, res: Response) => void;
onError?: (error: any) => void;
};

View File

@@ -0,0 +1,26 @@
import { getAuthHeaders, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function byProject({
projectId,
onSuccess,
onError,
}: {
projectId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/issues/by-project`);
url.searchParams.set("projectId", `${projectId}`);
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get issues by project (${res.status})`);
} else {
const data = await res.json();
onSuccess?.(data, res);
}
}

View File

@@ -0,0 +1,36 @@
import { getAuthHeaders, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function create({
projectId,
title,
description,
onSuccess,
onError,
}: {
projectId: number;
title: string;
description: string;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/issue/create`);
url.searchParams.set("projectId", `${projectId}`);
url.searchParams.set("title", title.trim());
if (description.trim() !== "") url.searchParams.set("description", description.trim());
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to create issue (${res.status})`);
} else {
const data = await res.json();
if (!data.id) {
onError?.(`failed to create issue (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -0,0 +1,2 @@
export { byProject } from "./byProject";
export { create } from "./create";

View File

@@ -0,0 +1,30 @@
import { getAuthHeaders, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function byUser({
userId,
onSuccess,
onError,
}: {
userId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisations/by-user`);
url.searchParams.set("userId", `${userId}`);
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to create organisation (${res.status})`);
} else {
const data = await res.json();
if (!data.id) {
onError?.(`failed to create organisation (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -0,0 +1,39 @@
import { getAuthHeaders, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function create({
name,
slug,
userId,
description,
onSuccess,
onError,
}: {
name: string;
slug: string;
userId: number;
description: string;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisation/create`);
url.searchParams.set("name", name.trim());
url.searchParams.set("slug", slug.trim());
url.searchParams.set("userId", `${userId}`);
if (description.trim() !== "") url.searchParams.set("description", description.trim());
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to create organisation (${res.status})`);
} else {
const data = await res.json();
if (!data.id) {
onError?.(`failed to create organisation (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -0,0 +1,2 @@
export { byUser } from "./byUser";
export { create } from "./create";

View File

@@ -0,0 +1,26 @@
import { getAuthHeaders, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function byOrganisation({
organisationId,
onSuccess,
onError,
}: {
organisationId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/projects/by-organisation`);
url.searchParams.set("organisationId", `${organisationId}`);
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to get projects by organisation (${res.status})`);
} else {
const data = await res.json();
onSuccess?.(data, res);
}
}

View File

@@ -0,0 +1,39 @@
import { getAuthHeaders, getServerURL } from "@/lib/utils";
import type { ServerQueryInput } from "..";
export async function create({
key,
name,
creatorId,
organisationId,
onSuccess,
onError,
}: {
key: string;
name: string;
creatorId: number;
organisationId: number;
} & ServerQueryInput) {
const url = new URL(`${getServerURL()}/project/create`);
url.searchParams.set("key", key.trim());
url.searchParams.set("name", name.trim());
url.searchParams.set("creatorId", `${creatorId}`);
url.searchParams.set("organisationId", `${organisationId}`);
const res = await fetch(url.toString(), {
headers: getAuthHeaders(),
});
if (!res.ok) {
const error = await res.text();
onError?.(error || `failed to create project (${res.status})`);
} else {
const data = await res.json();
if (!data.id) {
onError?.(`failed to create project (${res.status})`);
return;
}
onSuccess?.(data, res);
}
}

View File

@@ -0,0 +1,2 @@
export { byOrganisation } from "./byOrganisation";
export { create } from "./create";