mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
customise organisation issue types
This commit is contained in:
@@ -145,6 +145,44 @@ export async function replaceIssueStatus(organisationId: number, oldStatus: stri
|
||||
return { updated: result.rowCount ?? 0 };
|
||||
}
|
||||
|
||||
export async function getIssueTypeCountByOrganisation(organisationId: number, type: string) {
|
||||
const { Project } = await import("@sprint/shared");
|
||||
|
||||
const projects = await db
|
||||
.select({ id: Project.id })
|
||||
.from(Project)
|
||||
.where(eq(Project.organisationId, organisationId));
|
||||
const projectIds = projects.map((p) => p.id);
|
||||
|
||||
if (projectIds.length === 0) return { count: 0 };
|
||||
|
||||
const [result] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(Issue)
|
||||
.where(and(eq(Issue.type, type), inArray(Issue.projectId, projectIds)));
|
||||
|
||||
return { count: result?.count ?? 0 };
|
||||
}
|
||||
|
||||
export async function replaceIssueType(organisationId: number, oldType: string, newType: string) {
|
||||
const { Project } = await import("@sprint/shared");
|
||||
|
||||
const projects = await db
|
||||
.select({ id: Project.id })
|
||||
.from(Project)
|
||||
.where(eq(Project.organisationId, organisationId));
|
||||
const projectIds = projects.map((p) => p.id);
|
||||
|
||||
if (projectIds.length === 0) return { updated: 0 };
|
||||
|
||||
const result = await db
|
||||
.update(Issue)
|
||||
.set({ type: newType })
|
||||
.where(and(eq(Issue.type, oldType), inArray(Issue.projectId, projectIds)));
|
||||
|
||||
return { updated: result.rowCount ?? 0 };
|
||||
}
|
||||
|
||||
export async function getIssuesWithUsersByProject(projectId: number): Promise<IssueResponse[]> {
|
||||
const Creator = aliasedTable(User, "Creator");
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export async function updateOrganisation(
|
||||
iconURL?: string | null;
|
||||
statuses?: Record<string, string>;
|
||||
features?: Record<string, boolean>;
|
||||
issueTypes?: Record<string, { icon: string; color: string }>;
|
||||
},
|
||||
) {
|
||||
const [organisation] = await db
|
||||
|
||||
@@ -50,7 +50,9 @@ const main = async () => {
|
||||
|
||||
"/issues/by-project": withGlobal(withAuth(routes.issuesByProject)),
|
||||
"/issues/replace-status": withGlobal(withAuth(withCSRF(routes.issuesReplaceStatus))),
|
||||
"/issues/replace-type": withGlobal(withAuth(withCSRF(routes.issuesReplaceType))),
|
||||
"/issues/status-count": withGlobal(withAuth(routes.issuesStatusCount)),
|
||||
"/issues/type-count": withGlobal(withAuth(routes.issuesTypeCount)),
|
||||
"/issues/all": withGlobal(withAuth(routes.issues)),
|
||||
"/issue-comments/by-issue": withGlobal(withAuth(routes.issueCommentsByIssue)),
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import issueCommentsByIssue from "./issue-comments/by-issue";
|
||||
import issues from "./issues/all";
|
||||
import issuesByProject from "./issues/by-project";
|
||||
import issuesReplaceStatus from "./issues/replace-status";
|
||||
import issuesReplaceType from "./issues/replace-type";
|
||||
import issuesStatusCount from "./issues/status-count";
|
||||
import issuesTypeCount from "./issues/type-count";
|
||||
import organisationAddMember from "./organisation/add-member";
|
||||
import organisationById from "./organisation/by-id";
|
||||
import organisationsByUser from "./organisation/by-user";
|
||||
@@ -64,7 +66,9 @@ export const routes = {
|
||||
issuesByProject,
|
||||
issues,
|
||||
issuesReplaceStatus,
|
||||
issuesReplaceType,
|
||||
issuesStatusCount,
|
||||
issuesTypeCount,
|
||||
|
||||
organisationCreate,
|
||||
organisationById,
|
||||
|
||||
@@ -48,9 +48,6 @@ export default async function issueUpdate(req: AuthedRequest) {
|
||||
if (!requesterMember) {
|
||||
return errorResponse("you are not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
if (requesterMember.role !== "owner" && requesterMember.role !== "admin") {
|
||||
return errorResponse("only organisation owners and admins can edit issues", "PERMISSION_DENIED", 403);
|
||||
}
|
||||
|
||||
let issue: IssueRecord | undefined = existingIssue;
|
||||
if (hasIssueFieldUpdates) {
|
||||
|
||||
24
packages/backend/src/routes/issues/replace-type.ts
Normal file
24
packages/backend/src/routes/issues/replace-type.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IssuesReplaceTypeRequestSchema } from "@sprint/shared";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { getOrganisationMemberRole, replaceIssueType } from "../../db/queries";
|
||||
import { errorResponse, parseJsonBody } from "../../validation";
|
||||
|
||||
export default async function issuesReplaceType(req: AuthedRequest) {
|
||||
const parsed = await parseJsonBody(req, IssuesReplaceTypeRequestSchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
const { organisationId, oldType, newType } = parsed.data;
|
||||
|
||||
const membership = await getOrganisationMemberRole(organisationId, req.userId);
|
||||
if (!membership) {
|
||||
return errorResponse("not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
|
||||
if (membership.role !== "owner" && membership.role !== "admin") {
|
||||
return errorResponse("only admins and owners can replace types", "PERMISSION_DENIED", 403);
|
||||
}
|
||||
|
||||
const result = await replaceIssueType(organisationId, oldType, newType);
|
||||
|
||||
return Response.json(result);
|
||||
}
|
||||
21
packages/backend/src/routes/issues/type-count.ts
Normal file
21
packages/backend/src/routes/issues/type-count.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IssuesTypeCountQuerySchema } from "@sprint/shared";
|
||||
import type { AuthedRequest } from "../../auth/middleware";
|
||||
import { getIssueTypeCountByOrganisation, getOrganisationMemberRole } from "../../db/queries";
|
||||
import { errorResponse, parseQueryParams } from "../../validation";
|
||||
|
||||
export default async function issuesTypeCount(req: AuthedRequest) {
|
||||
const url = new URL(req.url);
|
||||
const parsed = parseQueryParams(url, IssuesTypeCountQuerySchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
const { organisationId, type } = parsed.data;
|
||||
|
||||
const membership = await getOrganisationMemberRole(organisationId, req.userId);
|
||||
if (!membership) {
|
||||
return errorResponse("not a member of this organisation", "NOT_MEMBER", 403);
|
||||
}
|
||||
|
||||
const result = await getIssueTypeCountByOrganisation(organisationId, type);
|
||||
|
||||
return Response.json(result);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export default async function organisationUpdate(req: AuthedRequest) {
|
||||
const parsed = await parseJsonBody(req, OrgUpdateRequestSchema);
|
||||
if ("error" in parsed) return parsed.error;
|
||||
|
||||
const { id, name, description, slug, iconURL, statuses, features } = parsed.data;
|
||||
const { id, name, description, slug, iconURL, statuses, features, issueTypes } = parsed.data;
|
||||
|
||||
const existingOrganisation = await getOrganisationById(id);
|
||||
if (!existingOrganisation) {
|
||||
@@ -22,9 +22,9 @@ export default async function organisationUpdate(req: AuthedRequest) {
|
||||
return errorResponse("only owners and admins can edit organisations", "PERMISSION_DENIED", 403);
|
||||
}
|
||||
|
||||
if (!name && !description && !slug && !statuses && !features && iconURL === undefined) {
|
||||
if (!name && !description && !slug && !statuses && !features && !issueTypes && iconURL === undefined) {
|
||||
return errorResponse(
|
||||
"at least one of name, description, slug, iconURL, or statuses must be provided",
|
||||
"at least one of name, description, slug, iconURL, statuses, issueTypes, or features must be provided",
|
||||
"NO_UPDATES",
|
||||
400,
|
||||
);
|
||||
@@ -37,6 +37,7 @@ export default async function organisationUpdate(req: AuthedRequest) {
|
||||
iconURL,
|
||||
statuses,
|
||||
features,
|
||||
issueTypes,
|
||||
});
|
||||
|
||||
return Response.json(organisation);
|
||||
|
||||
Reference in New Issue
Block a user