mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(labs): let a person order the lab categories (#1349)
The labs list layout gains categoryOrder, a bounded allow-list of category keys (200 entries, 64 characters each, deduplicated, malformed values dropped on read). GET /api/labs/overview reads the caller's own layout and answers categories already in that order, so every client renders them as given. Listed categories come first; keys the data does not have are ignored; a category the order does not name follows in the default order (catalog panels, then the person's own by label, then other). Other left at the end of the order stays last, behind any category added later; moved anywhere else, it stays there. Settings > Arrangement > Labs shows a category order card while categories are on. It lists the overview's categories under their localised labels, sends the whole order after every move, moves the cached overview at once and refetches it. The card says how the default order works. The OpenAPI document carries the new field.
This commit is contained in:
1 parent
157809c24f
commit
928ade5109
28 files changed
+660
-34
No files matched your search
@@ -2,10 +2,12 @@ import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireRecordAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
import { categoriseOverview } from "@/lib/labs/marker-category";
|
||||
import { loadLabOverview } from "@/lib/labs/overview";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { resolveModuleListLayout } from "@/lib/settings/module-list-layout";
|
||||
import { labOverviewQuerySchema } from "@/lib/validations/labs";
|
||||
|
||||
/**
|
||||
@@ -18,7 +20,11 @@ import { labOverviewQuerySchema } from "@/lib/validations/labs";
|
||||
* `categories` and `grouped` are the list's categories, resolved here once
|
||||
* (`src/lib/labs/marker-category.ts`): order, label, members, counts and a
|
||||
* combined status. Labels follow the viewer's locale, so on a managed record
|
||||
* the caller's own language wins, not the record owner's.
|
||||
* the caller's own language wins, not the record owner's. The order is the
|
||||
* default one with the caller's `categoryOrder` (the labs list layout,
|
||||
* `PUT /api/settings/module-list-layout`) applied, for the same reason: it is
|
||||
* the viewer's presentation, so a person acting for another record sees the
|
||||
* categories in their own order. A client renders the order as given.
|
||||
*/
|
||||
export const GET = apiHandler(async (request: NextRequest) => {
|
||||
const { user, actor } = await requireRecordAuth("read", "labs");
|
||||
@@ -34,15 +40,33 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
const markers = await loadLabOverview(user.id);
|
||||
const [markers, layoutRow] = await Promise.all([
|
||||
loadLabOverview(user.id),
|
||||
prisma.user.findUnique({
|
||||
where: { id: actor.id },
|
||||
select: { moduleListLayoutJson: true },
|
||||
}),
|
||||
]);
|
||||
const { categoryOrder } = resolveModuleListLayout(
|
||||
layoutRow?.moduleListLayoutJson,
|
||||
).lists.labs;
|
||||
const locale = await resolveServerLocale({
|
||||
request,
|
||||
userLocale: actor.locale ?? null,
|
||||
});
|
||||
const { categories, grouped } = categoriseOverview(markers, locale);
|
||||
const { categories, grouped } = categoriseOverview(
|
||||
markers,
|
||||
locale,
|
||||
categoryOrder,
|
||||
);
|
||||
annotate({
|
||||
action: { name: "labs.overview" },
|
||||
meta: { markers: markers.length, categories: categories.length, grouped },
|
||||
meta: {
|
||||
markers: markers.length,
|
||||
categories: categories.length,
|
||||
grouped,
|
||||
categoryOrderLength: categoryOrder.length,
|
||||
},
|
||||
});
|
||||
return apiSuccess({ markers, categories, grouped });
|
||||
});
|
||||
@@ -58,14 +58,15 @@ export const GET = apiHandler(async () => {
|
||||
export const PUT = apiHandler(async (request: Request) => {
|
||||
const { user } = await requireActorAuth();
|
||||
|
||||
// 500 ids of up to 64 characters plus the envelope.
|
||||
// 500 ids plus 200 category keys of up to 64 characters, plus the envelope.
|
||||
const parsed = await parseJsonBody(request, moduleListLayoutPutSchema, {
|
||||
maxBytes: 48 * 1024,
|
||||
maxBytes: 64 * 1024,
|
||||
action: "settings.module-list-layout.validation-failed",
|
||||
});
|
||||
if (!parsed.ok) return parsed.error;
|
||||
|
||||
const { list, view, order, sortDir, groupBy, groupsOpen } = parsed.data;
|
||||
const { list, view, order, sortDir, groupBy, groupsOpen, categoryOrder } =
|
||||
parsed.data;
|
||||
// Preserve-when-absent: one stored read fills whichever field the body
|
||||
// left out, and every other list stays as stored. Only fields a person has
|
||||
// chosen are written, so a field never picked keeps following the default.
|
||||
@@ -79,6 +80,7 @@ export const PUT = apiHandler(async (request: Request) => {
|
||||
...(sortDir ? { sortDir } : {}),
|
||||
...(groupBy ? { groupBy } : {}),
|
||||
...(groupsOpen ? { groupsOpen } : {}),
|
||||
...(categoryOrder ? { categoryOrder: [...new Set(categoryOrder)] } : {}),
|
||||
};
|
||||
const next = { ...stored, [list]: nextChoices };
|
||||
await tx.user.update({
|
||||
@@ -98,6 +100,7 @@ export const PUT = apiHandler(async (request: Request) => {
|
||||
groupBy: layout.lists[list].groupBy,
|
||||
groupsOpen: layout.lists[list].groupsOpen,
|
||||
orderLength: layout.lists[list].order.length,
|
||||
categoryOrderLength: layout.lists[list].categoryOrder.length,
|
||||
},
|
||||
});
|
||||
return apiSuccess(layout);
|
||||
|
||||
Reference in new issue
Block a user