Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/app/src/actions/abbreviations/updateAbbreviation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use server";

import { revalidatePath } from "next/cache";
import { z } from "zod";
import { updateAbbreviation } from "@/db/crud/abbreviation";

const updateAbbreviationSchema = z.object({
projectId: z.string(),
abbreviationId: z.string(),
description: z.string().optional(),
code: z.string().optional(),
});

export async function updateAbbreviationAction(
projectId: string,
abbreviationId: string,
updates: { code?: string; description?: string },
) {
try {
const validatedData = updateAbbreviationSchema.parse({
projectId,
abbreviationId,
...updates,
});

// Prepare update payload
const updatePayload: Record<string, string> = {};
if (updates.description !== undefined) {
updatePayload.description = validatedData.description!;
}
if (updates.code !== undefined) {
updatePayload.code = validatedData.code!;
}

if (Object.keys(updatePayload).length > 0) {
// Update directly in the database
await updateAbbreviation(abbreviationId, updatePayload);
}

// Revalidate the page to refresh the data
revalidatePath(`/projects/${projectId}/abbreviations`);

return { success: true };
} catch (error) {
console.error("Error updating abbreviation:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
107 changes: 107 additions & 0 deletions packages/app/src/app/(app)/projects/[projectId]/abbreviations/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { getServerUser } from "@/lib/auth";
import { getProjectForUser } from "@/lib/dal/projects";
import { PageFrame } from "@/components/common/page-frame";
import { ALargeSmall } from "lucide-react";
import { readAllAbbreviationsForProject } from "@/db/crud/abbreviation";
import { AbbreviationsClient } from "@/components/abbreviations/abbreviations-client";
import tableConfig from "@assets/tableConfig.json";
import { GroupConfig } from "@common/db/schema/common";
import { TableName } from "@common/db/schema/data";

type Props = {
params: {
projectId: string;
};
searchParams: {
search?: string;
table?: string;
column?: string;
};
};

type AbbreviationData = Awaited<
ReturnType<typeof readAllAbbreviationsForProject>
>[number];

// Transform abbreviations data to include display names
function transformAbbreviationsWithDisplayNames(
abbreviations: AbbreviationData[],
) {
const config = tableConfig as GroupConfig[];

// Create lookup maps for efficiency
const tableDisplayMap = new Map(
config.map((table) => [table.dbName, table.label]),
);

const columnDisplayMap = new Map();
config.forEach((table) => {
const tableColumnMap = new Map();
table.columns.forEach((column) => {
// Map both dbName and nameCamelCase to the display name
tableColumnMap.set(column.dbName, column.label);
tableColumnMap.set(column.nameCamelCase, column.label);
});
columnDisplayMap.set(table.dbName, tableColumnMap);
});

return abbreviations.map((abbr) => {
const tableDisplayName =
tableDisplayMap.get(abbr.table as TableName) || abbr.table;
const columnDisplayName =
columnDisplayMap.get(abbr.table)?.get(abbr.column) || abbr.column;

return {
...abbr,
tableDisplayName,
columnDisplayName,
};
});
}

export default async function AbbreviationsPage({
params,
searchParams,
}: Props) {
const user = await getServerUser();
const { project } = await getProjectForUser(user, params.projectId);

const allAbbreviations = await readAllAbbreviationsForProject(
params.projectId,
);

// Transform data to include display names
const abbreviationsWithDisplayNames =
transformAbbreviationsWithDisplayNames(allAbbreviations);

const breadcrumbs = [
{
label: "Projects",
href: "/projects",
},
{
label: project.name,
href: `/projects/${params.projectId}`,
},
{
label: "Abbreviations",
href: `/projects/${params.projectId}/abbreviations`,
},
];

return (
<PageFrame
breadcrumbs={breadcrumbs}
icon={<ALargeSmall />}
title="Abbreviations"
>
<AbbreviationsClient
projectId={params.projectId}
initialData={abbreviationsWithDisplayNames}
searchQuery={searchParams.search || ""}
selectedTable={searchParams.table || "all"}
selectedColumn={searchParams.column || null}
/>
</PageFrame>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { getServerUser } from "@/lib/auth";
import { getProjectForUser } from "@/lib/dal/projects";
import { db } from "@/db";
import { abbreviation } from "@common/db/schema/abbreviation";
import { eq } from "drizzle-orm";
import { revalidateTag } from "next/cache";

const patchBodySchema = z.object({
description: z.string().optional(),
code: z.string().optional(),
});

type RequestArgs = {
params: {
projectId: string;
abbreviationId: string;
};
};

export async function PATCH(request: NextRequest, { params }: RequestArgs) {
const projectId = params.projectId;
const abbreviationId = params.abbreviationId;

const body = patchBodySchema.safeParse(await request.json());
if (body.error) {
return NextResponse.json(
{ message: "Invalid body", errors: body.error.errors },
{ status: 400 },
);
}

const user = await getServerUser();
const userProject = await getProjectForUser(user, projectId);
if (!userProject) {
return NextResponse.json({ message: "Project not found" }, { status: 404 });
}

try {
// Build update payload
const updatePayload: Record<string, any> = {
updatedAt: new Date(),
};

if (body.data.description !== undefined) {
updatePayload.description = body.data.description;
}
if (body.data.code !== undefined) {
updatePayload.code = body.data.code;
}

// Update the abbreviation
const [updatedAbbreviation] = await db
.update(abbreviation)
.set(updatePayload)
.where(eq(abbreviation.id, abbreviationId))
.returning();

if (!updatedAbbreviation) {
return NextResponse.json(
{ message: "Abbreviation not found" },
{ status: 404 },
);
}

// Revalidate the cache
revalidateTag("abbreviations");

const response = NextResponse.json(updatedAbbreviation);

return response;
} catch (error) {
console.error("Error updating abbreviation:", error);
return NextResponse.json(
{ message: "Internal server error" },
{ status: 500 },
);
}
}
104 changes: 104 additions & 0 deletions packages/app/src/app/api/projects/[projectId]/abbreviations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { getServerUser } from "@/lib/auth";
import { getProjectForUser } from "@/lib/dal/projects";
import { readAllAbbreviationsForProject } from "@/db/crud/abbreviation";
import { unstable_cache } from "next/cache";

const LIMIT_MAX = 1000;

const queryParamsSchema = z.object({
limit: z.number().min(1).max(LIMIT_MAX),
offset: z.number().min(0),
searchQuery: z.string().optional(),
selectedTable: z.string().optional(),
});

type RequestArgs = {
params: {
projectId: string;
};
};

// Cached function for fetching abbreviations
const getCachedAbbreviations = unstable_cache(
async (projectId: string) => {
return await readAllAbbreviationsForProject(projectId);
},
["abbreviations"],
{
tags: ["abbreviations"],
revalidate: 300, // 5 minutes
},
);

export async function GET(request: NextRequest, { params }: RequestArgs) {
const projectId = params.projectId;

const searchParams = request.nextUrl.searchParams;
const limit =
searchParams.get("limit") !== null
? parseInt(searchParams.get("limit")!)
: NaN;
const offset =
searchParams.get("offset") !== null
? parseInt(searchParams.get("offset")!)
: NaN;
const searchQuery = searchParams.get("searchQuery") || undefined;
const selectedTable = searchParams.get("selectedTable") || undefined;

try {
queryParamsSchema.parse({ limit, offset, searchQuery, selectedTable });
} catch (error) {
return NextResponse.json(
{ message: "Invalid query parameters", error },
{ status: 400 },
);
}

const user = await getServerUser();
const userProject = await getProjectForUser(user, projectId);
if (!userProject) {
return NextResponse.json({ message: "Project not found" }, { status: 404 });
}

try {
const allAbbreviations = await getCachedAbbreviations(projectId);

// Filter by search query
let filtered = allAbbreviations;
if (searchQuery) {
filtered = filtered.filter(
(abbr) =>
abbr.code.toLowerCase().includes(searchQuery.toLowerCase()) ||
(abbr.description &&
abbr.description
.toLowerCase()
.includes(searchQuery.toLowerCase())) ||
abbr.table.toLowerCase().includes(searchQuery.toLowerCase()) ||
abbr.column.toLowerCase().includes(searchQuery.toLowerCase()),
);
}

// Filter by table
if (selectedTable && selectedTable !== "all") {
filtered = filtered.filter((abbr) => abbr.table === selectedTable);
}

// Apply pagination
const paginated = filtered.slice(offset, offset + limit);

const response = NextResponse.json({
rows: paginated,
rowCount: filtered.length,
});

return response;
} catch (error) {
console.error("Error fetching abbreviations:", error);
return NextResponse.json(
{ message: "Internal server error" },
{ status: 500 },
);
}
}
Loading