mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
merge: W3 labs biomarker list + hidden flag into v1.22
This commit is contained in:
commit
37bfd7bb4e
16 files changed
+193
-78
No files matched your search
@@ -6741,6 +6741,8 @@ paths:
|
||||
- type: string
|
||||
const: ""
|
||||
- type: "null"
|
||||
hidden:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: Updated biomarker.
|
||||
@@ -20212,6 +20214,8 @@ components:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
hidden:
|
||||
type: boolean
|
||||
createdAt:
|
||||
type: string
|
||||
updatedAt:
|
||||
@@ -20225,11 +20229,14 @@ components:
|
||||
- panel
|
||||
- hasContext
|
||||
- context
|
||||
- hidden
|
||||
- createdAt
|
||||
- updatedAt
|
||||
additionalProperties: false
|
||||
description: A user-scoped catalog marker. `context` is the decrypted per-marker note (or null); `hasContext` flags its
|
||||
presence. A `LabResult` linking this marker resolves its unit + reference bounds from here.
|
||||
presence. `hidden` drops the marker from the active catalog list and the lab-entry pickers without deleting it
|
||||
(its readings and unit/range survive). A `LabResult` linking this marker resolves its unit + reference bounds
|
||||
from here.
|
||||
CreateBiomarkerResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
+8
-1
@@ -6720,7 +6720,14 @@
|
||||
"savedToast": "Biomarker gespeichert",
|
||||
"updatedToast": "Biomarker aktualisiert",
|
||||
"saveError": "Biomarker konnte nicht gespeichert werden"
|
||||
}
|
||||
},
|
||||
"hide": "Ausblenden",
|
||||
"unhide": "Wiederherstellen",
|
||||
"hiddenToast": "Biomarker ausgeblendet",
|
||||
"unhiddenToast": "Biomarker wiederhergestellt",
|
||||
"hideError": "Biomarker konnte nicht aktualisiert werden",
|
||||
"allHidden": "Alle Biomarker sind ausgeblendet.",
|
||||
"hiddenSection": "Ausgeblendet"
|
||||
},
|
||||
"catalog": {
|
||||
"total-cholesterol": "Gesamtcholesterin",
|
||||
|
||||
+8
-1
@@ -6720,7 +6720,14 @@
|
||||
"savedToast": "Biomarker saved",
|
||||
"updatedToast": "Biomarker updated",
|
||||
"saveError": "Could not save the biomarker"
|
||||
}
|
||||
},
|
||||
"hide": "Hide",
|
||||
"unhide": "Restore",
|
||||
"hiddenToast": "Biomarker hidden",
|
||||
"unhiddenToast": "Biomarker restored",
|
||||
"hideError": "Could not update the biomarker",
|
||||
"allHidden": "Every biomarker is hidden.",
|
||||
"hiddenSection": "Hidden"
|
||||
},
|
||||
"catalog": {
|
||||
"total-cholesterol": "Total cholesterol",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- v1.22.0 — Biomarker catalog: per-marker "hidden" flag.
|
||||
--
|
||||
-- A marker the user no longer needs is hidden rather than deleted: it leaves
|
||||
-- the active catalog list and the lab-entry pickers, but its readings and its
|
||||
-- canonical unit/reference-range definition survive (unhide restores it). A
|
||||
-- plain additive boolean column with a server-side default; no existing row is
|
||||
-- touched (every back-filled row defaults to visible). `IF NOT EXISTS` makes
|
||||
-- the rerun safe.
|
||||
ALTER TABLE "biomarkers" ADD COLUMN IF NOT EXISTS "hidden" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -3657,6 +3657,10 @@ model Biomarker {
|
||||
/// Optional panel grouping moved off the per-entry row, e.g.
|
||||
/// "Großes Blutbild". NULL = standalone marker.
|
||||
panel String?
|
||||
/// A marker the user no longer needs: dropped from the active catalog list
|
||||
/// and from the lab-entry pickers, but never deleted — its readings and
|
||||
/// canonical unit/range definition survive.
|
||||
hidden Boolean @default(false) @map("hidden")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ function serialiseBiomarker(row: {
|
||||
lowerBound: number | null;
|
||||
upperBound: number | null;
|
||||
panel: string | null;
|
||||
hidden: boolean;
|
||||
contextEncrypted: Uint8Array | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -54,6 +55,7 @@ function serialiseBiomarker(row: {
|
||||
context: row.contextEncrypted
|
||||
? decryptContextFromBytes(row.contextEncrypted)
|
||||
: null,
|
||||
hidden: row.hidden,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
@@ -137,6 +139,7 @@ export const PUT = apiHandler(
|
||||
if (d.lowerBound !== undefined) data.lowerBound = d.lowerBound;
|
||||
if (d.upperBound !== undefined) data.upperBound = d.upperBound;
|
||||
if (d.panel !== undefined) data.panel = d.panel;
|
||||
if (d.hidden !== undefined) data.hidden = d.hidden;
|
||||
|
||||
// Inverted-range guard for a PARTIAL bound update. The schema refine only
|
||||
// fires when both bounds arrive together; moving a single bound past the
|
||||
|
||||
@@ -50,6 +50,7 @@ function serialiseBiomarker(row: {
|
||||
lowerBound: number | null;
|
||||
upperBound: number | null;
|
||||
panel: string | null;
|
||||
hidden: boolean;
|
||||
contextEncrypted: Uint8Array | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@@ -63,6 +64,7 @@ function serialiseBiomarker(row: {
|
||||
panel: row.panel,
|
||||
hasContext: row.contextEncrypted !== null,
|
||||
context: decryptContextSoft(row.contextEncrypted),
|
||||
hidden: row.hidden,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
@@ -71,9 +73,11 @@ function serialiseBiomarker(row: {
|
||||
export const GET = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
// Visible markers first, then hidden — the manager renders them in two
|
||||
// sections and the lab-entry pickers drop the hidden tail client-side.
|
||||
const rows = await prisma.biomarker.findMany({
|
||||
where: { userId: user.id },
|
||||
orderBy: [{ name: "asc" }],
|
||||
orderBy: [{ hidden: "asc" }, { name: "asc" }],
|
||||
});
|
||||
|
||||
annotate({
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { FlaskConical, Pencil, Plus } from "lucide-react";
|
||||
import { Eye, EyeOff, FlaskConical, Pencil, Plus } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { DeleteButton } from "@/components/data-list";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { apiDelete, apiGet } from "@/lib/api/api-fetch";
|
||||
import { apiDelete, apiGet, apiPut } from "@/lib/api/api-fetch";
|
||||
import { formatReferenceRange } from "@/lib/labs/reference-range";
|
||||
import { formatLabValue } from "@/lib/labs/format-value";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -21,12 +20,18 @@ import { BiomarkerForm } from "./biomarker-form";
|
||||
import type { BiomarkerDto, BiomarkerListResponse } from "./types";
|
||||
|
||||
/**
|
||||
* v1.18.1 — the Biomarker catalog manager.
|
||||
* v1.18.1 — the Biomarker catalog manager. v1.22 — compact editable rows.
|
||||
*
|
||||
* Lives on the Labs page (not Settings) because the catalog IS the Labs
|
||||
* feature's primary object. Lists every defined marker with its unit +
|
||||
* reference range, opens a define-new / edit sheet, and wires delete (the
|
||||
* `onDelete: SetNull` FK keeps existing readings, just unlinks them).
|
||||
* feature's primary object. Each marker renders as a single compact row
|
||||
* (name + unit inline, reference range / panel on a muted sub-line) with a
|
||||
* hide toggle, an edit button (reusing the define/edit sheet), and a delete
|
||||
* (the `onDelete: SetNull` FK keeps existing readings, just unlinks them).
|
||||
*
|
||||
* "Hide" is a soft remove: a marker the user no longer needs leaves the
|
||||
* active list and the lab-entry pickers but keeps its readings + canonical
|
||||
* unit/range. Hidden markers collect in a separate section where they can be
|
||||
* restored or deleted outright.
|
||||
*/
|
||||
export function BiomarkerManager() {
|
||||
const { t } = useTranslations();
|
||||
@@ -50,6 +55,20 @@ export function BiomarkerManager() {
|
||||
onError: () => toast.error(t("labs.biomarker.deleteError")),
|
||||
});
|
||||
|
||||
const hiddenMutation = useMutation({
|
||||
mutationFn: ({ id, hidden }: { id: string; hidden: boolean }) =>
|
||||
apiPut(`/api/biomarkers/${id}`, { hidden }),
|
||||
onSuccess: (_data, { hidden }) => {
|
||||
toast.success(
|
||||
hidden
|
||||
? t("labs.biomarker.hiddenToast")
|
||||
: t("labs.biomarker.unhiddenToast"),
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.biomarkers() });
|
||||
},
|
||||
onError: () => toast.error(t("labs.biomarker.hideError")),
|
||||
});
|
||||
|
||||
function openNew() {
|
||||
setEditing(null);
|
||||
setFormOpen(true);
|
||||
@@ -68,6 +87,73 @@ export function BiomarkerManager() {
|
||||
}
|
||||
|
||||
const markers = data?.biomarkers ?? [];
|
||||
const visible = markers.filter((m) => !m.hidden);
|
||||
const hidden = markers.filter((m) => m.hidden);
|
||||
|
||||
function renderRow(marker: BiomarkerDto) {
|
||||
const range = formatReferenceRange(
|
||||
marker.lowerBound,
|
||||
marker.upperBound,
|
||||
formatLabValue,
|
||||
);
|
||||
return (
|
||||
<li
|
||||
key={marker.id}
|
||||
data-hidden={marker.hidden ? "true" : undefined}
|
||||
className="border-border bg-background/30 flex min-h-12 items-center gap-2 rounded-md border px-3 py-2 data-[hidden=true]:opacity-70"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2">
|
||||
<span className="truncate text-sm font-medium">{marker.name}</span>
|
||||
<span className="text-muted-foreground text-xs">{marker.unit}</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{range
|
||||
? `${t("labs.referenceLabel")} ${range} ${marker.unit}`
|
||||
: t("labs.biomarker.noRange")}
|
||||
{marker.panel ? ` · ${marker.panel}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-11 sm:size-9"
|
||||
onClick={() =>
|
||||
hiddenMutation.mutate({ id: marker.id, hidden: !marker.hidden })
|
||||
}
|
||||
disabled={hiddenMutation.isPending}
|
||||
aria-label={
|
||||
marker.hidden
|
||||
? t("labs.biomarker.unhide")
|
||||
: t("labs.biomarker.hide")
|
||||
}
|
||||
>
|
||||
{marker.hidden ? (
|
||||
<Eye className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-11 sm:size-9"
|
||||
onClick={() => openEdit(marker)}
|
||||
aria-label={t("labs.biomarker.edit")}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<DeleteButton
|
||||
onConfirm={() => deleteMutation.mutate(marker.id)}
|
||||
title={t("labs.biomarker.deleteConfirmTitle")}
|
||||
description={t("labs.biomarker.deleteConfirmDescription")}
|
||||
confirmLabel={t("labs.biomarker.delete")}
|
||||
className="size-11 sm:size-9"
|
||||
iconClassName="h-4 w-4"
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -84,7 +170,7 @@ export function BiomarkerManager() {
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 3 }, (_, i) => (
|
||||
<Skeleton key={i} className="h-14 w-full" />
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : isError ? (
|
||||
@@ -101,60 +187,24 @@ export function BiomarkerManager() {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{markers.map((marker) => {
|
||||
const range = formatReferenceRange(
|
||||
marker.lowerBound,
|
||||
marker.upperBound,
|
||||
formatLabValue,
|
||||
);
|
||||
return (
|
||||
<li key={marker.id}>
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-between gap-3 py-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2">
|
||||
<span className="truncate text-sm font-medium">
|
||||
{marker.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{marker.unit}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{range
|
||||
? `${t("labs.referenceLabel")} ${range} ${marker.unit}`
|
||||
: t("labs.biomarker.noRange")}
|
||||
{marker.panel ? ` · ${marker.panel}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-9"
|
||||
onClick={() => openEdit(marker)}
|
||||
aria-label={t("labs.biomarker.edit")}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<DeleteButton
|
||||
onConfirm={() => deleteMutation.mutate(marker.id)}
|
||||
title={t("labs.biomarker.deleteConfirmTitle")}
|
||||
description={t(
|
||||
"labs.biomarker.deleteConfirmDescription",
|
||||
)}
|
||||
confirmLabel={t("labs.biomarker.delete")}
|
||||
className="size-9"
|
||||
iconClassName="h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="space-y-4">
|
||||
{visible.length > 0 ? (
|
||||
<ul className="space-y-2">{visible.map(renderRow)}</ul>
|
||||
) : (
|
||||
<p className="text-muted-foreground py-2 text-sm">
|
||||
{t("labs.biomarker.allHidden")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hidden.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||
{t("labs.biomarker.hiddenSection")}
|
||||
</p>
|
||||
<ul className="space-y-2">{hidden.map(renderRow)}</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ResponsiveSheet
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "recharts";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CHART_HEIGHT_PX } from "@/lib/charts/constants";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { formatDateShort } from "@/lib/format";
|
||||
import { formatLabValue } from "@/lib/labs/format-value";
|
||||
@@ -128,7 +127,7 @@ export function LabBiomarkerChart({
|
||||
{t("labs.chart.empty")}
|
||||
</p>
|
||||
) : (
|
||||
<div style={{ height: CHART_HEIGHT_PX }}>
|
||||
<div className="h-[240px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={points}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { LearnMoreLink } from "@/components/ui/learn-more-link";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { apiDelete, apiGet } from "@/lib/api/api-fetch";
|
||||
import { classifyReferenceRange } from "@/lib/labs/reference-range";
|
||||
@@ -228,8 +227,6 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
|
||||
<p className="text-muted-foreground text-sm">{marker.context}</p>
|
||||
) : null}
|
||||
|
||||
<LearnMoreLink concept="LAB_BIOMARKER" />
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
{isLoading ? (
|
||||
|
||||
@@ -110,8 +110,12 @@ export function LabForm({
|
||||
null,
|
||||
);
|
||||
|
||||
const markers = catalog?.biomarkers ?? [];
|
||||
const selected = markers.find((m) => m.id === biomarkerId);
|
||||
const allMarkers = catalog?.biomarkers ?? [];
|
||||
const selected = allMarkers.find((m) => m.id === biomarkerId);
|
||||
// v1.22 — hidden markers drop from the picker, but keep the currently
|
||||
// selected one visible (e.g. editing a reading whose marker was later
|
||||
// hidden) so the Select still resolves its label.
|
||||
const markers = allMarkers.filter((m) => !m.hidden || m.id === biomarkerId);
|
||||
|
||||
function handleSelect(next: string) {
|
||||
if (next === DEFINE_NEW) {
|
||||
|
||||
@@ -220,11 +220,12 @@ export function OcrReviewDialog({
|
||||
if (file) onFilePicked(file);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
disabled={extract.isPending}
|
||||
className="border-muted-foreground/25 hover:bg-muted/50 focus-visible:ring-ring flex min-h-44 w-full flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-6 text-center transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-60"
|
||||
className="border-muted-foreground/25 hover:bg-muted/50 h-auto min-h-44 w-full flex-col gap-3 rounded-lg border-dashed p-6 text-center whitespace-normal"
|
||||
>
|
||||
{extract.isPending ? (
|
||||
<>
|
||||
@@ -261,7 +262,7 @@ export function OcrReviewDialog({
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 py-2">
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface BiomarkerDto {
|
||||
panel: string | null;
|
||||
hasContext: boolean;
|
||||
context: string | null;
|
||||
/** v1.22 — a hidden marker drops from the active list + lab-entry pickers. */
|
||||
hidden: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ createBiomarkerSchema.meta({
|
||||
updateBiomarkerSchema.meta({
|
||||
id: "UpdateBiomarkerRequest",
|
||||
description:
|
||||
"Partial edit of a biomarker. An omitted key leaves the column untouched; an explicit `null` on `context` / `panel` / a bound clears it. A rename that collides with another of the caller's markers is rejected 409.",
|
||||
"Partial edit of a biomarker. An omitted key leaves the column untouched; an explicit `null` on `context` / `panel` / a bound clears it. Set `hidden` to drop / restore the marker in the active list + lab-entry pickers. A rename that collides with another of the caller's markers is rejected 409.",
|
||||
});
|
||||
|
||||
const biomarkerRow = z
|
||||
@@ -43,13 +43,14 @@ const biomarkerRow = z
|
||||
panel: z.string().nullable(),
|
||||
hasContext: z.boolean(),
|
||||
context: z.string().nullable(),
|
||||
hidden: z.boolean(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
.meta({
|
||||
id: "Biomarker",
|
||||
description:
|
||||
"A user-scoped catalog marker. `context` is the decrypted per-marker note (or null); `hasContext` flags its presence. A `LabResult` linking this marker resolves its unit + reference bounds from here.",
|
||||
"A user-scoped catalog marker. `context` is the decrypted per-marker note (or null); `hasContext` flags its presence. `hidden` drops the marker from the active catalog list and the lab-entry pickers without deleting it (its readings and unit/range survive). A `LabResult` linking this marker resolves its unit + reference bounds from here.",
|
||||
});
|
||||
|
||||
const listResponse = z
|
||||
|
||||
@@ -72,4 +72,21 @@ describe("updateBiomarkerSchema", () => {
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts a hidden toggle", () => {
|
||||
const r = updateBiomarkerSchema.safeParse({ hidden: true });
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves hidden undefined when omitted", () => {
|
||||
const r = updateBiomarkerSchema.safeParse({ unit: "mmol/L" });
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data.hidden).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects a non-boolean hidden", () => {
|
||||
const r = updateBiomarkerSchema.safeParse({ hidden: "yes" });
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,9 @@ export const updateBiomarkerSchema = z
|
||||
upperBound: upperBound.or(z.null()),
|
||||
context: optionalContext.or(z.null()),
|
||||
panel: optionalPanel.or(z.null()),
|
||||
// v1.22 — hide / unhide a marker. An omitted key leaves visibility
|
||||
// untouched; `true` drops the marker from the active list + pickers.
|
||||
hidden: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(d) =>
|
||||
|
||||
Reference in new issue
Block a user